Rust入門としてLINE botを構築した道のり Part 1
これはなに
ちょっとしたLINE botを構築する機会があり、勉強としてはじめてのRustを使ってみたので、そのメモ。ド初心者のRustなので、改善点あったらぜひご指摘ください。
botの要件
- 決まったキーワードに決まったテキスト・画像で返信する
- 発言内容によってフラグが立ち、その後のルートが分岐する(=要DB)
大まかな構成
- サーバーはCloud Runで動かす
- DBはCloud SQL
- (数本のフラグを保存するだけにしては大げさっぽいが勉強として)
Part 1でできたこと
- hyperでweb serverを立てる
- それをCloud Runにデプロイする
- LINEからのwebhookを受け取りオウム返しをする
準備
- rustup を入れた
- Rust 本を8章くらいまで読んだ
Hello World on Cloud Run
# Use the official Rust image.
# https://hub.docker.com/_/rust
FROM rust:1.62.1
# Copy local code to the container image.
WORKDIR /usr/src/app
COPY . .
# Install production dependencies and build a release artifact.
RUN cargo build --release
EXPOSE 8080
# Run the web service on container startup.
ENTRYPOINT ["target/release/my-linebot"]
- Cloud Run へデプロイする
gcloud builds submit --region=global --tag=gcr.io/<project>/my-linebot
gcloud run deploy linebot-server --image gcr.io/<project>/my-linebot --region=asia-northeast1
endpointを作る
/line
のpathへのPOST requestを使ってLINEからのhookを受けられるようにする
let make_svc = make_service_fn(|_socket: &AddrStream| async move {
Ok::<_, Infallible>(service_fn(move |req: Request<Body>| async move {
let mut response = Response::new(Body::empty());
match (req.method(), req.uri().path()) {
(&Method::POST, "/line") => {
*response.body_mut() = Body::from("ok");
}
_ => {
*response.status_mut() = StatusCode::NOT_FOUND;
*response.body_mut() = Body::from("not found");
}
}
Ok::<_, Infallible>(response)
}))
});
参考: [5]
ファイルを分ける
[6] を読んでモジュールシステムを理解しつつ...
mod routes;
// ...
(&Method::POST, "/line") => {
response = routes::line::handle_line_request(req).await;
}
pub mod line;
use hyper::{Body, Request, Response};
pub async fn handle_line_request(_req: Request<Body>) -> Response<Body> {
Response::new(Body::from("LINE!"))
}
LINEからのrequestをパースする
serde を使用してbodyをstructにパースしていく
LINE messaging APIから来るbodyの型を定義して...
(ここで、JSON内のcamelCaseの扱いと、"type" フィールドの扱いでちょっと詰まった。[7] )
use serde_derive::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug)]
pub struct MessageEventMessage {
text: String,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct MessageEvent {
#[serde(rename(deserialize = "replyToken"))]
reply_token: String,
message: MessageEventMessage,
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(tag = "type")]
pub enum WebHookEvent {
#[serde(rename = "message")]
Message(MessageEvent),
}
#[derive(Serialize, Deserialize, Debug)]
pub struct HookBody {
destination: String,
events: Vec<WebHookEvent>,
}
その形にparse。let-else構文便利ですね〜
use hyper::{body, Body, Request, Response};
use crate::models::line::HookBody;
pub async fn handle_line_request(req: Request<Body>) -> Response<Body> {
let err_res = Response::new(Body::from("Err!"));
let Ok(full_body) = body::to_bytes(req.into_body()).await else {
return err_res;
};
let Ok(body_str) = String::from_utf8(full_body.to_vec()) else {
return err_res;
};
let Ok::<HookBody, _>(parsed) = serde_json::from_str(&body_str) else {
println!("Parse failed");
return err_res;
};
println!("{:?}", parsed);
Response::new(Body::from("LINE!"))
}
オウム返しをする
parseしたメッセージをそのまま返信する。
reply tokenなどを取り出しておいて
let Some(event )= parsed.events.get(0) else {
return Response::new(Body::empty());
};
let (reply_token, reply_messages) = match event {
WebHookEvent::Message(x) => (
x.reply_token.clone(),
match &x.message {
MessageEventMessage::Text(msg) => vec![ReplyMessage::Text(TextMessage {
text: msg.text.clone(),
})],
},
),
};
送信用の構造[10]を定義して
use serde_derive::Serialize;
#[derive(Serialize, Debug)]
pub struct TextMessage {
pub text: String,
}
#[derive(Serialize, Debug)]
#[serde(tag = "type")]
pub enum ReplyMessage {
#[serde(rename = "text")]
Text(TextMessage),
}
#[derive(Serialize, Debug)]
pub struct ReplyBody {
#[serde(rename = "replyToken")]
pub reply_token: String,
pub messages: Vec<ReplyMessage>,
}
送信するbodyを作って
let reply_body = ReplyBody {
reply_token,
messages: reply_messages,
};
Reply用のAPIに接続するクライアントを作成して (参考: [11][12])
async fn send_reply(body: ReplyBody) -> Result<(), Box<dyn error::Error>> {
let https = HttpsConnector::new();
let client = Client::builder().build::<_, hyper::Body>(https);
let access_token = env::var("LINE_CHANNEL_ACCESS_TOKEN").expect("Access token not provided");
let req = Request::builder()
.method(Method::POST)
.uri("https://api.line.me/v2/bot/message/reply")
.header("Authorization", format!("Bearer {}", access_token))
.header("content-type", "application/json")
.body(Body::from(serde_json::to_vec(&body)?))
.unwrap();
client.request(req).await?;
Ok(())
}
返す!
if let Err(e) = send_reply(reply_body).await {
println!("{}", e);
return err_res;
};
Response::builder().status(200).body(Body::empty()).unwrap()
できたー!
一旦オウム返しできたので、Part 1はここまで!
次回予告
次回は
- テキストに応じて分岐
- 画像を返却する
- フラグ管理をする
の3本です。次回もまた見てくださいね〜 じゃんけんポン!ウフフフフ
-
https://github.com/knative/docs/tree/7afbdf47c030b33a05eeb957abd135477255851e/code-samples/community/serving/helloworld-rust ↩︎
-
https://github.com/knative/docs/blob/c85e40487b24923142fcf10fa96d1b7ec73b470d/code-samples/community/serving/helloworld-rust/Dockerfile ↩︎
-
Quickstart: Deploy a service to Cloud Run | Cloud Run Documentation | Google Cloud ↩︎
Discussion