LINE WORKS API 2.0 でスタンプメッセージを送ろう - 実装編

この記事は [LINE WORKS Tech Talk (LWTT) 名古屋](https://lwugdev.connpass.com/event/262194/) で活用したハンズオン資料に修正を加えたものです。

# 対象にしている人

* ボットを作ってみたい。
* nodejs を用いたサーバーサイドプログラムを体験したい。
* これから LINE WORKS の連携開発をしたい。
* API 1.0 は触ったけど API 2.0 はまだ。

# 実装編

必要な環境は [LINE WORKS API 2.0 でスタンプメッセージを送ろう - 準備編](https://qiita.com/bugtrap/items/9dbc2640613892f92c7f) を参照してください。

* Developer Console でアプリを作成（API の認証情報などを生成します）
* Node.js プロジェクトの準備
* プログラム作成

## 1. 概要

これから作成するプログラムは大きく 3部構成（認証部、ボット登録、メッセージ送信）、2パートに分かれています。

**【パート1】ボットを登録＋ドメインに追加するシーケンス**

```plantuml
プログラム -> "(AUTH) /token" : Server Account 認証 (bot スコープの要求)

プログラム <- "(AUTH) /token" : アクセストークンの返却

プログラム -> "(API) /bots" : ボットを登録 with アクセストークン

プログラム <- "(API) /bots" : API 応答 (Bot ID)

プログラム -> "(API) /bots/{botId}/domains" : 登録したボットをドメインに追加 with アクセストークン

プログラム <- "(API) /bots/{botId}/domains" : API 応答
```

**【パート2】スタンプを送信するシーケンス**

```plantuml
プログラム -> "(AUTH) /token" : Server Account 認証 (bot スコープの要求)

プログラム <- "(AUTH) /token" : アクセストークンの返却

プログラム -> "(API) /bots/{botId}/～/message" : スタンプメッセージ送信 with アクセストークン

"(API) /bots/{botId}/～/message" -> ユーザー : スタンプが届く

プログラム <- "(API) /bots/{botId}/～/message" : API 応答
```

では、さっそく始めていきましょう。

## 2. Developer Console でアプリを作成

Developer Console を開きます。

https://developers.worksmobile.com/jp/

「アプリの新規追加」をクリックします。

<img width="50%" alt="" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3934/58d3674e-2c15-e452-14a9-d01ba9d9e3a1.png">

アプリ名を入力して、「追加」をクリックします。

<img width="50%" alt="" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3934/628b36c4-ece4-79e7-ff88-85c316c2d2c5.png">

すると以下のようにアプリが作成されます。続いて **OAuth Scopes** 欄にある「管理」をクリックします。

<img width="50%" alt="" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3934/6286876a-e072-7378-5956-e095c999f2f6.png">

ボットのAPIを利用したいので「bot」スコープにチェックを付けて、「保存」をクリックします。
【一口メモ】API ガイドに必要なスコープの記載があります。

<img width="50%" alt="" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3934/f692f70a-e024-3ddc-5b8f-3b4a230ea956.png">

さらに「保存」をクリックして一度設定を完了します。
再度アプリを開いて、 **Service Account** 欄にある「発行」をクリックします。

続いて **Private Key** 欄の「発行 / 再発行」をクリックして、秘密鍵ファイル（`Private_XXXXXXX.key`）をダウンロードします。

<img width="50%" alt="" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3934/e588bca1-be13-e5bf-f9ae-75a593183158.png">

### メモ・保存する認証情報

アプリの設定から、以下の項目をメモします。

* Client ID
* Client Secret
* Service Account
* ダウンロードした秘密鍵ファイル

コンソール左側に表示されている「Domain ID」の数字をメモします。

* Domain ID 

# いざコーディング！

## サンプル

これから準備するのと同じコードを GitHub のリポジトリに公開しています。
「Code＞Download ZIP」から気軽にダウンロードして試せます。

https://github.com/bugtrap/lwbot-stamper

## 3. Node.js プロジェクトの準備

ハンズオン用に新しいフォルダを作成します。（ここでは `C:\handson\lwbot-stamper` とします）

1. PowerShell を起動して作成したフォルダまで移動します。
1. npm コマンドでプロジェクトを初期準備＋必要なパッケージ（axios、jsonwebtoken）をインストールします。

```powershell
cd C:\handson\lwbot-stamper

npm init -y
npm install axios jsonwebtoken
```

## 4. 秘密鍵ファイルのコピー

ダウンロードした秘密鍵ファイル（`Private_XXXXXXX.key`）を作成したフォルダ内にコピーします。

## 5. プログラムの作成

作成したフォルダ内に、以下3ファイル「lw-auth.js」「bot-register.js」「bot-talk.js」を作ります。

**プログラム内の【】部分を適宜、メモ・保存する認証情報に書き換えます。**

### lw-auth.js

```javascript
const fs = require("node:fs");
const axios = require("axios");
const jwt = require("jsonwebtoken");

// ---- 認証情報 ----
const clientId = "【Client ID】";
const clientSecret = "【Client Secret】";
const serverAccount = "【Service Account】";           // 例: abc12.serviceaccount@line-works-domain
const privateKeyFile = "【Private Key ファイルパス】"; // 例: private_20221024xxxxxx.key

/**
 * サーバーアカウント認証に使う JWT を返します
 * @returns Server Account JWT
 */
function getServerAccountJWT() {
  const payload = {
    iss: clientId,
    sub: serverAccount,
    iat: Date.now(),
    exp: Date.now() + 3600,
  };

  const privatePem = fs.readFileSync(privateKeyFile, "utf-8");
  const token = jwt.sign(payload, privatePem, { algorithm: "RS256" });

  // console.debug("Server Account JWT", token);

  return token;
}

/**
 * サーバーアカウント認証してアクセストークンを取得します
 * @returns アクセストークン
 */
async function getAccessToken() {
  const jwt = getServerAccountJWT();

  // @see https://axios-http.com/docs/urlencoded
  // @see https://developers.worksmobile.com/jp/reference/authorization-sa
  const params = new URLSearchParams({
    assertion: jwt,
    grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer",
    client_id: clientId,
    client_secret: clientSecret,
    scope: "bot",
  });

  const response = await axios.post("https://auth.worksmobile.com/oauth2/v2.0/token", params);
  // console.debug("Token Response", response.data);

  const { access_token } = response.data;
  // console.debug("Access Token", access_token);

  return access_token;
}

module.exports = {
  getAccessToken,
};
```

### bot-register.js

```javascript
const LWAuth = require("./lw-auth");
const axios = require("axios");

// ---- ボットの登録情報 ----
const domainId = "【ドメインID】";                  // 例: 401234567
const adminUserId = "【ボット管理者のユーザーID】"; // 例: admin@line-works-domain
const botName = "【ボットの名前】";

/**
 * ボットを登録します
 */
async function registerBot(accessToken) {
  const headers = {
    Authorization: `Bearer ${accessToken}`
  };

  // @see https://developers.worksmobile.com/jp/reference/bot-create
  const res = await axios.post(`https://www.worksapis.com/v1.0/bots`, {
    botName: botName,
    description: botName,
    photoUrl: "https://localhost/bot.png",
    administrators: [adminUserId],
  }, { headers });

  console.log("[registerBot]", res.data);

  const { botId } = res.data;
  return botId;
}

