ken_nogi/NodeJS/アルファオフィススケジュール/createLINEWORKSGroup.js
Kenichiro NOGI ed33892f08 chore: 作業中の変更を整理しコミット(複数プロジェクト分)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-02 11:09:50 +09:00

291 lines
9.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
仕様書
https://developers.worksmobile.com/jp/docs/group-create
使い方:
node createLINEWORKSGroup.js "新規グループ名"
環境変数:
LINEWORKS_DOMAIN_ID 任意: domainId (未指定時は DEFAULT_SETTINGS.domainId)
LINEWORKS_DEFAULT_ADMIN_USER_ID 任意: 管理者 userId の初期値
LINEWORKS_DEFAULT_MEMBER_ID 任意: メンバー id の初期値
LINEWORKS_DEFAULT_GROUP_DOMAIN 任意: groupEmail 生成時のデフォルトドメイン
LW_CLIENT_ID 任意: LINE WORKS Client ID
LW_CLIENT_SECRET 任意: LINE WORKS Client Secret
LW_SERVICE_ACCOUNT 任意: サービスアカウント
LW_PRIVATE_KEY 任意: 秘密鍵文字列
LW_PRIVATE_KEY_FILE 任意: 秘密鍵ファイルパス
LW_SCOPE 任意: トークン取得スコープ (既定: group)
*/
"use strict";
const fs = require("fs");
const path = require("path");
const crypto = require("crypto");
const LW_TOKEN_URL = "https://auth.worksmobile.com/oauth2/v2.0/token";
const API_URL = "https://www.worksapis.com/v1.0/groups";
// 認証設定importCal.js / importBoard.js と同系統)
const PRIVATE_KEY_FILE = process.env.LW_PRIVATE_KEY_FILE
? path.resolve(process.env.LW_PRIVATE_KEY_FILE)
: path.join(__dirname, "private_20260307184804.key");
const LW_CLIENT_ID = process.env.LW_CLIENT_ID || "tre8J_Tk8RblfsMSyZRh";
const LW_CLIENT_SECRET = process.env.LW_CLIENT_SECRET || "O5_R5gcHxg";
const LW_SERVICE_ACCOUNT = process.env.LW_SERVICE_ACCOUNT || "wh4k8.serviceaccount@next-hd.co.jp";
const LW_SCOPE = process.env.LW_SCOPE || "group";
const LW_PRIVATE_KEY = (process.env.LW_PRIVATE_KEY && process.env.LW_PRIVATE_KEY.trim())
? process.env.LW_PRIVATE_KEY
: (fs.existsSync(PRIVATE_KEY_FILE) ? fs.readFileSync(PRIVATE_KEY_FILE, "utf8") : "");
// groupName 以外はここでデフォルト値を調整可能
const DEFAULT_SETTINGS = {
// 必須項目
domainId: 401101389,
administrators: [
{
userId: "b4e31b40-ea40-4b53-199c-04469159be6d" || "",
},
],
members: [
{
id: "b4e31b40-ea40-4b53-199c-04469159be6d" || "",
type: "USER",
},
],
groupEmailDomain: "next-hd.co.jp",
// 任意項目
description: "共用アドレス用グループ",
visible: false,
useServiceNotification: false,
serviceManageable: false,
groupExternalKey: null,
useMessage: false,
useNote: false,
useCalendar: false,
useTask: false,
useFolder: false,
useMail: true,
groupEmail: null,
aliasEmails: [],
canReceiveExternalMail: true,
toExternalEmails: [],
membersAllowedToUseGroupEmailAsRecipient: [],
membersAllowedToUseGroupEmailAsSender: [],
};
function parseArgs() {
const groupName = process.argv.slice(2).join(" ").trim();
if (!groupName) {
throw new Error('グループ名を引数で指定してください。例: node createLINEWORKSGroup.js "営業本部"');
}
return { groupName };
}
function ensureRequiredEnvForAuth() {
const missing = [];
if (!LW_CLIENT_ID) missing.push("LW_CLIENT_ID");
if (!LW_CLIENT_SECRET) missing.push("LW_CLIENT_SECRET");
if (!LW_SERVICE_ACCOUNT) missing.push("LW_SERVICE_ACCOUNT");
if (!LW_PRIVATE_KEY) missing.push("LW_PRIVATE_KEY");
if (missing.length > 0) {
if (missing.includes("LW_PRIVATE_KEY") && !fs.existsSync(PRIVATE_KEY_FILE)) {
throw new Error(`秘密鍵が見つかりません: ${PRIVATE_KEY_FILE}`);
}
throw new Error(`環境変数が不足しています: ${missing.join(", ")}`);
}
}
function base64UrlEncode(value) {
return Buffer.from(value)
.toString("base64")
.replace(/=/g, "")
.replace(/\+/g, "-")
.replace(/\//g, "_");
}
function createJwtAssertion() {
const now = Math.floor(Date.now() / 1000);
const header = { alg: "RS256", typ: "JWT" };
const payload = {
iss: LW_CLIENT_ID,
sub: LW_SERVICE_ACCOUNT,
aud: LW_TOKEN_URL,
iat: now,
exp: now + 300,
};
const encodedHeader = base64UrlEncode(JSON.stringify(header));
const encodedPayload = base64UrlEncode(JSON.stringify(payload));
const signingInput = `${encodedHeader}.${encodedPayload}`;
const signer = crypto.createSign("RSA-SHA256");
signer.update(signingInput);
signer.end();
const signature = signer
.sign(LW_PRIVATE_KEY)
.toString("base64")
.replace(/=/g, "")
.replace(/\+/g, "-")
.replace(/\//g, "_");
return `${signingInput}.${signature}`;
}
async function getAccessToken(scope = LW_SCOPE) {
ensureRequiredEnvForAuth();
const assertion = createJwtAssertion();
const form = new URLSearchParams({
grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer",
assertion,
client_id: LW_CLIENT_ID,
client_secret: LW_CLIENT_SECRET,
scope,
});
const response = await fetch(LW_TOKEN_URL, {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: form,
});
if (!response.ok) {
const detail = await response.text();
throw new Error(`アクセストークン取得失敗: ${response.status} ${detail}`);
}
const data = await response.json();
if (!data.access_token) {
throw new Error("アクセストークン取得失敗: access_token が返却されませんでした");
}
return data.access_token;
}
function buildRequestBody(groupName) {
const domainId = Number(process.env.LINEWORKS_DOMAIN_ID || DEFAULT_SETTINGS.domainId);
const groupEmailDomain = String(DEFAULT_SETTINGS.groupEmailDomain || "").trim();
const groupEmailLocalPart = String(groupName).split("@")[0] || groupName;
const generatedGroupEmail = `${groupEmailLocalPart}@${groupEmailDomain}`;
const finalGroupName = "【ML】" + groupName; // グループ名の先頭に識別用のプレフィックスを付与(任意)
const body = {
domainId: domainId,
groupName: finalGroupName,
administrators: DEFAULT_SETTINGS.administrators,
members: DEFAULT_SETTINGS.members,
description: DEFAULT_SETTINGS.description,
visible: DEFAULT_SETTINGS.visible,
useServiceNotification: DEFAULT_SETTINGS.useServiceNotification,
serviceManageable: DEFAULT_SETTINGS.serviceManageable,
groupExternalKey: DEFAULT_SETTINGS.groupExternalKey,
useMessage: DEFAULT_SETTINGS.useMessage,
useNote: DEFAULT_SETTINGS.useNote,
useCalendar: DEFAULT_SETTINGS.useCalendar,
useTask: DEFAULT_SETTINGS.useTask,
useFolder: DEFAULT_SETTINGS.useFolder,
useMail: DEFAULT_SETTINGS.useMail,
groupEmail: generatedGroupEmail,
aliasEmails: DEFAULT_SETTINGS.aliasEmails,
canReceiveExternalMail: DEFAULT_SETTINGS.canReceiveExternalMail,
toExternalEmails: DEFAULT_SETTINGS.toExternalEmails,
membersAllowedToUseGroupEmailAsRecipient:
DEFAULT_SETTINGS.membersAllowedToUseGroupEmailAsRecipient,
membersAllowedToUseGroupEmailAsSender:
DEFAULT_SETTINGS.membersAllowedToUseGroupEmailAsSender,
};
// null/undefined は送信しない
Object.keys(body).forEach((key) => {
if (body[key] == null) {
delete body[key];
}
});
return body;
}
function validateBody(body) {
if (!Number.isInteger(body.domainId) || body.domainId <= 0) {
throw new Error("domainId が未設定です。DEFAULT_SETTINGS.domainId または LINEWORKS_DOMAIN_ID を設定してください。");
}
if (!Array.isArray(body.administrators) || body.administrators.length === 0) {
throw new Error("administrators は 1 件以上必要です。");
}
if (body.administrators.some((admin) => !admin || !admin.userId)) {
throw new Error("administrators[].userId が未設定です。DEFAULT_SETTINGS を設定してください。");
}
if (!Array.isArray(body.members) || body.members.length === 0) {
throw new Error("members は 1 件以上必要です。");
}
if (body.members.some((member) => !member || !member.id || !member.type)) {
throw new Error("members[].id / members[].type が未設定です。DEFAULT_SETTINGS を設定してください。");
}
if (body.useMail === true && !body.groupEmail) {
throw new Error("useMail=true の場合は groupEmail が必須です。");
}
}
async function createGroup(body, accessToken) {
if (typeof fetch !== "function") {
throw new Error("この Node.js 実行環境は fetch に未対応です。Node.js 18 以上を使用してください。");
}
const response = await fetch(API_URL, {
method: "POST",
headers: {
Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
body: JSON.stringify(body),
});
const raw = await response.text();
let data = null;
try {
data = raw ? JSON.parse(raw) : null;
} catch (_err) {
data = raw;
}
if (!response.ok) {
const detail = typeof data === "string" ? data : JSON.stringify(data, null, 2);
throw new Error(`API error ${response.status}: ${detail}`);
}
return data;
}
async function main() {
const accessToken = await getAccessToken();
const { groupName } = parseArgs();
const body = buildRequestBody(groupName);
validateBody(body);
const created = await createGroup(body, accessToken);
console.log("グループ作成に成功しました。");
console.log(JSON.stringify(created, null, 2));
}
main().catch((err) => {
console.error("グループ作成に失敗しました。");
console.error(err.message);
process.exit(1);
});