はじめに
業務や趣味でLINEのAPIを触ることが多くなってきており、その際のLINEのBOTIDを取得するためのCLIを適当に作ってみようというのがきっかけです。もともとはスクリプト回していましたが、Rustのコマンドラインツールに関する記事をいくらか見たのもあり、せっかくなので作ってみようと思います。
やりたいこと
CLIで引数に渡したシークレットトークンをLINEのBOTIDに変換してくれるコマンドが欲しい!
せっかくなのでRustで実装してみよう!!
Clap
Rust製のコマンドラインツールについて、今回はClapを使用しました。
Githubを参照し、こんな感じで使いました
fn main() {
let app = App::new("bi")
.version("0.1.0")
.author("ussy")
.about("Line Bot Id CLI")
.arg(Arg::with_name("arg")
.help("need to set channel access token")
.required(true)
);
// 引数を解析
let matches = app.get_matches();
// argが指定されていれば値を表示
if let Some(arg) = matches.value_of("arg") {
// 渡した引数の表示
println!("{:?}", arg);
}
}
詳しくはGitに上がっていますが、簡単に説明します。
- new: "bi"というアプリを作成(これがコマンド名になります)
- version: バージョン情報
- author: 作者
- about: アプリ概要
- arg: 引数
- help: ヘルプ
- required: このappの引数の必須チェック
この辺りの実装は、この方の記事を参考にしたり、ドキュメントを読んだりしました。
今回やりたかったのは引数にシークレットトークンを常に要求するものなので、requiredはtrueにしています。
実行結果は以下のようになります。
$ cargo run test
Finished dev [unoptimized + debuginfo] target(s) in 0.00s
Running `target/debug/claptest test`
"test"
引数が出力されています。
一先ずはサンプルは動いているようです。
reqwest
reqwestはHTTPクライアントを提供するクレートです。
今回はこちらを使ってリクエストを投げてみます。
extern crate reqwest;
use reqwest::Error;
use serde::Deserialize;
# [derive(Deserialize, Debug)]
# [allow(non_snake_case)]
struct Response {
userId: String,
basicId: String,
displayName: String,
pictureUrl: String,
chatMode: String,
markAsReadMode: String,
}
fn main() {
let token = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"; // LINEアクセストークンを指定してください
match get_bot_id(token.to_string()) {
Ok(resp) => {
let resp_json: Response = resp.json().unwrap();
println!("{}", resp_json.userId)
},
Err(error) => {
println!("{}", error);
}
}
}
fn get_bot_id(token: String) -> Result<reqwest::blocking::Response, Error> {
let client = reqwest::blocking::Client::new();
let resp = client.get("https://api.line.me/v2/bot/info")
.header("Authorization", &format!("Bearer {}", token))
.send()?;
Ok(resp)
}
実行結果は以下のようになります。
$ cargo run
Finished dev [unoptimized + debuginfo] target(s) in 2.23s
Running `target/debug/botIdTest`
Uc5ee133c4cd81a781d56yyyyyyyyyyyyyy
reqwestのclientというものを使用しているのですが、今回は多数のリクエストを捌く目的でなく、IDを得るための一つのリクエストを投げるのを想定しているので、非同期処理を行う必要が無いと考え、reqwest::blockingを使用しました。これにより、async/await周りをここでは考えていません。
行っていることはシンプルで、get()で指定したuriにヘッダーを'Authorization: Bearer xxx'で指定してリクエストを投げています。
ここでは扱っていませんが、bodyやFormを指定することももちろんできます。
詳しくは公式ドキュメントに記載してあります。
さて、Rustでは受け取るレスポンスがどのような形であるかを指定してあげる必要があります。そのため、Response構造体を作成し、返ってくる構造を定義しました。この定義はLINE公式に記載があります。
これで準備が整いました。
LINE BOT ID取得のCLI
後はこれらを組み合わせるだけです。
全体像は以下のようになりました。
[package]
name = "bi"
version = "0.1.0"
edition = "2018"
description = "Get Line Bot Id"
[dependencies]
clap = "2.20.3"
reqwest = { version = "0.10.7", features = ["blocking", "json"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
extern crate clap;
extern crate reqwest;
use clap::{App, Arg};
use reqwest::Error;
use serde::{Deserialize, Serialize};
# [derive(Deserialize, Serialize, Debug)]
# [allow(non_snake_case)]
struct Response {
userId: String,
basicId: String,
displayName: String,
pictureUrl: String,
chatMode: String,
markAsReadMode: String,
}
fn main() {
let app = App::new("bi")
.version("0.1.0")
.author("ussy")
.about("Line Bot Id CLI")
.arg(Arg::with_name("arg")
.help("need to set channel access token")
.required(true)
);
// 引数を解析
let matches = app.get_matches();
// argが指定されていれば値を表示
if let Some(arg) = matches.value_of("arg") {
match get_bot_id(arg.to_string()) {
Ok(resp) => {
if resp.status().is_success() {
let resp_json: Response = resp.json().unwrap();
println!("{}", resp_json.userId)
} else {
println!("status: {:?}", resp.status());
}
},
Err(error) => {
println!("{}", error);
}
}
}
}
fn get_bot_id(token: String) -> Result<reqwest::blocking::Response, Error> {
let client = reqwest::blocking::Client::new();
let resp = client.get("https://api.line.me/v2/bot/info")
.header("Authorization", &format!("Bearer {}", token))
.send()?;
Ok(resp)
}
argで引数を取ってきて、ステータスコードを確認しています。
一応はツールっぽく、ステータスコードが200以外であればステータスコードを表示し、200であれば結果を返すようにしました。
実際にbuildしてみましょう。
$ cargo build
Finished dev [unoptimized + debuginfo] target(s) in 0.03s
通りました。これで動作はするのですが、コマンドラインツールとして使用したいので、パスを通して呼び出せるようにしたいと思います。
RustのCargoを使うと、実はこれも簡単にできてしまいます。
それでは以下のコマンドでパスを通します。
$ cargo install --path .
Installing bi v0.1.0 (/home/hoge/bui)
Updating crates.io index
Downloaded syn v1.0.85
Downloaded indexmap v1.8.0
Downloaded 2 crates (287.6 KB) in 0.85s
Compiling syn v1.0.85
Compiling indexmap v1.8.0
Compiling pin-project-internal v1.0.10
Compiling serde_derive v1.0.133
Compiling pin-project v1.0.10
Compiling tracing-futures v0.2.5
Compiling h2 v0.2.7
Compiling serde v1.0.133
Compiling hyper v0.13.10
Compiling serde_urlencoded v0.7.0
Compiling serde_json v1.0.74
Compiling hyper-tls v0.4.3
Compiling reqwest v0.10.10
Compiling bi v0.1.0 (/home/hoge/bui)
Finished release [optimized] target(s) in 1m 02s
Replacing /home/hoge/.cargo/bin/bi
Replaced package `bi v0.1.0 (/home/hoge/bui)` with `bi v0.1.0 (/home/hoge/bui)` (executable `bi`)
無事通りました。
このコマンドは、.cargo/bin配下に実行ファイルを配置するものです。
これにより、.cargoが呼び出せる場所では、"bi"コマンドが使用できます。
試してみましょう。
$ bi xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Uc5ee133c4cd81a781d56yyyyyyyyyyyyyy
アクセストークンを貼れないため、疑似的ですが、成功しました。
失敗した時も試してみます。
$ bi fail
status: 401
きちんとステータスコードが出力されています。
これでやりたいことができました。
まとめ
業務で元々shでBotIdを出力するサンプルを動かしていたのですが、簡単に動かしたいなと思い、パスを通そうと考えていました。
そこで、Clapについて記事を見たので、せっかくならRustの勉強もかねて使ってみようと思い、触ってみました。
最後はlnとかでリンクを通すと思っていたのですが、Cargoにとても便利なコマンドがあったため感動しました。
あまり奇麗には作れていないのですが、これから少しずつ自分で便利だと思うコマンドを増やしていこうと思います。
clapはver3.0がstableにリリースされていると伺ったので、さっそく触ってみたいと思います。
ご指摘、感想などありましたらお願いします。