Cloudflare Workers Cronトリガーを試す with Twitter API & ガーシーch

# はじめに
Cloudflare WorkersにCronの機能があったので、試した結果をまとめる。

https://developers.cloudflare.com/workers/examples/cron-trigger/

試しに作ったものは以下のとおり。
- ①Twitterからユーザーの情報を取得（ガーシーch）
- ②取得した情報をKVに保存する
- ①＋②の処理をWorkersのCronトリガーで定期実行する
- Workersの機能でKVに保存した情報をリストで取得できるようにする（確認用）

:::note info
## 2022/7/2 追記
6月末にガーシーchがBANされました。
その影響でうまく表示できなくなっていたので以下の修正をしました。

<details>
<summary>詳しく見てみる</summary>

### 状況
- Cloudflareのエラーの画面が表示される状態
- システムエラーで全く動かなくなっていた
- エラー時の実装を全くしていなかったため
- twitterが返してきているデータは以下
```
{"errors":[{"parameter":"username","resource_id":"GaaSyy_ch","value":"GaaSyy_ch","detail":"User has been suspended: [GaaSyy_ch].","title":"Forbidden","resource_type":"user","type":"https://api.twitter.com/2/problems/resource-not-found"}]}
```

### 対処
現在時刻を基準に過去データを取るようにしていたのを、「2022/06/30T00:00」を基準に取るように変えた。
現在時刻基準だとエラーしか取れないため。
エラーの場合でも表示できるようにエラー情報を出すように変えた。

### 補足
取れる情報はこちら。
6/29の20～21時にアカウント停止になったようです。
直前はフォロワー数が急増している。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/273897/0d935b0e-74ba-819e-5f20-0ae123bdddfd.png)

</deatils>
:::

# 前提事項
Cloudflare Workersに関する基礎的な情報は省略。

# 成果物
ブラウザでガーシーchの毎時の情報が見れるものが完成。
情報は自動で毎時蓄積されていく。

https://workers-cron-app.donraku.workers.dev/tail
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/273897/43b5e13f-fe91-a3ff-37d1-797612206869.png)

https://workers-cron-app.donraku.workers.dev/tail/12
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/273897/c3260ecd-9aac-f408-8712-dce5a8bb89f9.png)
2022/5/23時点では、フォロワー数が毎時数十件単位で増えているようだ。

# 詳細
ソースとポイントを晒していく。

# Twitterからユーザーの情報を取得する処理
当然ながらTwitterAPIの申請済みの前提。
Twitterから取得したTOKENをTWITTER_TOKENに設定しておく必要あり。

```javascript:index.jsの抜粋
/**
 * Twitterからユーザー情報のデータを取得
 * @param {string} username 
 * @returns 取得したJSON文字列
 */
async function fetchTwitterData(username) {
  const url = `https://api.twitter.com/2/users/by/username/${username}?user.fields=public_metrics`;
  const request = new Request(url, {
    method: "GET",
    headers: {
      "authorization": `Bearer ${TWITTER_TOKEN}`
    }
  });
  const resp = await fetch(request);
  const value = JSON.stringify(await resp.json());
  console.log(value);
  return value;
}
```

## 補足：TWITTER_TOKENの設定について
TOKENはセキュアな情報なので、ソースには書かないでCloudflareの環境変数に設定する。

設定：
`wrangler secret put TWITTER_TOKEN`

リストで確認：
`wrangler secret list`

```console:実行例
C:\donraku\cloudflare\workers-cron-app>wrangler secret put TWITTER_TOKEN
 ⛅️ wrangler 2.0.5 (update available 2.0.6)
-----------------------------------------------------
Enter a secret      ****************************************************************************************************
value:              ************
🌀 Creating the secret for script workers-cron-app
✨ Success! Uploaded secret TWITTER_TOKEN

