AtCoderレートのバッジを表示する
#Rust #AtCoder
まとめ
AtCoderレートのバッジを表示する方法を調べた
Shields.ioのバッジを生成するためのバックエンドをVercelのServerless Functionsで作成した
ac-shields-badges
こんな感じで表示される
https://img.shields.io/endpoint?url=https%3A%2F%2Fac-shields-badges.vercel.app%2Fapi%2Fac-rate%3Fuser_id%3Demanon001%26contest_type%3Dalgorithm#.png
https://img.shields.io/endpoint?url=https%3A%2F%2Fac-shields-badges.vercel.app%2Fapi%2Fac-rate%3Fuser_id%3Demanon001%26contest_type%3Dheuristic#.png
ソースコード
https://github.com/emanon001/ac-shields-badges
バッジを表示する方法
Shields.ioを使用するのが簡単そう
今回の場合はバッジのメッセージが動的に変わるため、JSONを返すendpointを作成する必要がある
に書いてある通りに作成する
エンドポイントの作成
今回はVercelのServerless Functionsを作成した
Rustを使いたかったのでを使用した
ref. VercelのServerless FunctionsをRustで書く
エンドポイントの仕様
/api/ac-rate?user_id=<user_id>&contest_type=<contest_type>
user_idはAtCoderユーザーID
contest_typeはコンテストの種類。algorithm or heuristic
最新のレートはどのように取得するのか
AtCoderのプロフィールページの内容から取得する
https://atcoder.jp/users/<user_id>?contestType=<contest_type>&lang=en
user_id: AtCoderのユーザーID
contest_type: algo | heuristic
algorithm ではないことに注意
contestType query parameter 自体を指定しない場合は algo が指定された扱いとなる
スクレイピングは easy-scraperを使用
Ratedコンテストに一度も参加していない場合はHTMLの構造が変わるので注意すること
code:rust
pub fn get_ac_rate(
user_id: &UserId,
contest_type: ContestType,
) -> Result<Option<Rate>, Box<dyn std::error::Error>> {
let doc = reqwest::blocking::get(user_profile_url(user_id, contest_type))?.text()?;
if doc.contains("This user has not competed in a rated contest yet.") {
return Ok(None);
}
let pat = Pattern::new(
r#"
<table>
<tbody>
<tr>
<th>Rating</th>
<td>
<img>
<span>{{rate}}</span>
</td>
</tr>
</tbody>
</table>"#,
)?;
let ms = pat.matches(&doc);
match ms.first() {
Some(m) => {
let rate: u32 = m"rate".parse()?;
Ok(Some(Rate(rate)))
}
None => Err("rate not found".into()),
}
}
Shields.io 向けのレスポンスを返す
に書いてある通りに作成する
レート帯によってメッセージの色を変更する
code:rust
#derive(Serialize)
#serde(rename_all = "camelCase")
pub struct ShieldsResponseBody {
schema_version: u32,
label: String,
message: String,
color: String,
}
impl ShieldsResponseBody {
pub fn new_ac_rate_response(contest_type: ContestType, rate: Option<Rate>) -> Self {
let label = format!(
"AtCoder{}",
match contest_type {
ContestType::Algorithm => "Ⓐ",
ContestType::Heuristic => "Ⓗ",
}
);
let (message, color) = match rate {
Some(rate) => {
let message = rate.to_string();
let color = match rate.0 {
..=399 => "808080",
400..=799 => "804000",
800..=1199 => "008000",
1200..=1599 => "00C0C0",
1600..=1999 => "0000FF",
2000..=2399 => "C0C000",
2400..=2799 => "FF8000",
_ => "FF0000",
}
.to_string();
(message, color)
}
None => ("-".to_owned(), "000000".to_owned()),
};
Self {
schema_version: 1,
label,
message,
color,
}
}
}
キャッシュ設定
リクエスト毎に毎回AtCoderに問い合わせたくないので、VercelのCDNでレスポンスをキャッシュする
https://vercel.com/docs/concepts/functions/serverless-functions/edge-caching
レートはそこまで即時性が必要ないため一日間キャッシュする
max-age=0, s-maxage=86400 を採用
code:rust
let response = Response::builder()
.status(StatusCode::OK)
.header("Content-Type", "application/json; charset=utf-8")
.header("Cache-Control", "max-age=0, s-maxage=86400")
.body(serde_json::to_string(&body).unwrap())
.expect("Internal Server Error");
RateLimitの設定
AtCoder側へのリクエスト数を制限したいので、リスエスト前にRateLimitを確認する
簡単な方法としては、グローバル変数にリクエスト日時を保持することが考えられる
変更可能なグローバル変数は once_cell で定義できる
code:rust
static ATCODER_REQUEST_TIME_HISTORY: Lazy<Mutex<VecDeque<Instant>>> = Lazy::new(|| {
let m = VecDeque::new();
Mutex::new(m)
});
バッジを表示する
以下のJavaScriptコードを実行して、バッジのURLを作成する
code:js
const getBadgeUrl = (userId, contestType) => {
const endpoint = encodeURIComponent(
https://ac-shields-badges.vercel.app/api/ac-rate?user_id=${userId}&contest_type=${contestType}
);
return https://img.shields.io/endpoint?url=${endpoint};
};
const userId = "emanon001"; // AtCoderユーザーID
console.table({
algorithm: getBadgeUrl(userId, "algorithm"),
heuristic: getBadgeUrl(userId, "heuristic"),
});
Scrapbox内で表示する方法
URLの末尾が画像の拡張子で終わっていれば画像として解釈されるため、#.png を付ける
https://img.shields.io/endpoint?url=https%3A%2F%2Fac-shields-badges.vercel.app%2Fapi%2Fac-rate%3Fuser_id%3Demanon001%26contest_type%3Dalgorithm#.png
https://img.shields.io/endpoint?url=https%3A%2F%2Fac-shields-badges.vercel.app%2Fapi%2Fac-rate%3Fuser_id%3Demanon001%26contest_type%3Dheuristic#.png