[GAS]REST APIでConfluenceの任意のページを自動で作成する

## 背景
社内の週次MTGでConfluenceにて議事録ページを作成する必要があり、参加者全員がMTGまでに必要事項を記入する必要があるけど、議事録ページを誰かが作成するまで記入できない＋手動だと面倒なので自動化できないかな？と思ったのが始まりです。

調べたところConfluenceにあるルール機能で自動化可能みたいなのですが、契約プラン上使えず…。(Premiumプランは使用できるみたいなので、Premiumの方はルール機能の利用をお勧めします。)
ConfluenceのREST APIを使用したらGASでも自動化できるみたいなので作ってみました。

議事録ページのフォーマットをコピーして作成、作成後Slackへ通知を飛ばすようにしています。

**Confluence REST APIの公式ドキュメント**

https://developer.atlassian.com/cloud/confluence/rest/v2/intro/#about

## 事前準備
実装するにあたっていくつか事前に用意しておく必要のある情報があります。


### 1. APIトークンの取得
Confluenceの管理者からAPIトークンを取得します。これを使って認証を行います。
https://id.atlassian.com/login からログインし、画面右上のプロフィールアイコンからアカウント設定＞セキュリティ＞APIトークンから任意の名称をつけて作成が可能です。
※上記URLではなくConfluenceのご自分のスペースからアクセスする場合は、画面右上のプロフィールアイコンに[アカウント管理]があるのでそちらから遷移してください。

