これらの記事の続きです。
Cloudflare WorkersでLINE Botを作ってみます。
BunやNode.js版との違い
WebのスタンダードAPIを利用するのが基本となり、Node.jsやBun依存のメソッドは利用できないと思ったほうがよさそうです。
設定ファイルのwrangler.toml
にcompatibility_flags = [ "nodejs_compat" ]
を記述すればNode.jsのAPI使えるという話なのですが、僕が試した限りだと関連するところだとBufferは動いたんですけど、Cryptoモジュールがうまく動いてくれなかったです。
service core:user:linebottest: Uncaught SyntaxError: The requested module 'node:crypto' does not provide an export named 'crypto'
Content-Lengthのバイト数チェック
- Node.js版
'Content-Length': Buffer.byteLength(postDataStr)
- Cloudflare Workersでの場合
'Content-Length': new TextEncoder().encode(postDataStr).length //長さチェック用
SHA256暗号化周り
この辺はNode.jsのcryptoモジュールをCloudflare Workersでも使えそうな表記があったのですが、実際に試すと対応してないっぽいエラーがでたのでとりあえず、Web Cripto APIを利用してみます。
- Node.js版
const CH_SECRET = `シークレット`;
const SIGNATURE = crypto.createHmac('SHA256', CH_SECRET);
- Cloudflare Workers版
const CH_SECRET = `シークレット`;
async function createHmacKey(secret) {
const encoder = new TextEncoder();
return await crypto.subtle.importKey('raw', encoder.encode(secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
}
const SIGNATURE = createHmacKey(CH_SECRET);
HTTPリクエストの受け取り周り
こうみると、Node.js版が特殊でBunのネイティブAPIはWebのスタンダードに合わせてそうですね。
- Node.js版
server.on('request', (req, res) => {
let body = '';
req.on('data', chunk => body += chunk);
req.on('end', async () => {
//HTTPリクエストを受け取った時の処理
const events = JSON.parse(body).events; //Webhook Bodyの処理
});
});
- Bun版
Bun.serve({
async fetch(req) {
//HTTPリクエストを受け取った時の処理
const bodyStr = await req.text(); //Webhook Bodyの処理
}
})
- Cloudflare Workers版
export default {
async fetch(request, env, ctx) {
//HTTPリクエストを受け取った時の処理
const bodyStr = await request.text(); //Webhook Bodyの処理
}
}
動くコードと実装
環境
- wrangler: v3.22.3
コピペ用コード
const HOST = 'api.line.me';
const REPLY_PATH = '/v2/bot/message/reply';//リプライ用
const REPLY_API_ENDPOINT = `https://${HOST}${REPLY_PATH}`;
/**
* httpリクエスト部分 Fetch APIを使用
*/
const httpClient = async (replyToken, SendMessageObject, env) => {
try {
const SIGNATURE = crypto.subtle.importKey('raw', new TextEncoder().encode(env.CH_SECRET), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
const postDataStr = JSON.stringify({ replyToken: replyToken, messages: SendMessageObject });
const options = {
method: 'POST',
headers: {
'Content-Type': 'application/json; charset=UTF-8',
'x-line-signature': SIGNATURE,
'Authorization': `Bearer ${env.CH_ACCESS_TOKEN}`,
'Content-Length': new TextEncoder().encode(postDataStr).length //長さチェック用
},
body: postDataStr
};
return fetch(REPLY_API_ENDPOINT, options);
} catch (error) {
throw new Error(error);
};
};
// 署名チェック
async function signatureValidation(xLineSignature, channelSecret, body) {
const encoder = new TextEncoder();
const data = encoder.encode(body);
const key = await crypto.subtle.importKey(
'raw',
encoder.encode(channelSecret),
{ name: 'HMAC', hash: 'SHA-256' },
false,
['sign']
);
const signature = await crypto.subtle.sign('HMAC', key, data);
const base64Signature = btoa(String.fromCharCode(...new Uint8Array(signature)));
return xLineSignature === base64Signature;
}
const handleEvent = async (event, env, ctx) => {
//メッセージが送られて来た場合
if(event.type !== 'message' || event.message.type !== 'text'){
console.log('TEXTメッセージではないので無視');
return;
}
const SendMessageObject = [{
type: 'text',
quoteToken: event.message.quoteToken, //引用リプライ
text: event.message.text
}];
try {
console.log('repTOken:', event.replyToken);
const response = await httpClient(event.replyToken, SendMessageObject, env);
const data = await response.json();
return data;
} catch (error) {
throw new Error(error);
}
}
export default {
async fetch(request, env, ctx) {
if (request.method !== 'POST') {
return new Response('Only POST methods are allowed', { status: 405 });
}
const url = new URL(request.url);
if (url.pathname !== '/') {
return new Response('Not found', { status: 404 });
}
const bodyStr = await request.text();
const headers = request.headers;
if (!await signatureValidation(headers.get('x-line-signature'), env.CH_SECRET, bodyStr)) {
return new Response('Invalid signature', { status: 401 });
}
let events;
try {
const parsedBody = JSON.parse(bodyStr);
events = parsedBody.events;
if (events.length < 1) {
console.log('No events received');
return new Response('No events', { status: 200 });
}
} catch (error) {
console.error('Error parsing body:', error);
return new Response('Bad request', { status: 400 });
}
console.log('body', body);
const responses = await Promise.all(events.map(event => handleEvent(event, env, ctx)));
return new Response(JSON.stringify(responses), { headers: { 'Content-Type': 'application/json' } });
},
}
.dev.vars
のファイルに環境変数を書いて読み込めます。
CH_SECRET=YYYYYYY
CH_ACCESS_TOKEN=XXXXXXXXXXX
$ wrangler dev index.js
LINE Bot作ってみた
消費税の計算LINE BotをNode.js+IBM Cloudで作ってたものを載せ替えてみました。
立ち上がりのコールドスタート感がなくて良き
よもやま: Cloudflare WorkersでLINE Bot作るならHonoがよさそう
勉強がてら書いてみましたが、Cloudflare Workersで動作させるLINE Botを作る時にはHonoを使ったほうが書き心地よさそうです。
元々のLINE Botがexpress依存のライブラリになっているため、expressの書き味で書けるHonoは入りやすいのかなと思います。
SDKのline.Middlewareの部分がExpressのMiddlewareになっているためそのままだとHonoでは利用できず署名チェックなどは自前実装が必要になりそうでした。HTTPリクエスト部分もaxios依存っぽい感じだったので使えないかもですね。