firebase functionsでline worksのBotを作る。
firebase functionsでline works のBotを作ったので手順を公開します。
サーバーレスで様々なAPIと接続できるので、シンプルに作るのであれば結構良いかと思います。
firebase functionsとは
googleが提供しているBaas(Backend as a service)です。
サーバーを扱わないので比較的安定して作動させることができます。
line works とは
Lineが提供するグループウェアです。
グループウェアとしてはとても安く、UIもほぼLineと同じなのでITに疎い方でも
安心して使用することができます。(とても大事)
構成図
ここで重要なことはCallbackまでがBotで、Messageを送信するのはApplicationの役割ということです。
流れは
firebaseのプロジェクトの作成→Line Worksのアカウントを登録し、BotとApplicationを登録する→コーディング
という流れです。
firebase プロジェクトの作成
こちらを参考にして作りました。firebase-tools
はインストールしておいた方が良いとおもいます。
Line Worksのアカウントの作成とBotの登録
アプリの登録(トークルームにメッセージを送信する際に使う)
Applicationのスコープを設定してください。
bot bot.message bot.read
が必要です。
Service Account認証もしておきます。
private keyをダウンロードしておきましょう。
Botの登録(トークルームからメッセージを受信するときに使う)
Callback URLは最後に設定するので、最初はなしで設定してください。
コーディング
Line Worksの認証方法ではJWT認証を使用するみたいです。
この辺がちょっと面倒なので認証部のみコードを準備しました。
auth.ts
import * as jwt from "jsonwebtoken";
import * as logger from "firebase-functions/logger";
import axios from "axios";
// 認証関連の定数
export const SERVICE_ACCOUNT = "YOUR_SERVICE_ACCOUNT";
export const SERVICE_ACCOUNT_PRIVATE_KEY = `YOUR_ACCOUNT_PRIVATE_KEY`;
// OAuth2クライアント情報
export const CLIENT_ID = "CLIENT_ID";
export const CLIENT_SECRET = "CLIENT_SECRET";
/**
* JWT認証トークンを取得する関数
* @return {Promise<string>} JWTトークン
*/
export async function getJwtToken(): Promise<string> {
const now = Math.floor(Date.now() / 1000);
// LINE Works APIの公式仕様に準拠したJWTペイロード
const payload = {
iss: CLIENT_ID, // 発行者:クライアントID
sub: SERVICE_ACCOUNT, // サブジェクト:サービスアカウント
aud: "https://auth.worksmobile.com/oauth2/v2.0/token", // 対象者:トークンエンドポイント
iat: now, // 発行時刻
exp: now + 3600, // 有効期限(1時間)
};
// 秘密鍵のフォーマットとペイロード詳細を確認
logger.info("JWT signing details", {
hasPrivateKey: !!SERVICE_ACCOUNT_PRIVATE_KEY,
keyStart: SERVICE_ACCOUNT_PRIVATE_KEY.substring(0, 30),
keyEnd: SERVICE_ACCOUNT_PRIVATE_KEY.substring(SERVICE_ACCOUNT_PRIVATE_KEY.length - 30),
payloadIss: payload.iss,
payloadSub: payload.sub,
payloadAud: payload.aud,
payloadIat: payload.iat,
payloadExp: payload.exp,
currentTime: now,
timeToExpiry: payload.exp - now,
});
try {
const token = jwt.sign(payload, SERVICE_ACCOUNT_PRIVATE_KEY, {
algorithm: "RS256",
keyid: CLIENT_ID, // キーID:クライアントID
});
logger.info("JWT token generated successfully", {
tokenLength: token.length,
tokenHeader: token.split(".")[0],
});
return token;
} catch (error) {
logger.error("Failed to sign JWT token", {
error: error instanceof Error ? error.message : String(error),
payloadDetails: payload,
});
throw error;
}
}
/**
* アクセストークンを取得する関数
* @return {Promise<string>} アクセストークン
*/
export async function getAccessToken(): Promise<string> {
try {
const jwtToken = await getJwtToken();
// JWTトークンの内容をログ出力(デバッグ用)
logger.info("Generated JWT token", {
jwtToken: jwtToken.substring(0, 50) + "...",
serviceAccount: SERVICE_ACCOUNT,
});
const params = new URLSearchParams();
params.append("grant_type", "urn:ietf:params:oauth:grant-type:jwt-bearer");
params.append("assertion", jwtToken);
params.append("client_id", CLIENT_ID);
params.append("client_secret", CLIENT_SECRET);
params.append("scope", "bot"); // Botのスコープ
logger.info("Sending OAuth2 request", {
url: "https://auth.worksmobile.com/oauth2/v2.0/token",
clientId: CLIENT_ID,
hasClientSecret: !!CLIENT_SECRET,
serviceAccount: SERVICE_ACCOUNT,
grantType: "urn:ietf:params:oauth:grant-type:jwt-bearer",
scope: "bot",
});
const response = await axios.post(
"https://auth.worksmobile.com/oauth2/v2.0/token",
params.toString(),
{
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
}
);
logger.info("OAuth2 response received", {
status: response.status,
hasAccessToken: !!response.data.access_token,
tokenType: response.data.token_type,
expiresIn: response.data.expires_in,
});
return response.data.access_token;
} catch (error) {
// より詳細なエラー情報をログ出力
if (error instanceof Error && "response" in error) {
const axiosError = error as { response?: { status?: number; statusText?: string; data?: unknown }; config?: { url?: string; method?: string; headers?: unknown } };
logger.error("OAuth2 authentication failed", {
error: error.message,
status: axiosError.response?.status,
statusText: axiosError.response?.statusText,
responseData: axiosError.response?.data,
requestConfig: {
url: axiosError.config?.url,
method: axiosError.config?.method,
headers: axiosError.config?.headers,
},
// OAuth2設定情報(セキュリティのため一部マスク)
authConfig: {
clientId: CLIENT_ID,
hasClientSecret: !!CLIENT_SECRET,
serviceAccount: SERVICE_ACCOUNT,
},
});
} else {
logger.error("Failed to get access token", {
error: error instanceof Error ? error.message : String(error),
});
}
throw error;
}
}
sendMessage.ts
YOURBOTID
は書き換えてください。
/**
* LINE Works APIにメッセージを送信する関数
* @param {string} accessToken アクセストークン
* @param {string} userId 送信先ユーザーID
* @param {BotMessage} message 送信するメッセージ
* @return {Promise<void>}
*/
async function sendMessage(accessToken: string, userId: string, message: BotMessage): Promise<void> {
try {
const apiUrl = "https://www.worksapis.com/v1.0/bots/"+YOURBOTID+"/users/" + userId + "/messages";
const response = await axios.post(apiUrl, message, {
headers: {
"Authorization": `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
});
logger.info("✅ Message sent successfully", {
status: response.status,
userId: userId,
messageType: message.content.type,
});
} catch (error) {
if (error instanceof Error && "response" in error) {
const axiosError = error as { response?: { status?: number; statusText?: string; data?: unknown }; config?: { url?: string; method?: string } };
logger.error("❌ Failed to send message", {
error: error.message,
status: axiosError.response?.status,
statusText: axiosError.response?.statusText,
responseData: axiosError.response?.data,
userId: userId,
messageType: message.content.type,
});
} else {
logger.error("❌ Failed to send message", {
error: error instanceof Error ? error.message : String(error),
userId: userId,
});
}
throw error;
}
}
こちらをonRequest
内で使用します。
// アクセストークンを取得
const accessToken = await getAccessToken();
// メッセージを送信
await sendMessage(accessToken, userId, message);
最後にデプロイします。
firebase deploy --only functions
APIのURLが発行されるのでそちらをBotに登録してください。
以上です。お疲れさまでした。
Discussion