actix webでLINE Messaging API

# 概要

rustの高性能webフレームワークの[actix/actix-web](https://github.com/actix/actix-web)を使って、LINE Messaging APIのbotを実装してみます

## 参考

- [LINE Messaging APIをSpringBoot(on Heroku)で試す](https://qiita.com/gosshys/items/fba889fd734f5bb03756)
- [Tech Empower Round 18 results](https://www.techempower.com/benchmarks/#section=data-r18) 
- [uma0317/line-messaging-api-rust： LINE Messaging API Wrapper in Rust](https://github.com/uma0317/line-messaging-api-rust)

# やり方

actix-webのexsampleが充実しているのでそれを利用して実装します

## actix-webの実装例をMacに持ってくる

githubの[actix/examples](https://github.com/actix/examples)からLINE Messaging APIを呼ぶのに良さそうな実装例をダウンロードします

今回は、async_ex1を選びます。

```
svn export https://github.com/actix/examples/trunk/async_ex1
cd async_ex1
```

### Cargo.tomlを編集する

Cargo.toml内の`workspace = ".."`はexampleのルートを指定しているので不要なので消します
(これがあるとcargo buildが失敗します)

```
package]
name = "awc_examples"
version = "2.0.0"
authors = ["dowwie <dkcdkg@gmail.com>"]
edition = "2018"
#workspace = ".." <- ここを消す

[dependencies]
actix-rt = "1.0.0"
...
..
.
```

## herokuのbuildpackを適用する

heroku CLIは事前にインストールしておいてください。
rust用のherokuのbuildpackは[emk/heroku-buildpack-rust](https://github.com/emk/heroku-buildpack-rust)というのがあったのでそれを使いました

```
git init
heroku create --buildpack emk/rust
```

こんな感じのログがでます。`rocky-caverns-18502`が自動で作られたapp名です

```
Creating app... done, ⬢ rocky-caverns-18502
Setting buildpack to emk/rust... done
https://rocky-caverns-18502.herokuapp.com/ | https://git.heroku.com/rocky-caverns-18502.git
```

## herokuで動作するポート番号を環境変数から取得する

```
 use std::collections::HashMap;
 use std::io;
+use std::env;
 
 use actix_web::{
     client::Client,
@@ -84,15 +85,17 @@ async fn create_something(
 async fn main() -> io::Result<()> {
     std::env::set_var("RUST_LOG", "actix_web=info");
     env_logger::init();
-    let endpoint = "127.0.0.1:8080";
+    let port = env::var("PORT")
+        .unwrap_or_else(|_| "3000".to_string())
+        .parse()
+        .expect("PORT must be a number");
 
-    println!("Starting server at: {:?}", endpoint);
     HttpServer::new(|| {
         App::new()
             .data(Client::default())
             .service(web::resource("/something").route(web::post().to(create_something)))
     })
-    .bind(endpoint)?
+    .bind(("0.0.0.0", port))?
     .run()
     .await
 }
```

## herokuのProcfileを用意する

Procfileに実行ファイルのパスを指定します。`cargo build`で生成される実行ファイルはCargo.tomlで`awc_examples`となっているのでその名前を設定します

```
echo 'web: ./target/release/awc_examples' > Procfile
```

## herokuにデプロイする

crateのダウンロードとコンパイルで15分くらいかかります。2回目からはキャッシュが効くようで1分くらいで済むようになります

```
git add *
git commit -am "init"
git push heroku master
```

正しくデプロイできたかを確認する

```
heroku logs
```

こんな感じのログがでていれば正常にデプロイできています

```
2020-02-16T08:18:30.361574+00:00 heroku[web.1]: State changed from crashed to starting
2020-02-16T08:18:30.834408+00:00 heroku[web.1]: Starting process with command `./target/release/awc_examples`
2020-02-16T08:18:34.396161+00:00 heroku[web.1]: State changed from starting to up
```

### 動かしてみる

curlで実装例で用意されているエンドポイントを叩いてみます

```
curl https://rocky-caverns-18502.herokuapp.com/something -X POST -H 'Content-Type:application/json' -d '{"id":"1", "name": "JohnDoe"}'
```

`{"id":"1","name":"JohnDoe"}`と返ってくれば成功です

## LINE Messaging APIと連携する

### LINE Messaging APIのモデルを足す

前回と同じようにreply用のbotを作成します
reply用のメッセージを受け取るモデルとreplyを返すモデルを作成します
名前は適当なのでわかりづらくてすみません

```
#[derive(Debug, Serialize, Deserialize)]
struct LineMessage {
    r#type: String,
    id: String,
    text: String,
}

#[derive(Debug, Serialize, Deserialize)]
struct LineReplyMessage {
    r#type: String,
    text: String,
}

#[derive(Debug, Serialize, Deserialize)]
struct LineSource {
    #[serde(rename = "userId")]
    user_id: String,
    r#type: String,
}

#[derive(Debug, Serialize, Deserialize)]
struct LineEvent {
    r#type: String,
    #[serde(rename = "replyToken")]
    reply_token: String,
    source: LineSource,
    timestamp: i64,
    mode: String,
    message: LineMessage,
}

#[derive(Debug, Serialize, Deserialize)]
struct LineMsg {
    events: Vec<LineEvent>,
    destination: String,
}

#[derive(Debug, Serialize, Deserialize)]
struct LineReply {
    #[serde(rename = "replyToken")]
    reply_token: String,
    messages: Vec<LineReplyMessage>,
}
```

## reply用の関数を足す

前回同様、発言があったらuserIdを返却することにします
print文が埋め込まれていますがデバッグ用なので必須ではありません
ヘッダーに設定するtokenの発行方法はこちらを参照([Messaging API reference | LINE Developers](https://developers.line.biz/en/reference/messaging-api/#issue-channel-access-token))

```
async fn callback(
    data: web::Json<LineMsg>,
    client: web::Data<Client>,
) -> Result<HttpResponse, Error> {
    // body is loaded, now we can deserialize serde-json
    println!("data: {:?}", data);
    let events = data.events.get(0);
    let event = events.unwrap();
    println!("events: {:?}", events);
    println!("source: {:?}", event.source);
    println!("userId: {:?}", event.source.user_id);

    let reply = LineReply {
        reply_token: event.reply_token.clone(),
        messages: vec![
            LineReplyMessage {
                r#type: String::from("text"),
                text: event.source.user_id.clone(),
            }
        ],
    };

    println!("reply: {:?}", reply);

    let mut res = client
        .post("https://api.line.me/v2/bot/message/reply")
        .bearer_auth("<ここにtokenを追加>")
        .send_json(&reply)
        .await
        .map_err(Error::from)?; // <- convert SendRequestError to an Error

    let mut body = BytesMut::new();
    while let Some(chunk) = res.next().await {
        body.extend_from_slice(&chunk?);
    }

    println!("body: {:?}", body);

    Ok(HttpResponse::Ok().body("OK")) // <- send response
}
```

## エンドポイントを追加する

`/callback`のエンドポイントを追加します

```
#[actix_rt::main]
async fn main() -> io::Result<()> {
    std::env::set_var("RUST_LOG", "actix_web=info");
    env_logger::init();
    let port = env::var("PORT")
        .unwrap_or_else(|_| "3000".to_string())
        .parse()
        .expect("PORT must be a number");

    HttpServer::new(|| {
        App::new()
            .data(Client::default())
            .service(web::resource("/something").route(web::post().to(create_something)))
+             .service(web::resource("/callback").route(web::post().to(callback)))
    })
    .bind(("0.0.0.0", port))?
    .run()
    .await
}
```

## デプロイする

```
git commit -am "add callback"
git push heroku master
```

## LINE Messaging APIのwebhookエンドポイントを追加

[LINE Developers](https://developers.line.biz/console/channel/1489442525/messaging-api)でWebhook settingsのWebhook URLを編集します

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/38174/526d0052-9c2f-da47-4f94-0f83a8334171.png)

## 動作確認

うごきました

![IMG_715698431144-1.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/38174/1b385b9c-3484-7ef0-f3c2-10046067e40d.jpeg)