/**
 * ボットをドメインに追加します
 */
async function registerBotToDomain(accessToken, botId, domainId) {
  const headers = {
    Authorization: `Bearer ${accessToken}`
  };

  // @see https://developers.worksmobile.com/jp/reference/bot-domain-register
  const res = await axios.post(`https://www.worksapis.com/v1.0/bots/${botId}/domains/${domainId}`, {
    botName: botName,
    photoUrl: "https://localhost/bot.png",
    description: botName,
    administrators: [adminUserId],
  }, { headers });

  console.log("[registerBotToDomain]", res.data);
}

async function main() {
  const accessToken = await LWAuth.getAccessToken();
  const botId = await registerBot(accessToken);
  await registerBotToDomain(accessToken, botId, domainId);

  console.log("ボットID", botId);
}

main().catch((raeson) => {
  console.error("失敗", raeson.message, raeson.response?.data);
});
```

### bot-talk.js

※ボットIDは後で書き換えます。一先ずサンプル通りでOKです。

```javascript
const LWAuth = require("./lw-auth");
const axios = require("axios");

// ---- ボットの設定 ----
const botId = "【ボットID】";     // 例: 4012345
const userId = "【ユーザーID】";  // 例: example.taro@line-works-domain

async function talkToUser(accessToken, botId, userId) {
  const headers = {
    Authorization: `Bearer ${accessToken}`
  };

  // @see https://developers.worksmobile.com/jp/reference/bot-user-message-send
  // @see https://developers.worksmobile.com/jp/reference/bot-send-content

  const res = await axios.post(`https://www.worksapis.com/v1.0/bots/${botId}/users/${userId}/messages`, {
    content: {
      type: "sticker",
      packageId: "1",
      stickerId: "2",
    }
  }, { headers });
}

async function main() {
  const accessToken = await LWAuth.getAccessToken();
  await talkToUser(accessToken, botId, userId);

  console.log(`${userId} さんに送信完了！`);
}

main().catch((raeson) => {
  console.error("失敗", raeson.message, raeson.response?.data);
});
```

# いざ実行！

2フェーズに分けて行います。

## ボット登録

```powershell
node bot-register.js

ボットID 1234567
```

ボットID が取得できたら `bot-talk.js` 内のボットIDを書き換えます。

```javascript:bot-talk.js
// ---- ボットの設定 ----
const botId = "【ボットID】";     // ← 【ボットID】部分を書き換えます
const userId = "【ユーザーID】";  // 例: example.taro@line-works-domain
```

## スタンプ！

```powershell
node bot-talk.js

example.taro@line-works-domain さんに送信完了！
```

## 来てるかな？

LINE WORKS のトークを開いてみます。

https://talk.worksmobile.com/

# 最後に

このサンプルはボット登録も含めて出来る限り省ステップで実行できることを目的に作成されています。
そのため、シークレット情報などもハードコーディングしています。ボットIDの管理などを含めて実用にはもう一工夫必要になります。

LINE WORKS API 2.0 を活用する上でアクセストークンを得る部分が最も鬼門になろうと思います。実際に私も何度かハマりました。
認証のエッセンスは全て `lw-auth.js` に纏まっているので参考になれば幸いです。

その他の場所もハマった部分はコメントのデバッグコードとして振り返るためにあえて残しました。