最近は修論で四六時中 実験を回しっぱなしの日々です。実験自体はスクリプトで自動化しているので放置しておけば良いのですが、数十回、数百回と繰り返し実行させるとなるとそれなりに時間を要します。

で、その実験の最中は別の作業をしたり、仮眠をとったり、散歩に出かけたり…と別の行動に出るのですが、モニタ画面から一旦離れることが多いので、実験が終わってからそれを確認するまでのタイムラグが生じてしまいます。

そこで、実験が終わったら LINE で自動で通知するために、コマンドラインから LINE メッセージを送信するツールを作成しました。

LINE Official Account の概要

要は公式アカウント。よくお店とか市役所が利用してて、友だち登録すると広告やお知らせが流れてくるアレです。

普通の LINE アカウントと同じようにメッセージを送受信することもできるのですが、それだけでありません。

メッセージを登録者全員 or 任意の友だちに送信したり、リンク付きのメニューや画像を表示させたり、Web API にリクエストを飛ばしてメッセージを配信させたりすることが可能です。

Messaging API の概要

LINE Official Account で利用できる各種サービスのうち、「Web API にリクエストを飛ばしてメッセージを配信させる」を実現するのが Messaging API です。

流れとしては以下のようになります。

  1. LINE Developers でアカウントを作成、Messaging API を利用登録し、チャネルアクセストークンを作成する
  2. メッセージ送信用の JSON を作成する
  3. HTTPS POST で Messaging API に対して JSON を送信する。このときチャネルアクセストークンを HTTPS ヘッダで渡す

Messaging API とのやり取りはすべて HTTPS で行われます。メッセージ送信用の JSON はフォーマットが決まっており、下記のページに示されています。

https://developers.line.biz/ja/reference/messaging-api/

Messaging API の種類

参考:Messaging APIの概要 | LINE Developers

Messaging API には下記の4種類の Web API が用意されています。

  • Push API :登録した友だちの内、任意の1人のユーザに対してメッセージを送信する
  • Multicast API :登録した友だちの内、任意の複数人のユーザに対してメッセージを送信する
  • Broadcast API :登録した全ての友だちに対してメッセージを送信する
  • Narrowcast API :登録した友だちの内、特定の属性(性別、年齢など)に属するユーザ全員に送信する

要は送信先を指定するか否か、どうやって指定するかによって分かれている感じです。

使い勝手はほぼ同じですが、リクエスト送信時、Push API と Multicast API ではメッセージの他に送信先ユーザの ID が、Narrowcast API では属性の情報が必要になります。Broadcast API では送信先を特定しないのでメッセージだけ送信すれば OK です。

メッセージの受信(Webhook; 今回は使用せず)

参考:メッセージ(Webhook)を受信する | LINE Developers

Webhook は登録されている友だちからメッセージを送られた際に、そのメッセージを自前のサーバに対して転送する仕組みです。LINE Official Account では通常、メッセージに対して定型文を返すなどの設定もできますが、Webhook を用いることで、メッセージをサーバ側でログを取ったり、サーバ側で作成したメッセージを返答させたり(=クイックリプライ)することが可能です。

Webhook を用いると、予め Messaging API 設定で登録しておいたサーバに対して HTTPS POST でメッセージを含む JSON が送信されます。登録した友だちからのメッセージに対してサーバ側でハンドル(すなわち、受信や返信)したい場合は必須です。

しかし、今回は Messaging API で自発的にメッセージを送信するだけなので使いません。後日の記事で別途実装したいと思います。

なお、HTTPS で通信しますので、Webhook の利用にはサーバの SSL/TLS 証明書の作成が必要です。公式ドキュメント曰く、この証明書は無料で発行できる Let’s Encrypt でも OK だそうです。

利用料金

参考:https://www.lycbiz.com/jp/service/line-official-account/plan/

無料で始められますが、メッセージ数に上限があります。その上限は月 200 件。この件数は送信メッセージを送った先の人数でカウントされ、送信先が一対一であれば一日あたり約 6.7 件といったところです。

ただし、課金対象は Messaging API のうち、自発的にメッセージを送信する上記 4 つの API のみです。Webhook でのメッセージ返信は課金対象としてカウントされませんので、上限なく無料で利用可能です。

今回の手順

1. LINE Developers アカウントの作成

LINE Developers を開き、アカウントを作成します。既存の LINE アカウントで作成できますが、メールアドレスからでも作成可能です。

2. プロバイダーの作成

プロバイダーというのは LINE Official Account を利用する団体のことを指します。すなわち組織や企業です。もちろん個人でも作成可能です。

スクリーンショット 2024-09-22 152013