APIトークン作成ページは[こちら](https://id.atlassian.com/manage-profile/security/api-tokens)からもアクセスできます。

### 2. APIエンドポイントの確認
ConfluenceのREST APIを利用してページを作成します。エンドポイントは以下のようになります。
以下の`<your-domain>`部分をご確認の上、ご準備ください。
```
POST https://<your-domain>.atlassian.net/wiki/rest/api/content/
```

### 3. コピーしたいConfluenceページのページIDの確認
今回は議事録ページのフォーマットをあらかじめ作成しているので、そのページのページIDを使用します。

**確認方法**
　対象のConfluenceページを開き、目的のページにアクセスしURLを確認します。

　ページのURLは次の形式になっています：
```
https://<your-domain>.atlassian.net/wiki/spaces/<SPACEKEY>/pages/<PAGEID>/Page+Title
```
　例えば、URLが以下のようであれば：
```
https://<your-domain>.atlassian.net/wiki/spaces/DEV/pages/123456789/Example+Page
```
　この場合、ページIDは `123456789` です。

### 4. Confluenceページを作成するスペースのキーの確認
3のページIDの確認同様に、ページを自動作成したいスペースのURLを確認します。
以下のSPACEKEYに当たる部分がスペースキーです。
```
https://<your-domain>.atlassian.net/wiki/spaces/<SPACEKEY>/pages/<PAGEID>/Page+Title
```

### 5. 新しいConfluenceページの親ページのページID
3と同様の手順で親ページのページIDを取得してください。

### 6. Slack通知に使用するWebhook URLの取得
カスタムインテグレーションAppのIncoming Webhookは公式で非推奨となっておりますので、Slack AppsのIncoming Webhook機能を使用します。

以下のようなWebhook URLが作成されるのでコピーしておいてください。
```
https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX
```
以下のQiitaを参考にさせていただきました。
参考URL:[【Slack】Slack API を使って特定のチャンネルに通知するアプリを追加する全手順](https://qiita.com/to3izo/items/c2d16f8b3e52b09e543e)



### 7. Slack通知でメンションしたいチャンネルのCHANNELID
該当のチャンネルのチャンネル詳細を開くとポップアップウィンドウ最下部にチャンネルIDの記載がありますのでそちらをコピーしておいてください。
![スクリーンショット 2024-11-19 18.56.29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3757872/30673334-757c-7f43-2935-8b31ba651e36.png)
![スクリーンショット 2024-11-19 18.56.43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3757872/65402f55-3fcc-a27f-827e-131c1821f6ec.png)

## 実装

### 1. リクエストの作成

ページ作成に必要な情報を含むJSONを作成します。以下は例です。
```
{
  "type": "page",
  "title": "新しいページのタイトル",
  "space": {
    "key": "SPACE_KEY"
  },
  "body": {
    "storage": {
      "value": "<p>ここにコンテンツを入力</p>",
      "representation": "storage"
    }
  }
}
```

### 2. スクリプトの作成

PythonやNode.jsなどを使用してHTTPリクエストを送信するスクリプトを作成します。
私はGASで作成したので以下にコード記載します。
```js:copyConfluencePage.gs
const CONFLUENCE_BASE_URL = 'https://<your-domain>.atlassian.net/wiki/rest/api/content/'; // 事前準備2で取得したAPIエンドポイント
const API_TOKEN = '実行者のAPIトークン'; // 実行者のAPIトークン(事前準備1で取得したAPIトークン)
const USERNAME = '実行者のメールアドレス'; // 実行者のメールアドレス
const PAGE_ID = '123456789'; // コピーしたいページのID(事前準備3で取得したページID)
const SPACE_KEY = 'TESTSPACE'; // ページを作成するスペースのキー(事前準備4で取得したスペースキー)
const PARENT_PAGE_ID = '987654321'; // 新しいページの親ページのID(事前準備5で取得したページID)

/**
 * メイン処理です。
 */
function copyConfluencePage() {
  const pageContent = getConfluencePage(PAGE_ID);
  
  // 現在の日付を取得
  const todayDate = Utilities.formatDate(new Date(), Session.getScriptTimeZone(), 'yyyy-MM-dd');
  
  // 日付をページ内容に挿入
  const updatedContent = pageContent.replace(/<time datetime="\d{4}-\d{2}-\d{2}" \/>/, `<time datetime="${todayDate}" />`);

  const newPageTitle = `定例 - ${Utilities.formatDate(new Date(), Session.getScriptTimeZone(), 'yyyy/MM/dd')}`;
  const newPageId = createConfluencePage(newPageTitle, updatedContent); // ページを作成後、ページIDを取得
  
  // 新しいページのURLを生成
  const newPageUrl = `https://<your-domain>.atlassian.net/wiki/spaces/TESTSPACE/pages/${newPageId}`;

  // Slackに通知を送信
  sendSlackNotification(newPageTitle, newPageUrl);
}

/**
 * コピー元フォーマットの内容を取得します。
 */
function getConfluencePage(pageId) {
  const url = `${CONFLUENCE_BASE_URL}${pageId}?expand=body.storage`;
  const options = {
    method: 'get',
    headers: {
      'Authorization': 'Basic ' + Utilities.base64Encode(`${USERNAME}:${API_TOKEN}`),
      'Accept': 'application/json'
    },
    muteHttpExceptions: true // エラーハンドリングを追加
  };

  const response = UrlFetchApp.fetch(url, options);
  Logger.log('Response Code: ' + response.getResponseCode());
  Logger.log('Response Content: ' + response.getContentText());

  // エラーハンドリング
  if (response.getResponseCode() !== 200) {
    throw new Error(`Failed to fetch page: ${response.getResponseCode()}`);
  }

  const json = JSON.parse(response.getContentText());
  return json.body.storage.value; // HTMLコンテンツを取得
}

/**
 * ページを作成し、作成されたページのページIDを返します。
 * @return newPageId
 */
function createConfluencePage(title, content) {
  const url = `${CONFLUENCE_BASE_URL}`;
  const payload = {
    type: 'page',
    title: title,
    space: { key: SPACE_KEY },
    ancestors: [{ id: PARENT_PAGE_ID }], // 親ページのIDを追加
    body: {
      storage: {
        value: content,
        representation: 'storage'
      }
    }
  };

  const options = {
    method: 'post',
    contentType: 'application/json',
    headers: {
      'Authorization': 'Basic ' + Utilities.base64Encode(`${USERNAME}:${API_TOKEN}`)
    },
    payload: JSON.stringify(payload)
  };

  const response = UrlFetchApp.fetch(url, options);

  const json = JSON.parse(response.getContentText());
  Logger.log('Page created successfully:' + response.getContentText());
  
  // 新しく作成されたページのIDを取得
  const newPageId = json.id; // 新しいページのID
  return newPageId; // ページIDを返す
}

/**
 * Slackに通知を送信します。
 */
function sendSlackNotification(title,pageUrl) {
  const webhookUrl = 'https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX'; // ここにWebhook URLを入力(事前準備6で取得したWebhook URL)
  const channelId = 'SLACKCHANNELID'; //メンションしたいチャンネルのID(事前準備7で取得したCHANNELID)
  const payload = JSON.stringify({
    text: `<#${channelId}> 新しいページが作成されました: ${title}`,
    attachments: [
      {
        color: "#36a64f",
        title: title,
        title_link: pageUrl,
        text: "本日の定例議事録が作成されました。\nMTG開始までに業務報告をご記載ください。",
        actions: [
          {
            type: "button",
            text: "ページを確認",
            url: pageUrl
          }
        ],
        footer: "Confluence Notification",
        ts: Math.floor(Date.now() / 1000)
      }
    ]
  });

  const options = {
    method: 'post',
    contentType: 'application/json',
    payload: payload
  };

  UrlFetchApp.fetch(webhookUrl, options);
}
```

### 3. トリガー設定、実行
[Google App Script](https://script.google.com/home)で新しいプロジェクトを作成し、そちらに上記スクリプトを貼り付けます。保存後、トリガーの設定を行います。
今回は毎週水曜日の指定した時間に１回動くように設定しています。
※GASでは`週ベースのタイマー`で指定できる時間が9時〜10時のように**1時間枠の中のどこか**でしか指定ができませんのでご注意ください。
![スクリーンショット 2024-12-25 16.46.42.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3757872/6fca4806-b024-4a8f-9624-91058217282b.png)
![スクリーンショット 2024-12-25 16.52.39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3757872/4f80906b-1311-5273-2bb0-6f29b3b0b3fb.png)
![スクリーンショット 2024-12-25 16.53.50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3757872/ffd67770-34fe-5f22-87ea-c02072dfc083.png)

マイトリガーを確認し、以下のように表示されていればトリガー設定完了です。
![スクリーンショット 2024-12-25 16.54.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3757872/73201775-a327-aa81-26c2-6b557b133f8e.png)

上記のスクリプトが実行されると新しいページが指定されたスペースに作成され、
以下のようなリンク付き通知が指定したSlackチャンネルへ飛びます。
![スクリーンショット 2024-12-25 17.05.02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3757872/bba0afd1-f5aa-7d92-fed8-11af33ed1886.png)


### 注意点
APIの使用には適切な権限が必要です。
スペースキーやタイトル、コンテンツは適宜変更してください。
これで、Confluenceの任意のページを自動で作成する準備が整います。必要に応じてカスタマイズしてみてください。

## 最後に
いざ作ってみたらパラメータ取得方法の調査で結構時間がかかってしまいました・・・。
上記記載の事前準備部分ができてしまえばきっと大丈夫だと思います。
この記事を見た人の業務が少しでも楽になったら嬉しいです！