4
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ZOZOAdvent Calendar 2024

Day 22

Confluence APIとGASで定例の資料を毎日自動生成する

Last updated at Posted at 2024-12-21

これは ZOZO Advent Calendar 2024 カレンダー、シリーズ 8 の 23 日目の記事です。

はじめに

弊社では社内wikiとしてConfluenceを利用しています。
定例などで毎日同じ資料を作成する必要があったのですが、Confluence APIとGoogle Apps Script(GAS)を使用して資料作成を自動化してみたので紹介しようと思います。

後述しますが、今回使用するAPIのバージョンは非推奨になっているので、可能であれば最新バージョンのものを採用するようにしてください(本記事も最新バージョンへの移行が完了次第更新しようと思います)。

Confluence API

Confluenceはページ情報の取得やページ作成といった機能をAPIとして提供しています。
ref. https://developer.atlassian.com/cloud/confluence/rest/v1/intro

手元で動作確認する前に、Confluence APIを使用するためにはアクセストークンが必要なのでマイページからパーソナルアクセストークンを生成しておきます。
(https://<ドメイン>/wiki/plugins/personalaccesstokens/usertokens.actionからトークン管理ページに飛べます)
スクリーンショット 2024-12-19 20.06.14.png

トークンを生成したらAPIを叩いてみます。
今回はページ作成APIを使用するので、Postmanで動作確認してみます。

スクリーンショット 2024-12-19 21.31.40.png
スクリーンショット 2024-12-19 21.36.13.png

AuthorizationヘッダーにはBearer xxxx(パーソナルアクセストークン)の形で値をセットします。
リクエストボディには作成するページのスペースキーを指定します。
スペースキーはスペースのホーム画面のサイドバーから「ページ」を選択するとhttps://<ドメイン>/wiki/collector/pages.action?key=<スペースキー>に遷移するので、このURLから取得できます。
(適当なページに対してコンテンツ取得APIを叩いても取得できます)

コンテンツ取得APIの実行例(jqで整形しています)
$ curl -X GET 'https://xxxx/wiki/rest/api/content/774059380' -H 'Authorization: Bearer xxxx' -H 'Accept:application/json' | jq .

  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100  2366  100  2366    0     0  17859      0 --:--:-- --:--:-- --:--:-- 17789
{
  "id": "774059380",
  "type": "page",
  "status": "current",
  "title": "2024/12/23 チーム定例",
  "space": {
    "id": xxxx,
    "key": "xxxx",    # これがスペースキー
    "name": "xxxx",
    "type": "personal",
    "_links": {
      "webui": "/spaces/viewspace.action?key=xxxx",
      "self": "https://xxxx/wiki/rest/api/space/xxxx"
    },
    "_expandable": {
      "metadata": "",
      "icon": "",
      "description": "",
      "retentionPolicy": "",
      "homepage": "/rest/api/content/xxxx"
    }
  },
  "history": {
    "latest": true,
    "createdBy": {
      "type": "known",
      "username": "xxxx",
      "userKey": "xxxx",
      "profilePicture": {
        "path": "/wiki/images/icons/profilepics/default.svg",
        "width": 48,
        "height": 48,
        "isDefault": true
      },
      "displayName": "xxxx",
      "_links": {
        "self": "https://xxxx/wiki/rest/api/user?key=xxxx"
      },
      "_expandable": {
        "status": ""
      }
    },
    "createdDate": "2024-12-19T14:55:18.399+09:00",
    "_links": {
      "self": "https://xxxx/wiki/rest/api/content/774059380/history"
    },
    "_expandable": {
      "lastUpdated": "",
      "previousVersion": "",
      "contributors": "",
      "nextVersion": ""
    }
  },
  "version": {
    "by": {
      "type": "known",
      "username": "xxxx",
      "userKey": "xxxx",
      "profilePicture": {
        "path": "/wiki/images/icons/profilepics/default.svg",
        "width": 48,
        "height": 48,
        "isDefault": true
      },
      "displayName": "xxxx",
      "_links": {
        "self": "https://xxxx/wiki/rest/api/user?key=xxxx"
      },
      "_expandable": {
        "status": ""
      }
    },
    "when": "2024-12-19T15:00:55.741+09:00",
    "message": "",
    "number": 1,
    "minorEdit": false,
    "hidden": false,
    "_links": {
      "self": "https://xxxx/wiki/rest/experimental/content/774059380/version/1"
    },
    "_expandable": {
      "content": "/rest/api/content/774059380"
    }
  },
  "extensions": {
    "position": "none"
  },
  "_links": {
    "webui": "/pages/viewpage.action?pageId=774059380",
    "edit": "/pages/resumedraft.action?draftId=774059380&draftShareId=76490fab-b46c-4d65-a2af-1d0ed52ae1b8",
    "tinyui": "/x/dDUjLg",
    "collection": "/rest/api/content",
    "base": "https://xxxx/wiki",
    "context": "/wiki",
    "self": "https://xxxx/wiki/rest/api/content/774059380"
  },
  "_expandable": {
    "container": "/rest/api/space/xxxx",
    "metadata": "",
    "operations": "",
    "children": "/rest/api/content/774059380/child",
    "restrictions": "/rest/api/content/774059380/restriction/byOperation",
    "ancestors": "",
    "body": "",
    "descendants": "/rest/api/content/774059380/descendant"
  }
}

親ページのIDも同様に、生成するページの親ページのURL(https://<ドメイン>/wiki/pages/viewpage.action?pageId=<ページID>)から取得できます。

APIを叩くとページが作成されるのを確認できました。
HTMLタグもいい感じに見出しやテーブルなどに解釈してくれてますね。

スクリーンショット 2024-12-20 12.02.56.png

ちなみに、テーブルが空欄だと余白が潰れて見栄えがあまり好ましくなかったので無理やり穴埋めしています。

このように、Confluence内で編集するより勝手は悪いので、凝ったデザインとかは難しいかもしれないです。

最新バージョンのAPIでは正常なレスポンスを確認できなかったので、とりあえず運用するという目的で今回は非推奨バージョンでAPIを叩きます(動作確認でき次第、本記事も更新しようと思います)。

最新バージョンでの動作確認

Bearerトークンをセットして投稿APIを叩いていますが、You are already logged inというタイトルのHTMLページがレスポンスされ、ページは作成されません。
(同じような現象で問い合わせされていたが、解決してなさそう)
有識者の方、原因に心当たりあればご教示いただけますととっても助かります。

スクリーンショット 2024-12-20 14.36.27.png

GAS

ページ作成できることを確認できたので、GASで定期的に実行してSlackに通知しようと思います。

Slack Appの準備

先にSlack Appを作成しておきます。
(こちらのブログが図解も豊富で分かりやすいかもです)

  1. https://api.slack.com/apps からCreate New App > From scratchを選択します
  2. アプリ名を入力し、ワークスペースを選択したらアプリが生成されます
  3. 左のメニューバーからOAuth & Permissionsを選択し、Add an OAuth Scopeでスコープを追加します。今回はchat:writechat:write.customize
    chat:write.publicincoming-webhookを追加しました
  4. 左のメニューバーからInstall Appを選択し、ワークスペースに通知する権限を許可します
  5. Bot User OAuth Tokenが表示されるので控えておきます

スプシの準備

自分の所属するチームの定例は①毎日行う②ファシリテーターは週ごとに交代するという運用方法なので、その要件に沿って実装していきます。

ファシリテーターを毎週変更するために、スプシに以下の内容でシートを用意しておきます。idはSlackで担当者をメンションするためのメンバーIDです。Slackでメンバーのプロフィールを開き、メンバーIDをコピーで取得できます。

GASの準備

GASの内容は以下のように実装しました。

MEMBER_NUM = 3;

let token = PropertiesService.getScriptProperties().getProperty("SLACK_TOKEN");
let channelID= PropertiesService.getScriptProperties().getProperty("CHANNEL_ID");

function main() {
  let facilitator = getFacilitator();
  let pageID = createPage();
  if (facilitator) {
    sendSlackMessage(facilitator, pageID);
  }
}

// その週のファシリテーターを取得します
function getFacilitator() {
  let today = new Date();
  let dayOfWeek = today.getDay();
  if (dayOfWeek === 0 || dayOfWeek === 6) {
    return;
  }
  let sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('<スプシのシート名>');
  let membersLastRow = sheet.getLastRow()
  let members = sheet.getRange(2, 2, membersLastRow - 1).getValues().map(row => row[0]);
  let facilitator_index = sheet.getRange('C2').getValue();
  if (dayOfWeek === 1) {
    if(facilitator_index+1 >= MEMBER_NUM) {
      facilitator_index = 0
    } else {
      facilitator_index++;
    }
    updateSpreadSheet(sheet, facilitator_index)
    sheet.getRange('C2').setValue(facilitator_index)
  }
  return members[facilitator_index];
}

// Confluence APIでページを作成します
function createPage() {
  let url = "https://<ドメイン>/wiki/rest/api/content";
  let dt = new Date();
  let y = dt.getFullYear();
  let m = ("00" + (dt.getMonth()+1)).slice(-2);
  let d = ("00" + (dt.getDate())).slice(-2);
  let title = y+m+d+' チーム定例';
  let payload = {
    "title": title,
    "type": "page",
    "space": {
      'key':'<スペースキー>'
    },
    "ancestors": [
      {
        "id": "<親ページのID>"
      }
    ],
    "body": {
      "storage": {
          "representation": "storage",
          "value": "<h1>質問・共有事項</h1><table><tr><th>記入者</th><th>内容</th><th>議事録</th></tr><tr><td></td><td></td><td></td></tr></table><br/><h1>進捗確認</h1><h2>案件1</h2><a href='https://example.com'>https://github.com/project1</a><br/><h2>案件2</h2><a href='https://example.com'>https://github.com/project2</a><br/>"
      }
    }
  }

  let options = {
    "method": "post",
    "headers": {
      "Authorization": "Bearer <パーソナルアクセストークン>",
      "Accept": "application/json",
      "Content-Type": "application/json"
    },
    "payload" : JSON.stringify(payload),
    "muteHttpExceptions": true
  }

  let response = JSON.parse(UrlFetchApp.fetch(url, options));
  return response["id"];
}

// Slackに通知します
function sendSlackMessage(facilitator, pageID) {
  let message = "<@" + facilitator + ">\n今日のチーム定例のファシリテーターです。\n資料: https://<ドメイン>/wiki/pages/viewpage.action?pageId=" + pageID;
  let slackApp = SlackApp.create(token);
  slackApp.postMessage(channelID, message);
}

トークンはSlack App作成時に控えておいたBot User OAuth Tokenです。
チャンネルIDは通知したいチャンネル名を右クリックし、コピー>リンクをコピーで取得したURLから取得できます(https://<ドメイン>/archives/<チャンネルID>という形式)。
どちらもスクリプトプロパティにキー・バリューを保存し、PropertiesService.getScriptProperties().getProperty()で取得しています(スクリプトプロパティは左メニューバーのプロジェクトの設定から保存できます)。

試しにmain関数を実行してみましょう。対象の関数がmainに指定されていることを確認し、実行ボタンを押します。

スクリーンショット 2024-12-21 17.46.58.png

Confluenceにページが作成されてSlackに通知が飛んでいることを確認できました!

bRYjx3UQjRNUmE41734769605_1734769710 (2).png

定期実行の設定

最後に、定期的にGASを実行するようにトリガーを追加します。
左メニューバーのトリガーを選択し、トリガーを追加をクリックします。
ポップアップが表示されるので、お好みで関数の実行タイミングを設定します。今回は毎日ページ作成と通知を行うために時間主導型日付ベースのタイマー午前11時〜午後12時で設定しました。

スクリーンショット 2024-12-21 17.35.28.png

これで毎日指定した時刻の間に関数が実行され、資料の作成とSlackの通知を自動化することができました🎉

まとめ

Confluence APIとGASを使って、毎日資料を作成してSlackに通知する方法を見てきました。
今回の実装だと、定例で更新された内容を次の資料に反映させることができなかったり、資料の内容をHTMLで直書きしていて更新が面倒なので、この辺りは今後改善していきたいですね。

ではまた。

4
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?