プロバイダー名は任意の名前を設定しておきます。

3. Messaging API チャネルの作成

プロバイダーを作成したら、次はプロバイダーに対して Messaging API を利用登録しておきます。

スクリーンショット 2024-09-22 152130

で、以前はこの ↑ コンソール画面から Messaging API チャネルを作成できたのですが、最近使用が変わったようで別画面での作成となっていました。

まずは「LINE公式アカウントを作成する」ボタンをポチ。

スクリーンショット 2024-09-22 152210

すると、LINE Official Account Manager というサイトに飛びます。

スクリーンショット 2024-09-22 153923

新しく作る公式アカウントの必要な情報を入力していきます。

スクリーンショット 2024-09-22 152902

このとき SMS 認証の登録が必要でした。

スクリーンショット 2024-09-22 152234

登録が完了すると、LINE Official Account Manager の管理画面が表示されます。

image-20240923112337652

で、これで Messaging API の利用設定が完了…と思ったのですが、どうやらこれは公式アカウントの作成手続きだったようで、別途 Messaging API の利用登録が必要でした。

右上の「設定」ボタンをクリック。

image-20240923112404535

「設定」→「Messaging API」の項目を選択し、「Messaging APIを利用する」をクリックします。

image-20240923112504655

作成する Messaging API をどのプロバイダーに対して紐づけるか聞かれます。ここでは先ほど作成したプロバイダーを選択。

スクリーンショット 2024-09-22 154111

プライバシーポリシーと利用規約の設定を任意で聞かれます。個人利用であればすっ飛ばして良いかと思います。

image-20240923112741481

これにて Messaging API の利用登録は完了です。先程のコンソール画面にも表示されるかと思います。

スクリーンショット 2024-09-22 154125

4. チャネルアクセストークンを作成

HTTPS でのメッセージ送信リクエストを飛ばすときに必要なアクセストークンを作成しておきます。

いくつかの種類があり、

  • チャネルアクセストークンv2.1:任意の有効期間を指定
  • ステートレスアクセストークン:15分のみ有効
  • 短期のチャネルアクセストークン:30日間有効
  • 長期のチャネルアクセストークン:無期限で有効

の4つがあります。詳細は下記のページをご覧ください。

https://developers.line.biz/ja/docs/basics/channel-access-token/#channel-access-token-types

チャネルアクセストークンは各々の運用方法とセキュリティポリシーに応じて選択しますが、今回は一番作成が簡単かつ登録更新が不要な「長期のチャネルアクセストークン」を採用しました。

まずは先程の LINE Developers のコンソール画面から、Messaging API チャネルを選択します。

スクリーンショット 2024-09-22 154152

「Messaging API設定」を開きます。

image-20240923113431806

一番下の「チャネルアクセストークン(長期)」を「発行」を選びます。

image-20240923113507830

発行するとチャネルアクセストークンが表示されます。以降、これを用いて Messaging API にリクエストを飛ばします。

5. メッセージ送信プログラムの作成

先述の通り、Messaging API には HTTPS POST でメッセージの送信リクエストを送ります。

仕様やフォーマットこちら ↓ のページに記載されています。

https://developers.line.biz/ja/reference/messaging-api/

今回は個人利用で登録するユーザは自分一人ですので、ユーザ ID の指定が不要な Broadcast API を利用します。Multicast API の場合、下記の URL にリクエストを飛ばすことになります。

POST https://api.line.me/v2/bot/message/validate/broadcast

ヘッダには先ほど作成したチャネルアクセストークンと Content-Type を指定しておきます。

-H 'Authorization: Bearer [チャネルアクセストークン]'
-H 'Content-Type: application/json'

送信する JSON フォーマットは至ってシンプルです。メッセージは配列になっていて、複数件同時に送ることも可能です。

{
  "messages": [
    {
      "type": "text",
      "text": "こんにちは"
    }
  ]
}

これを一番シンプルに Rust で書くとこんな感じになります。なお、HTTPS 通信には surf を、JSON 作成には serde を利用しています。

use serde::{Deserialize, Serialize};
use surf;

/// メッセージ用構造体
#[derive(Serialize, Deserialize)]
struct LineMessage {
    r#type: String,
    text: String,
}

/// Broadcast 用構造体
#[derive(Serialize, Deserialize)]
struct LineBroadCastMessage {
    messages: Vec<LineMessage>,
}

