特定のユーザーのQiita新規投稿を定時でチャットに流す

部内で Qiita に記事投稿したら教えてね、とのお願いがありました。
メールやチャットでの連絡だと忘れがちなので定期的にチャット(Teams)に POST してくれる JavaScript を書いてみます。

利用するサービス群は以下です：

- [Qiita API](https://qiita.com/api/v2/docs#概要)
- [Microsoft Teams Incoming Webhook](https://docs.microsoft.com/ja-jp/microsoftteams/microsoft-teams)
- [Firebase Cloud Functions](https://firebase.google.com/docs/functions/?hl=ja)
- [cron-job.org](https://cron-job.org/en/)

Firebase は事前にアカウント登録し、プロジェクトを作成、Functions を作成します。
Functions は無料プランだと外部 API 叩けないので、有料の Blaze プランにします。
無料枠内でなんとかなるのでなんとかする[^1]。
cron-job.org も事前にアカウントを登録します。こちらは無料。
Qiita は 1 時間 60 回までなら認証なしで API 叩けるので特に何もいりません。

## 各サービスの API を調べる

### Qiita

Qiita:Teams には Webhook はあるのですが、通常の Qiita にはありません。
Qiita の API に Organization 単位の投稿情報を取得するものも用意されていませんでした。

[Qiita API Docs](https://qiita.com/api/v2/docs#%E6%A6%82%E8%A6%81)

用意されている API からできそうな方法を考えてみます。
Organization 名からユーザーを限定することは難しそうです。
代替案として以下は使えるかもしれません。

- [GET /api/v2/users/:user_id/followees](https://qiita.com/api/v2/docs#get-apiv2usersuser_idfollowees)
  - ユーザがフォローしているユーザ一覧を取得します。

アカウント集約用アカウント(`tdcsoft`)を作成して[^2]、Organization のメンバーをフォローします。
この状態で API 叩いてみましょう。

```js
// GET https://qiita.com/api/v2/users/tdcsoft/followees
;[
  {
    description: null,
    facebook_id: null,
    followees_count: 3,
    followers_count: 5,
    github_login_name: null,
    id: 'ukiuki',
    items_count: 2,
    linkedin_id: null,
    location: null,
    name: '',
    organization: null,
    permanent_id: 94278,
    profile_image_url:
      'https://qiita-image-store.s3.amazonaws.com/0/94278/profile-images/1473706223',
    team_only: false,
    twitter_screen_name: null,
    website_url: null
  },
  {
    description: null,
    facebook_id: null,
    followees_count: 13,
    followers_count: 44,
    github_login_name: 'TakahiRoyte',
    id: 'TakahiRoyte',
    items_count: 13,
    linkedin_id: null,
    location: null,
    name: '',
    organization: null,
    permanent_id: 89911,
    profile_image_url:
      'https://qiita-image-store.s3.amazonaws.com/0/89911/profile-images/1518364681',
    team_only: false,
    twitter_screen_name: 'TakahiRoyte',
    website_url: null
  }
]
```

配列で私と ukiuki さんのユーザー情報取れますね。
あとはユーザーごとに最新の投稿を取得できれば良さそうです。
どうやら投稿記事の API がクエリ対応してそうです。

- [GET /api/v2/items](https://qiita.com/api/v2/docs#get-apiv2items)
  - 記事の一覧を作成日時の降順で返します。
    - query
      - 検索クエリ
      - Example: "qiita user:yaotti"
      - Type: string

対応しているクエリは[Qiita 検索画面](https://qiita.com/search?)に用意されてるものと同じですね。
作成するスクリプトを午前 00:05 に流すとして、「指定のユーザー名 + 昨日より新しい記事」を取れれば OK なはず。
API を叩いてみます。

```js
// GET https://qiita.com/api/v2/items?query=user%3ATakahiRoyte+created%3A%3E2018-12-01
;[
  {
    rendered_body: '略',
    body: '略',
    coediting: false,
    comments_count: 0,
    created_at: '2018-12-15T18:12:00+09:00',
    group: null,
    id: '855b6116c8132b0c9286',
    likes_count: 7,
    private: false,
    reactions_count: 0,
    tags: [
      {
        name: 'アジャイル',
        versions: []
      },
      {
        name: 'スクラム',
        versions: []
      },
      {
        name: 'ソフトウェア開発',
        versions: []
      }
    ],
    title: 'スクラムを失敗させる51のアンチパターン',
    updated_at: '2018-12-15T18:12:00+09:00',
    url: 'https://qiita.com/TakahiRoyte/items/855b6116c8132b0c9286',
    user: {
      description: null,
      facebook_id: null,
      followees_count: 13,
      followers_count: 44,
      github_login_name: 'TakahiRoyte',
      id: 'TakahiRoyte',
      items_count: 13,
      linkedin_id: null,
      location: null,
      name: '',
      organization: null,
      permanent_id: 89911,
      profile_image_url:
        'https://qiita-image-store.s3.amazonaws.com/0/89911/profile-images/1518364681',
      team_only: false,
      twitter_screen_name: 'TakahiRoyte',
      website_url: null
    },
    page_views_count: null
  }
]
```

取れますね！

### Microsoft Teams

Teams は Connectors という機能を有効にして Incoming Webhook を受け取ることができます。
Connector を作って Incoming Webhook の URL を生成しておきましょう。

[Teams Document - Using connectors](https://docs.microsoft.com/ja-JP/microsoftteams/platform/concepts/connectors/connectors-using)

Connector では Message Card というメッセージのフォーマット機能が使えます。
Microsoft が最近推している[Adaptive Cards](http://www.atmarkit.co.jp/ait/articles/1803/27/news039.html)は残念なことに対応していません。
以下のカードを自由に作れるサイトで色々試して良い感じのレイアウトを作りましょう。
左上のテンプレートから下の方の Legacy Message Card の好きなのを選んでカスタマイズします。

[Cards Playground](https://messagecardplayground.azurewebsites.net/)

最終的に下の形にしてみました。

```json
{
  "@type": "MessageCard",
  "@context": "http://schema.org/extensions",
  "themeColor": "55C500",
  "summary": "Qiita新着投稿",
  "sections": [
    {
      "activityTitle": "[スクラムを失敗させる51のアンチパターン](https://qiita.com/TakahiRoyte/items/855b6116c8132b0c9286)",
      "activitySubtitle": "TakahiRoyte",
      "activityImage": "https://qiita-image-store.s3.amazonaws.com/0/89911/profile-images/1518364681",
      "markdown": true
    },
    {
      "activityTitle": "[スクラムは問題を可視化するフレームワーク](https://qiita.com/TakahiRoyte/items/93ef9b29b2e61937fb14)",
      "activitySubtitle": "TakahiRoyte",
      "activityImage": "https://qiita-image-store.s3.amazonaws.com/0/89911/profile-images/1518364681",
      "markdown": true
    }
  ],
  "potentialAction": [
    {
      "@type": "OpenUri",
      "name": "Organizationページを開く",
      "targets": [
        {
          "os": "default",
          "uri": "https://qiita.com/organizations/tdc-soft"
        }
      ]
    }
  ]
}
```

行けそうなので実装の流れは以下の感じになりそうです。

1. 集約用アカウントのフォロワー一覧を取得する。
2. 各フォロワーの昨日より新しい投稿記事を取得する。
3. 取得したデータを元に文字列を整形し Teams に POST する。

## 実装

### プロジェクトセットアップ

firebase-tools を利用してプロジェクトをセットアップします。
追加でインストールする npm モジュールは HTTP リクエストを送る[axios](https://github.com/axios/axios)のみ。

```bash
$ npm install -g firebase-tools

$ firebase login
# Firebaseプロジェクトを作成したアカウントでログイン

$ mkdir qiita-new-post

$ cd qiita-new-post

$ firebase init functions
# 1. Yes
# 2. Select FirebaseProject -> 作成したFirebase Project
# 2. JavaScript
# 3. Use ESLint -> No
# 4. npm install now -> Yes

$ cd functions

$ npm install --save axios
```

Functions がちゃんとデプロイできるか確認します。
`functions/index.js`のコメントアウトを外しましょう。

```js:functions/index.js
const functions = require('firebase-functions')

// Create and Deploy Your First Cloud Functions
// https://firebase.google.com/docs/functions/write-firebase-functions

exports.helloWorld = functions.https.onRequest((request, response) => {
  response.send('Hello from Firebase!')
})
```

外したらデプロイコマンド叩きます。

```bash
$ firebase deploy --only functions
```

デプロイ後、表示された URL を直にブラウザで開くと`Hello from Firebase!`が表示されます。

### コーディング

設定と大まかな処理の流れを生成された`index.js`に書いていきます。
API 叩く＝非同期処理なのでメイン処理は async な関数にします。

```js:functions/index.js
const functions = require('firebase-functions')
const axios = require('axios')
const groupAccount = 'tdcsoft'
const qiitaApiBaseUrl = 'https://qiita.com/api/v2'
const teamsWebhookUrl = 'https://outlook.office.com/webhook/123.../IncomingWebhook/123...' // 略

exports.qiitaNewPost = functions.https.onRequest(async (request, response) => {
  // ユーザーリスト取得
  const users = await getUsers()

  // ユーザーリストに紐づく新着記事リスト取得
  const newPosts = await getUsersNewPosts(users)

  // 新規投稿がある場合、Teamsに投稿する
  if (newPosts.length) {
    await postNewPostsToTeams(newPosts)
  }
  response.send('Post Success')
})
```

2018/12/23 現在の Cloud Functions のデフォルトは Node 6 で`async/await`が使えません。
`package.json`に以下の設定を追加して Node 8 で動くようにします。

```json:package.json
"engines": {"node": "8"}
```

各メソッドの実装は 1 つ 1 つ説明してもあれなので、リポジトリを参照していただければと思います。

[qiita-new-post](https://github.com/TakahiRoyte/qiita-new-post)

## 定期実行の仕組みを作る

### Firebase Cloud Function へデプロイ

デプロイコマンド叩きます。

```bash
$ firebase deploy --only functions
```

完了。
試しに URL 叩くともう動くはずです。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/89911/49ec5faa-6f77-8087-ef6c-d576ced6bbd8.png)

動いてる！

### cron-job.org で定期実行セットアップ

Create New Cron で以下のように設定するだけです。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/89911/ae07593b-be1a-9b22-b557-6ac010258cea.png)

後は待つだけ。
完成です。

## あとがき

以上で動くはずですが cron-job.org からの動作確認はこの記事で試します。
もし動かなかったら修正します。

### 12/24 00:05 追記

動きました！
![image.png](https://qiita-image-store.s3.amazonaws.com/0/89911/5ae06179-3a60-ae10-2192-28898273b7f3.png)


[^1]: 個人的に既に有料プラン使ってたのでそこに生やしました。
[^2]: 利用規約を確認した上でユーザー作成しています。