足元サーバがないのでlambdaでConfluenceの定例用ページを定期投稿させてみる。

みなさまこんにちは。 
昨年末に [足元サーバがないのでlambdaでConfluenceに日報の投稿をさせてみる。](https://qiita.com/akiko-pusu/items/e17938e96e62f0561992) という記事を書きました。
あれから半年が経過し、そこそこlambdaやConfluence APIに慣れてきたところ。

今回は、その続編になります。

## はじめに

### 新規ページでやってみたけど

前回はConfluenceのREST APIでページ作成をしました。
その際に、マクロも入っていると嬉しいので、POSTのbodyには目次マクロなどを差し込むといったことを行いました。

### 前週のページをコピーして作りたい

ですが、「毎週x曜日に開催する定例用のページを定期的に作りたい」という要望が出てきました。

しかも、タイトルの日付だけを変えて新規にページを起こすというよりは、「**前の週のページをコピーして、新しいページとして作成したい**」といった要望です。

ブラウザでGUIであれば、ページをコピーすることは可能なのですが、ConfluenceのREST APIにはページコピーの機能がありません。

また、中には「ページ」ではなくて「ブログ」として定例ページを作成しているパターンもあります。
これも、基本は前週したブログ記事をコピーして、タイトルの日付だけを変えて定期したい、というものでした。

## やりたいことを整理する

ということで、できると嬉しいことは以下の通りとなりました。

- 定例のミーティングの前日とかに、**定期的に自動で**議事録ページができてほしい
- 新規ではなく、できれば**前週の内容をコピーして**ほしい
- タイトル日付は**実際にミーティングのある日**に設定してほしい (作成日でなくて)
  - 一部はブログもある
- あるページの子ページとして作成してほしい
- ページが出来上がったら、「ページの中身を調整してね」とSlackで通知してほしい

## 方法を考える

最終的には、サーバレスでLambdaで実行するとして、まずは前回と同じくPythonで書いてみることにしました。

考えた流れは以下の通りです。

- コピーもとになるページは、あるページの子ページにある、一番最新のものを利用する
 - ある親ページから、最新の子ページを取得
 - 該当の子ページの中身をAPIでGET
 - 日付を変えたタイトル、コピーした内容を本文にして、ある親ページの子ページとして作成
 - 作成後に、「このページはx月x日のページをコピーして作りました、本文を修正してね」のコメントを追加する
 - ページについているラベル情報もコピーして、追加したページに同じラベルを設定する 

## パラメーターはこんな感じ

なんとなく「自動でほしい」というページの幾つかを提示してもらい、パターンをつかんで、以下のようなパラメータを使って、ある程度同じようにできるようにしました。

- パラメータとして、以下は必須とする
 - 親ページID (どのツリーから下に作成するか）
 - ページタイプ（pageかblogか)
  - ブログの場合は、親ページの概念がないので、直前の参照元を特定できるラベルをパラメータに渡す 
 - 毎週何曜日に作成されるといいのか　（曜日情報）
 - 実際のミーティングは作成日から何日後に実施なのか ( + 2days? -3 days? など）
 - 作成したらSlackでメンションしたい相手は誰か
 - Slackのどのチャンネルに通知するか

## できたコードはこんな感じ

ただし、Lambdaで呼び出す lambda_handlerの部分のみ抜粋です。
ざっくりとした全文はこちらです...

- https://gist.github.com/akiko-pusu/da06284b6d828b8744cb929b7e0c5727
  - ConfluencePagePostというクラスを定義してますので、全文でないと動きません _ _

```py
def lambda_handler(event, context):
    # eventオブジェクトからパース

　　 # Confluenceの操作用（親ページ、コンテンツのタイプ、タイトル、x日後に日付をセット）
    parent_id = int(event['parent_id'])
    target_date_range = int(event['date_range'])
    title = event['title']
    content_type = event['type']
    
    # Slackへの通知用
    target_user = event['target_user']
    channel = event['channel']
   
    # 初期化
    from_id = 0 
       
    # ConfluenceのエンドポイントとAPI用のアカウント、キーは環境変数から
    base_url = os.environ["CONFLUENCE_BASE_URL"]
    account = os.environ['ACCOUNT']
    api_key = os.environ['API_KEY']
    # slackのWebHookです (Exp. 'https://hooks.slack.com/services/XXXXXXXX')
    hook_url = os.environ['SLACK_HOOK_URL']

    pagePost = ConfluencePagePost(base_url)
    pagePost.set_auth(account, api_key)

    now = datetime.now()
    target_date_obj = now + timedelta(days=target_date_range)
    target_date = target_date_obj.strftime("%Y%m%d")
    date_str = target_date_obj.strftime("%Y%m%d")

    if content_type == 'page':
        from_id = pagePost.get_latest_child(parent_id)

    if content_type == 'blogpost':
        spaceKey = event['space_key']
        tag = event['tag']
        from_id = pagePost.search_content_by_term(title, tag, spaceKey, 'blogpost')

    # 指定のページをコピーして、ページを作成
    content = pagePost.copy_content(from_id)
    response = pagePost.create_copy_from_content(date_str, content)

　　 # 作成したページに、「xxをコピーして作りました」のコメントを付加
    new_id = response['id']
    from_title = content['title']

    from_url = pagePost.get_page_url_by_page_id(from_id)
    comment_body = pagePost.generate_comment_body_for_copy(from_title, from_url)
    pagePost.add_comment(new_id, content['type'], comment_body)

    # 作成したページに、元のページと同じラベルを設定
    label = pagePost.copy_labels(from_id)
    pagePost.add_labels(new_id, label['results'])

　　 # slackへの通知準備
    to_url = pagePost.get_page_url_by_page_id(new_id)
    to = datetime.now()
        
    message = pagePost.generate_message_to_notice_slack(target_user, title, to_url, target_date)
    pagePost.send_message_to_slack(hook_url, message, channel)
```

最終的にはLambdaでの実行ですが、Pythonのコンソールからメソッド単位で実行をチェックできるように、細かく分けています。

### Lambda側での設定

さて、できあがったPythonのコードをLambda側にアップします。
ここでは触れませんが、LambdaやAPI Gatewayなどのサーバレス環境をローカル開発するためのライブラリがありますので、ローカルでの動作チェックやAWS側へのアップロードは、aws-sam-cli というものを使っています。

- Ref: https://github.com/awslabs/aws-sam-cli (旧: aws-sam-local)
  - これを使うと、開発用のローカルマシンにLambdaの環境を模したコンテナが立ち上がり、その中でlambda_handlerが呼ばれます

AWS側に登録したLambda関数は、イベントのソースを複数設定できます。
このため、定期実行をCloudWatchのタイマーに任せたい場合は、CloudWatch Eventで設定してみました。

また、SlackからOutgoing WebhookとしてAPI Gatewayに送信し、そのデータをLambda側に渡すということも試してみています。

![event-source-setting.png](https://qiita-image-store.s3.amazonaws.com/0/27976/d0434a0b-b564-796d-35fe-f88a03a656aa.png)

#### CloudWatch Eventとの関連付け

CloudWatchとの関連付けは、こんな感じです。
注意点としては、cron式っぽいのですが書き方がちょっと違うことと、UTCで定義のため、JSTだとこの時間になるように...といったことに注意して設定、ということです。

![cloudwatch_event.png](https://qiita-image-store.s3.amazonaws.com/0/27976/0b5a6a56-d375-6a79-6bf3-3ab224cd44c1.png)


CloudWatch側には、カスタムのイベントデータとして、次のようなJSONを渡します。

```
{ "parent_id": 親ページID, "date_range": "+1", 
  "target_user": "here", "title": "xxxx定例", 
  "channel": "#anime", "type": "page" }
```

このイベントデータが渡ると、親ページの下の最新の子ページの内容をコピーしてページを作り、タイトルを調整し、コメントをつけて、ラベルも追加して、Slackで最後に通知という処理が流れます。

![post-result.png](https://qiita-image-store.s3.amazonaws.com/0/27976/5a12d6d1-cc95-f56c-b2e1-77d0e100eb1b.png)


### 注意点とか

この例だと、環境変数で色々値を渡しているので、更に暗号化したい...。
また、定期実行とはいえ、CloudWatch側でイベントが発火してから、Lambdaのコンテナが立ち上がるまでに少し時間がかかることがあるので、「xx時丁度」を期待していると、精確さに欠けるところが出てきます。

外部APIを叩く関係で、Lambdaのタイムアウト値が小さすぎると途中で失敗することもあります。

## やってみての実感

[前回](https://qiita.com/akiko-pusu/items/e17938e96e62f0561992) の記事に添えていた、以下はなんとかクリアできました。

> Lambda側の定期実行の処理や、POST完了後にSlackに通知すると行った処理も必要になります。

ただし、CloudWatchのイベントの登録方法が特殊なこともあり、もうちょっと希望したスタッフでも管理しやすいようにしたいなと思っています。

例えば、Google SpreadSheetで定期実行したいページをリストで管理して、GASで定期的にAPI Gatewayを叩いてページを作成する、などです。
またうまくいきそうでしたら、続きを書いてみたいと思います。

## 20181212 追記：

``GASで定期的にAPI Gatewayを叩いて`` の件ですが、実はGoogleAppsScriptの定期実行も、若干の揺らぎがあって精確に「〇〇時ちょうど！」というのができなさそうでした...。
（職場でスプレッドシートに記載したお当番表を参照し、来週の予定になっている人にSlackでメンションを送る、というのを実装してみたのですが、タイミングに幅があるようで）