#[async_std::main]
async fn main() {
    let uri = "https://api.line.me/v2/bot/message/broadcast";
    let channel_access_token = "[Your Channel Access Token Here!]";

    let messages = vec![
        LineMessage { r#type: "text".to_string(), text: "こんにちは".to_string() },
    ];
    let line_broadcast_message = LineBroadCastMessage { messages };

    let res = surf::post(uri)
        .header("Content-Type", "application/json")
        .header("Authorization", format!("Bearer {}", channel_access_token))
        .body_json(&line_broadcast_message)
        .unwrap()
        .await
        .unwrap();
    
    println!("{:?}", res);
}

上2つの構造体は JSON 用の構造体です。serde ではこれらを基に JSON オブジェクトを作成します。

JSON 用の構造体にデータをぶち込むと、serde の serializer によって JSON に変換されます。これを body として、チャネルアクセストークンをヘッダに渡して Broadcast API にリクエストを送ります。

このプログラムをビルドして実行すると、先ほど作成した公式アカウントの登録ユーザ全員に対し「こんにちは」と送信されます。

引数やパイプから送信用メッセージを受け取る

引数の受け取りには clap を、パイプからの入力の有無の確認には atty を利用しています。

main.rs(JSON 構造体の生成、送信)

use serde::{Deserialize, Serialize};
use std::fmt::Debug;
use surf;

mod parse;

/// メッセージ用構造体
#[derive(Serialize, Deserialize)]
struct LineMessage {
    r#type: String,
    text: String,
}

/// Broadcast 用構造体
#[derive(Serialize, Deserialize)]
struct LineBroadCastMessage {
    messages: Vec<LineMessage>,
}

/// エラー処理用
enum RuntimeError {
    IOError(std::io::Error),
    HttpError(surf::Error),
}
impl ToString for RuntimeError {
    fn to_string(&self) -> String {
        match self {
            RuntimeError::IOError(e) => format!("IOError: {}", e),
            RuntimeError::HttpError(e) => format!("HttpError: {}", e),
        }
    }
}
impl Debug for RuntimeError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.to_string())
    }
}

#[async_std::main]
async fn main() -> Result<(), RuntimeError> {
    let uri = "https://api.line.me/v2/bot/message/broadcast";
    let channel_access_token = "[Your Channel Access Token Here!]";

    let args = parse::parse_args()?;
    let message = LineMessage {
        r#type: "text".to_string(),
        text: args.message,
    };

    let messages = vec![message];
    let line_broadcast_message = LineBroadCastMessage { messages };

    let res = surf::post(uri)
        .header("Content-Type", "application/json")
        .header("Authorization", format!("Bearer {}", channel_access_token))
        .body_json(&line_broadcast_message)
        .unwrap()
        .await
        .map_err(RuntimeError::HttpError)?;
    
    println!("{:?}", res);

    Ok(())
}

parse.rs(引数・パイプからメッセージを取得)

use clap::Parser;
use atty::Stream;
use std::io::{self, Read};
use super::RuntimeError;

#[derive(Debug, Clone)]
pub struct ArgStruct {
    pub message: String,
}

#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]
pub struct Args {
    pub message: Option<String>,
}

fn load_stdin() -> io::Result<Option<String>> {
    if atty::is(Stream::Stdin) {
        return Ok(None);
    }
    let mut buffer = String::new();
    io::stdin().read_to_string(&mut buffer)?;
    Ok(Some(buffer))
}

pub fn parse_args() -> Result<ArgStruct, RuntimeError> {
    let args = Args::parse();
    let std_message = load_stdin().map_err(RuntimeError::IOError)?;
    let message = match std_message {
        Some(message) => message,
        None => {
            match args.message {
                Some(message) => message,
                None => {
                    return Err(RuntimeError::IOError(io::Error::new(
                        io::ErrorKind::InvalidInput,
                        "No message provided.",
                    )));
                }
            }
        }
    };

    Ok(ArgStruct {
        message: message,
    })
}

Cargo.toml

[package]
name = "line-message"
version = "0.1.0"
edition = "2021"

[dependencies]
async-std = { version = "1.13.0", features = ["attributes"] }
atty = "0.2.14"
clap = { version = "4.5.18", features = ["derive"] }
clap_derive = "4.5.18"
serde = "1.0.210"
surf = "2.3.2"

送信テスト

こんな感じでデータ送信をテストしてみます。

$ uname -a | line-message
$ date | line-message
$ line-message "Hello, World!"

こちらが結果です。コマンドラインの引数やパイプから任意のメッセージが送信できていることが確認できました。

Screenshot_20240923-120657

Screenshot_20240923-120719

おわりに

今回は簡単なメッセージ送信専用プログラムを作成しました。

今回はメッセージ送信を実装しただけですが、ゆくゆくはメッセージに応じたコマンド実行や、Webhook を用いた返信機能の実装もしていきたいなと思います。