C:\donraku\cloudflare\workers-cron-app>wrangler secret list
[
  {
    "name": "TWITTER_TOKEN",
    "type": "secret_text"
  }
]
```

dashboardでは以下のように見える。画面でも設定は可能。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/273897/c972d3d1-7818-2584-db5a-db44623a80ea.png)

## Twitterからユーザー情報のデータを取得してKVに保存する処理
上の関数で取得した結果をKVに保存。
保存のキーは現在時刻。

```javascript:index.jsの抜粋
/**
 * Twitterからユーザー情報のデータを取得してKVに保存
 * @param {string} username 
 * @returns 保存したJSON文字列
 */
 async function storeTwitterData(username) {
  // データを取得
  const value = await fetchTwitterData(username);
  // 現在時刻でKVに保存
  const now = new Date();
  await KV_TW.put(now.toISOString(), value);
  return value;
}
```

## Cronトリガーで実行


```javascript:index.jsの抜粋
const TWITTER_USERNAME = "GaaSyy_ch";

addEventListener('scheduled', event => {
  event.waitUntil(triggerEvent(event));
});

async function triggerEvent(event) {
  console.log(event.cron);
  // Twitterからユーザー情報のデータを取得してKVに保存
  await storeTwitterData(TWITTER_USERNAME);
  console.log('cron processed');
}
```

## Cron設定について
`wrangler.toml`に設定を書いておくと指定の時間に起動する。
この例だと毎時0分に起動する。

```wrangler.toml
・・・省略・・・

[triggers]
crons = ["0 * * * *"]
```

dashboardでは以下のように見える。画面でも設定は可能。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/273897/46f97a53-25c4-3340-1c0c-3635252cc94e.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/273897/6c6fd1b6-55d5-cb57-8296-1292cfff2130.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/273897/014aa455-a417-fbd2-4cdc-4b22854c7827.png)

## KVに保存した情報をリストで取得する処理
確認用に保存した情報を見れるようにした。

`https://workers-cron-app.donraku.workers.dev/tail/3`
例えば、↑で直近3時間の情報を取得できる。

`https://workers-cron-app.donraku.workers.dev/store`
↑で情報取得もできる。

<details><summary>ソース</summary>

```javascript:index.jsの抜粋
addEventListener('fetch', event => {
  return event.respondWith(handleRequest(event.request));
});

async function handleRequest(request) {
  const { pathname } = new URL(request.url);

  if (pathname.startsWith("/store")) {
    const result = await storeTwitterData(TWITTER_USERNAME);
    return new Response(result); 
  }

  if (pathname.startsWith("/tail")) {
    let count = pathname.split("/")[2];
    if (count == null) {
      count = 5; //省略時は5
    } else if (count > 48) {
      count = 48; //最大48
    }

    let lists = [];
    for (let i = 0; i < count; i++) {
      const data = await fetchKVData(i);
      lists.push(data);
    }
    let strList = "ガーシーch 情報\n";
    strList += formatList(lists);

    return new Response(strList); 
  }

  return new Response("wellcome!"); 
}

async function fetchKVData(hoursAgo) {
  const dateUtc = new Date();
  dateUtc.setHours(dateUtc.getHours() - hoursAgo);

  const keyPrefix = dateUtc.toISOString().substring(0, 13) + ':00';
  const list = await KV_TW.list({ prefix: keyPrefix });
  if (list.keys == "") {
    return null;
  }

  const value = await KV_TW.get(list.keys[0].name);
  console.log(value);
  const json = JSON.parse(value);

  dateUtc.setHours(dateUtc.getHours() + 9);
  const dateJst = dateUtc.toISOString().substring(0, 13).replace("T", " ") + ":00";

  return {
    name: json.data.name,
    date_jst: dateJst,
    followers_count: json.data.public_metrics.followers_count,
    following_count: json.data.public_metrics.following_count,
    tweet_count: json.data.public_metrics.tweet_count,
    listed_count: json.data.public_metrics.listed_count
  };
}

function formatList(lists) {
  let strList = "";
  let followers_count = 0;
  for (let i = lists.length - 1; i >= 0; i--) {
    const data = lists[i];
    if (data == null) {
      strList += `no data.\n`
    } else {
      let flu = data.followers_count - followers_count;
      if (followers_count == 0) {
        flu = '--';
      } else if (flu > 0) {
        flu = "+" + flu;
      }
      strList += `${data.date_jst}`
      strList += ` ツイート：${data.tweet_count}`
      strList += ` フォロワー：${data.followers_count}(${flu})`
      strList += ` リスト登録：${data.listed_count}\n`
      followers_count = data.followers_count;
    }
  }
  return strList;
}
```

</details>

以上です。