291 lines
9.8 KiB
JavaScript
291 lines
9.8 KiB
JavaScript
/*
|
||
仕様書
|
||
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);
|
||
});
|