Chatwork API を試してみる
このスクラップについて
このスクラップでは Chatwork API を使用してメッセージの送受信を行う方法を試してみる
ちなみに前のスクラップでは LINE で同様のことを試した
ドキュメントに一通り目を通したが何から手をつけて良いのかわからない
LINE のドキュメントが素晴らしかったので落差が激しい
ワークスペースの準備
nest new -p npm hello-chatwork
cd hello-chatwork
npm install dotenv
touch .env
.gitignore に .env を忘れずに追加する
.env
API トークンの取得
Chatwork 画面右上から自分の名前 > サービス連携 > API トークンを選択するか https://www.chatwork.com/service/packages/chatwork/subpackages/api/token.php にアクセスして API トークンを取得して .env にコピー&ペーストする
CHATWORK_API_TOKEN="00000000000000000000000000000000"
テストコードの作成
touch test/chatwork-api.e2e-spec.ts
npm install node-fetch@2
npm install --save-dev @types/node-fetch
import fetch from 'node-fetch';
describe('Chatwork API', () => {
it('/me', async () => {
const fetchUrl = 'https://api.chatwork.com/v2/me';
const fetchResponse = await fetch(fetchUrl, {
headers: {
'X-Chatworktoken': process.env.CHATWORK_API_TOKEN,
},
});
console.log(fetchResponse.status);
console.log(await fetchResponse.text());
});
});
npm run test:e2e -- chatwork-api --setupFiles dotenv/config
実行結果の例は下記の通り
{
"account_id": 1111111,
"room_id": 222222222,
"name": "\u8584\u7530 \u9054\u54c9",
"chatwork_id": "xxxxxxxxxxxxx",
"organization_id": 3333333,
"organization_name": "",
"department": "",
"title": "",
"url": "",
"introduction": "",
"mail": "",
"tel_organization": "",
"tel_extension": "",
"tel_mobile": "",
"skype": "",
"facebook": "",
"twitter": "",
"avatar_image_url": "https://appdata.chatwork.com/avatar/xArw08pG7D.rsz.jpg",
"login_mail": "susukida@example.com"
}
メッセージの送信
まずはグループチャットを新規作成する
新規作成したらルーム ID を確認する
ルーム ID はグループチャットの歯車アイコン > グループチャットの設定から確認できる
ルーム ID を .env ファイルに追記する
ROOM_ID="000000000"
続いてテストコードを編集する
import fetch from 'node-fetch';
import { URLSearchParams } from 'url';
describe('Chatwork API', () => {
it('/me', async () => {
const fetchUrl = 'https://api.chatwork.com/v2/me';
const fetchResponse = await fetch(fetchUrl, {
method: 'GET',
headers: {
'X-Chatworktoken': process.env.CHATWORK_API_TOKEN,
},
});
console.log(fetchResponse.status);
console.log(await fetchResponse.text());
});
it('/rooms/{room_id}/messages', async () => {
const fetchUrl = `https://api.chatwork.com/v2/rooms/${process.env.ROOM_ID}/messages`;
const fetchResponse = await fetch(fetchUrl, {
method: 'POST',
headers: {
'X-Chatworktoken': process.env.CHATWORK_API_TOKEN,
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
self_unread: '0',
body: 'メッセージ本文',
}).toString(),
});
console.log(fetchResponse.status);
console.log(await fetchResponse.text());
});
});
npm run test:e2e -- chatwork-api --setupFiles dotenv/config
成功すると Chatwork のタイムライン上に「メッセージ本文」と投稿される
API から返送されるレスポンス内容の例は下記の通り
{"message_id":"0000000000000000000"}
今日は Webhook を使ったメッセージ受信を試してみる
リクエストの署名検証
まずは .env から環境変数を読み込めるようにする
npm install --save @nestjs/config
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { AppController } from './app.controller';
import { AppService } from './app.service';
@Module({
imports: [ConfigModule.forRoot()],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
.env に PORT, BASE_URL, WEBHOOK_TOKEN の 3 点を追加する
PORT="3000"
BASE_URL="http://localhost:3000"
CHATWORK_API_TOKEN="00000000000000000000000000000000"
ROOM_ID="000000000"
WEBHOOK_TOKEN="XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
続いてモジュールを編集してリクエストの生データにアクセスできるようにする
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule, {
rawBody: true,
});
await app.listen(3000);
}
bootstrap();
続いてコントローラーを編集する、相変わらずサービスを使っていなくて行儀が悪い
import { Controller, Post, RawBodyRequest, Req, Res } from '@nestjs/common';
import { createHmac, timingSafeEqual } from 'crypto';
import { Request, Response } from 'express';
import { AppService } from './app.service';
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
/**
* Chatwork Webhook 用のエンドポイントです。
* @param req Express リクエストオブジェクト
* @param res Express レスポンスオブジェクト
*/
@Post('api/v1/chatwork/webhook')
onApiV1ChatworkWebhook(
@Req() req: RawBodyRequest<Request>,
@Res() res: Response,
) {
// リクエスト署名を検証します。
const webhookToken = process.env.WEBHOOK_TOKEN;
const privateKey = Buffer.from(webhookToken).toString('base64');
const actualSignature = createHmac('sha256', privateKey)
.update(req.rawBody)
.digest('base64');
const expectedSignature = req.headers[
'x-chatworkwebhooksignature'
] as string;
const isSignatureVerified = timingSafeEqual(
Buffer.from(actualSignature),
Buffer.from(expectedSignature),
);
if (!isSignatureVerified) {
res.status(400).send('Bad Request: !isSignatureVerified');
return;
}
res.status(200).end('OK');
}
}
最後にテストコードを作成する
touch test/chatwork-webhook.e2e-spec.ts
import { createHmac } from 'crypto';
import fetch from 'node-fetch';
describe('Chatwork Webhook', () => {
it('/api/v1/chatwork/webhook', async () => {
const requestBody = JSON.stringify({
webhook_setting_id: '12345',
webhook_event_type: 'mention_to_me',
webhook_event_time: 1498028130,
webhook_event: {
from_account_id: 123456,
to_account_id: 1484814,
room_id: 567890123,
message_id: '789012345',
body: '[To:1484814]おかずはなんですか?',
send_time: 1498028125,
update_time: 0,
},
});
const webhookToken = process.env.WEBHOOK_TOKEN;
const privateKey = Buffer.from(webhookToken).toString('base64');
const webhookSignature = createHmac('sha256', privateKey)
.update(requestBody)
.digest('base64');
const fetchUrl = process.env.BASE_URL + '/api/v1/chatwork/webhook';
const fetchResponse = await fetch(fetchUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'User-Agent': 'Chatwork-Webhook/1.0.0',
'X-Chatworkwebhooksignature': webhookSignature,
},
body: requestBody,
});
console.log(fetchResponse.status);
console.log(await fetchResponse.text());
});
});
最後に実行
npm run test:e2e -- chatwork-webhook --setupFiles dotenv/config
> hello-chatwork@0.0.1 test:e2e
> jest --config ./test/jest-e2e.json
console.log
200
at Object.<anonymous> (chatwork-webhook.e2e-spec.ts:38:13)
console.log
OK
at Object.<anonymous> (chatwork-webhook.e2e-spec.ts:39:13)
PASS test/chatwork-webhook.e2e-spec.ts
Chatwork Webhook
✓ /api/v1/chatwork/webhook (51 ms)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 1.825 s
Ran all test suites matching /chatwork-webhook/i.
ちゃんと動いている様子
Cloud Run にデプロイする前に署名検証失敗時のログ出力処理を追加しておいた方が良さそう
Webhook のレスポンスは何かに使われるのだろうか?
恐らく何にも使われないと予想する
リクエストボディをそのまま表示する
ついでにリクエスト署名の検証失敗時のログ出力処理を追加した
import { Controller, Post, RawBodyRequest, Req, Res } from '@nestjs/common';
import { createHmac, timingSafeEqual } from 'crypto';
import { Request, Response } from 'express';
import { AppService } from './app.service';
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
/**
* Chatwork Webhook 用のエンドポイントです。
* @param req Express リクエストオブジェクト
* @param res Express レスポンスオブジェクト
*/
@Post('api/v1/chatwork/webhook')
onApiV1ChatworkWebhook(
@Req() req: RawBodyRequest<Request>,
@Res() res: Response,
) {
// リクエスト署名を検証します。
const webhookToken = process.env.WEBHOOK_TOKEN;
const privateKey = Buffer.from(webhookToken).toString('base64');
const actualSignature = createHmac('sha256', privateKey)
.update(req.rawBody)
.digest('base64');
const expectedSignature = req.headers[
'x-chatworkwebhooksignature'
] as string;
const isSignatureVerified = timingSafeEqual(
Buffer.from(actualSignature),
Buffer.from(expectedSignature),
);
if (!isSignatureVerified) {
res.status(400).send('Bad Request: !isSignatureVerified');
console.warn(JSON.stringify({ actualSignature, expectedSignature }));
return;
}
// リクエストの内容を出力します。
console.info(req.rawBody.toString());
res.status(200).end('OK');
}
}
PORT 環境変数の読み込みを忘れていた
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { AppController } from './app.controller';
import { AppService } from './app.service';
@Module({
imports: [ConfigModule.forRoot()],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
テストコードは変更なしで実行する
npm run test:e2e -- chatwork-webhook --setupFiles dotenv/config
下記はテストコードの実行結果ではなくサーバー側のログ出力
{"webhook_setting_id":"12345","webhook_event_type":"mention_to_me","webhook_event_time":1498028130,"webhook_event":{"from_account_id":123456,"to_account_id":1484814,"room_id":567890123,"message_id":"789012345","body":"[To:1484814]おかずはなんですか?","send_time":1498028125,"update_time":0}}
期待した通りに動作している、良かった
Cloud Run へデプロイ
touch .gcloudignore
/.git/
/dist/
/node_modules/
/test/
/.env
/README.md
アップロードされるファイルを確認する
gcloud meta list-files-for-upload
nest-cli.json
.gitignore
package-lock.json
package.json
tsconfig.build.json
.prettierrc
.eslintrc.js
tsconfig.json
.gcloudignore
src/main.ts
src/app.service.ts
src/app.module.ts
src/app.controller.spec.ts
src/app.controller.ts
続いて package.json を編集する
{
"scripts": {
"gcp-build": "npm run build",
"start": "node dist/main",
"deploy": "gcloud run deploy hello-chatwork --source . --platform managed --region asia-northeast1 --allow-unauthenticated"
}
デプロイコマンドを実行する
npm run deploy
デプロイが完了したら CLI か GCP Web コンソールから環境変数を設定する
Chatwork Webhook の登録
下記の URL にアクセスする
入力する必要があるのは下記 3 点
- Webhook 名
- Webhook URL
- イベント
Webhook 名はわかりやすい名前であれば何でも良い
Webhook URL は Cloud Run の URL + /api/v1/chatwork/webhook
イベントはアカウントイベントとルームイベントの 2 つがある
アカウントイベントは自分がメンションされた時(多分 @ とかを使ったメッセージが送信された時)に Webhook が実行される
ルームイベントは指定したルームでやり取りされる全てのメッセージが対象になる
ルームイベントはルーム ID を指定しなければならないので複数のルームを対象にするには Webhook を複数作成する必要がある、これはユースケースによっては厄介だがそういう仕様なので仕方ない
今回はアカウントイベントを使ってみる
作成ボタンを押すとトークンが表示されるので WEBHOOK_TOKEN として Cloud Run 環境変数に登録する
Chatwork でメンションする場合は Slack みたいに @ を使うのではなく、メッセージ入力部の左上にある TO ボタンを押します。
よく考えたら CHATWORK_API_TOKEN, ROOM_ID の環境変数は登録する必要ないが一応登録しておこう
自分で自分にメンションする
グループチャットに自分一人だとメンションできない(する必要がない)
下記のようにメッセージを送ればメンションできるのかな
[To:0000000]薄田 達哉さん
0000000 のアカウント ID は環境設定 > Chatworkについてから確認できる
メッセージを送ってみたが反応がない
まだ反映されていないのかな?
BASE_URL を変更してテストコードを実行して確認してみる
BASE_URL="https://hello-chatwork-xxxxxxxxxx-an.a.run.app" npm run test:e2e -- chatwork-webhook --setupFiles dotenv/config
テストコードは失敗したが Cloud Run のログではアクセスの記録を確認できた
自分で自分にメンションした場合はダメなのかな?
ルームイベントに変更してみる
もう一回試してみたけど結果は変わらなかったのでルームイベントに変更してみる
ルーム ID は URL の #rid 以下からコピーするかグループチャットの設定(歯車アイコン)から確認できる
変更したらログが表示された!
ただし、署名の検証に失敗している様子、やはりログを仕込んでおいて良かった
原因はすぐわかった
Chatwork のドキュメントを読み直したらすぐ分かった、下記が関連箇所
トークンをBase64デコードしたバイト列を秘密鍵とします(トークンはWebhook編集画面で確認できます)
Base64 デコードではなくエンコードしていた笑
なんで Base64 らしきものをもう一回エンコードするんだろうと疑問だったが解決した
ソースコードとテストコードを下記の通り修正する
import { Controller, Post, RawBodyRequest, Req, Res } from '@nestjs/common';
import { createHmac, timingSafeEqual } from 'crypto';
import { Request, Response } from 'express';
import { AppService } from './app.service';
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
/**
* Chatwork Webhook 用のエンドポイントです。
* @param req Express リクエストオブジェクト
* @param res Express レスポンスオブジェクト
*/
@Post('api/v1/chatwork/webhook')
onApiV1ChatworkWebhook(
@Req() req: RawBodyRequest<Request>,
@Res() res: Response,
) {
// リクエスト署名を検証します。
const webhookToken = process.env.WEBHOOK_TOKEN;
const privateKey = Buffer.from(webhookToken, 'base64');
const actualSignature = createHmac('sha256', privateKey)
.update(req.rawBody)
.digest('base64');
const expectedSignature = req.headers[
'x-chatworkwebhooksignature'
] as string;
const isSignatureVerified = timingSafeEqual(
Buffer.from(actualSignature),
Buffer.from(expectedSignature),
);
if (!isSignatureVerified) {
res.status(400).send('Bad Request: !isSignatureVerified');
console.warn(JSON.stringify({ actualSignature, expectedSignature }));
return;
}
// リクエストの内容を出力します。
console.info(req.rawBody.toString());
res.status(200).end('OK');
}
}
import { createHmac } from 'crypto';
import fetch from 'node-fetch';
describe('Chatwork Webhook', () => {
it('/api/v1/chatwork/webhook', async () => {
const requestBody = JSON.stringify({
webhook_setting_id: '12345',
webhook_event_type: 'mention_to_me',
webhook_event_time: 1498028130,
webhook_event: {
from_account_id: 123456,
to_account_id: 1484814,
room_id: 567890123,
message_id: '789012345',
body: '[To:1484814]おかずはなんですか?',
send_time: 1498028125,
update_time: 0,
},
});
const webhookToken = process.env.WEBHOOK_TOKEN;
const privateKey = Buffer.from(webhookToken, 'base64');
const webhookSignature = createHmac('sha256', privateKey)
.update(requestBody)
.digest('base64');
const fetchUrl = process.env.BASE_URL + '/api/v1/chatwork/webhook';
const fetchResponse = await fetch(fetchUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'User-Agent': 'Chatwork-Webhook/1.0.0',
'X-Chatworkwebhooksignature': webhookSignature,
},
body: requestBody,
});
console.log(fetchResponse.status);
console.log(await fetchResponse.text());
});
});
もう一回デプロイ
npm run deploy
メンションは他のユーザーからしてもらう必要があるのか
署名検証の確認が終わったらアカウントをもう一つ作って試してみよう
成功!
Cloud Run でチャットワークでルームに送信されたメッセージを確認することができた嬉しい
アカウントイベント Webhook の動作確認
Chatwork アカウントをもう 1 つ作成する
作成したら自分のアカウントに招待メールを送って承諾する
Webhook の編集ページからイベントをルームイベントからアカウントイベントに変更する
新しく作成したアカウントからメンション(TO)付きでメッセージを送ってみる
今度は成功した!
メンションは他のアカウントから送って送ってもらう必要があることがわかった
ルーム以外はルーム ID を調べられないと思っていたけど
よく考えたら URL の #!rid
以下がルーム ID なのでこれを使えば良い?
試しにメッセージを送ってみたらちゃんと届いた
良かった、これでユーザーごとに個別のルームを作成する必要がない
Webhook を無効にしておく
今のままだとチャットワークでメンションがある度に Webhook が実行されるので Webhook の編集ページからステータスを無効にするか、イベントをルームイベントに変更しておく
おわりに
Chatwork の方も無事にメッセージの送受信を検証することができた
不特定多数に使用する場合は LINE と同様にアカウント連携も必要だが、特定少数の場合は管理者側でユーザーとルーム ID を紐付けてもらうことで対応できそう
GitHub リポジトリ
ソースコードを GitHub で公開しましたのでお役に立つようであればお使いください
次のスクラップ
これで一旦クローズ、次は Stripe API について調べてみようと思う