🏄‍♂️

Rust入門としてLINE botを構築した道のり Part 1

2023/02/07に公開

これはなに

ちょっとしたLINE botを構築する機会があり、勉強としてはじめてのRustを使ってみたので、そのメモ。ド初心者のRustなので、改善点あったらぜひご指摘ください。

botの要件

  • 決まったキーワードに決まったテキスト・画像で返信する
  • 発言内容によってフラグが立ち、その後のルートが分岐する(=要DB)

大まかな構成

  • サーバーはCloud Runで動かす
  • DBはCloud SQL
    • (数本のフラグを保存するだけにしては大げさっぽいが勉強として)

Part 1でできたこと

  • hyperでweb serverを立てる
  • それをCloud Runにデプロイする
  • LINEからのwebhookを受け取りオウム返しをする

準備

Hello World on Cloud Run

  • ここ[1] にある cargo.tomlmain.rs をそのまま使って hello world できるサーバーを作成
  • Dockerfile: これ[2] を参考に調整
# 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

参考: [3][4]

endpointを作る

/line のpathへのPOST requestを使ってLINEからのhookを受けられるようにする

main.rs
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] を読んでモジュールシステムを理解しつつ...

main.rs
mod routes;

// ...

(&Method::POST, "/line") => {
    response = routes::line::handle_line_request(req).await;
}
routes/mod.rs
pub mod line;
routes/line.rs
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] )

models/line.rs
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構文便利ですね〜

routes/line.rs
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!"))
}

参考: [8][9]

オウム返しをする

parseしたメッセージをそのまま返信する。

reply tokenなどを取り出しておいて

routes/line.rs
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]を定義して

src/models/line/reply.rs
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を作って

routes/line.rs
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(())
}

返す!

routes/line.rs
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本です。次回もまた見てくださいね〜 じゃんけんポン!ウフフフフ

脚注
  1. https://github.com/knative/docs/tree/7afbdf47c030b33a05eeb957abd135477255851e/code-samples/community/serving/helloworld-rust ↩︎

  2. https://github.com/knative/docs/blob/c85e40487b24923142fcf10fa96d1b7ec73b470d/code-samples/community/serving/helloworld-rust/Dockerfile ↩︎

  3. Quickstart: Deploy a service to Cloud Run  |  Cloud Run Documentation  |  Google Cloud ↩︎

  4. Cloud RunでRustのAPI Serverを動かす ↩︎

  5. Echo, echo, echo | hyper ↩︎

  6. Clear explanation of Rust’s module system ↩︎

  7. String Literal に相当する型はあるのか? ↩︎

  8. メッセージ(Webhook)を受信する | LINE Developers ↩︎

  9. Request in hyper - Rust ↩︎

  10. メッセージを送信する | LINE Developers ↩︎

  11. [Rust] hyperのClient機能のまとめ ↩︎

  12. Response in hyper - Rust ↩︎

Discussion