Garmin Connectから自分のデータを取得する~ランサムウェアへの超個人的抵抗~(その3)

本記事は、株式会社ピー・アール・オーアドベントカレンダーの8日目です。

# 前回の続き
[前回](https://qiita.com/pro_matuzaki/items/1d8397fa279154a669ad)では、Garmin Connectから日々の活動を指定年月日以降でダウンロードする部分を作成しました。
今回は、アクティビティの一覧を取得し、そこを足掛かりとして各アクティビティのデータをダウンロードする処理を作りたいと思います。

# アクティビティ？
Garmin Connectにおけるアクティビティとは、特定の種類の運動データを指し、ランニングや自転車、水泳などといった基本的な運動から、変わったところだと釣りとかもあったりします。これはGarminの機器の方にプリセットされたアクティビティを明示的に選ぶことで記録されるデータになります。（なので、どのアクティビティを記録可能か？は機種依存になる）

# 基本的な方針
1. アクティビティ一覧から過去すべてのアクティビティのIDを取得する。
2. 取得したアクティビティIDに対しダウンロードURLをたたき、片っ端から取得する。
3. どこまで取得したか？はローカルストレージに取っておく

こんな感じでやっていきます。

# やっていきます
## アクティビティ一覧の取得方法
まずは、ブラウザで通信を見てみましょう。
![Untitled.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221091/8d54b55e-3855-a4f1-aaa1-4b2416736c33.png)

ほうほう。これですね。

```
https://connect.garmin.com/modern/proxy/activitylist-service/activities/search/activities?limit=20&start=0&_=1607133727539
```

上記で、limitやstartはわかりますね。
_ってやつは何でしょうね？値的にはUNIXタイムスタンプのような気がしたので、その前提でやっていきます。

上記URLを（Garmin Connect認証済みで）たたくと以下のようなレスポンスが返ってきました。

```
[
    {
        "activityId": 5909700814,
        "activityName": "横浜市 ウォーク",
        "description": null,
        "startTimeLocal": "2020-12-04 08:52:33",
        "startTimeGMT": "2020-12-03 23:52:33",
        "activityType": {
            "typeId": 9,
            "typeKey": "walking",
            "parentTypeId": 17,
            "sortOrder": 27,
            "isHidden": false
        },
        "eventType": {
            "typeId": 9,
            "typeKey": "uncategorized",
            "sortOrder": 10
        },
        "comments": null,
        "parentId": null,
        "distance": 1158.3299560546875,
        "duration": 698.3469848632812,
        "elapsedDuration": 698346.9848632812,
        "movingDuration": 698.3469848632812,
        "elevationGain": 10,
        "elevationLoss": 18,
        "averageSpeed": 1.659000039100647,
        "maxSpeed": 1.8849999904632568,
        "startLatitude": 35.396981528028846,
        "startLongitude": 139.5148977264762,
        "hasPolyline": true,
        "ownerId": 6080883,
				・
				・
				・
```

すごい情報量が返ってくるんですが、いったんここで必要なのはactivityIdだけです。
さて、このAPIから取得するとしてもページングがあるし、前回読み込んだアクティビティ以前を読み込みしたくはないので、ここでは以下の方針で考えてみました。

1. ローカルストレージから前回取り込んだアクティビティIDを取得
2. アクティビティ一覧を取得し、1.のアクティビティIDを探す。なければ次のページ読み込む。1.のアクティビティID見つかるまで読み込む
3. アクティビティIDが見つかったら、その次のアクティビティからアクティビティIDを取得し、ダウンロード開始。ダウロード済みのアクティビティIDをローカルストレージに入れておく
4. 2.で読み込んだリストの末端まで行ったら処理終了

うーん。やってることが地味ですね。今回の記事もLGTMもらえなさそうです。
（社内でもおそらくこれを必要としている人はいないので、まったく共感を得られない記事になってきました）

## fetchで取得
こんな感じで書いて、取得できるか確認してみました。

```
function getActivityList(start, limit) {
    var url = "https://connect.garmin.com/modern/proxy/activitylist-service/activities/search/activities?" +
            "limit=" + limit +
            "&start=" + start +
            "&_=" + moment().valueOf(); // unix timestamp (ms)
    var activityIds = [];
  
    return new Promise((resolve, reject) => {
      fetch(url, {
        credentials: 'include',
        mode: 'cors',
      })
        .then(response => response.text())
        .then(text => console.log(text))
        .then(function(){
          resolve(activityIds);
        })
        .catch(ex => reject(ex))
    });
}
```

あれ、失敗ですね。
![Untitled (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221091/c8c630e7-0868-b243-b51f-28fbc596a29c.png)

Cookie飛んでない疑惑です。
![Untitled (2).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221091/224bd710-ed48-5028-6d9e-b47fe34ae56c.png)

もしかして、backgroundからのfetchはセッション共有されてなくてCookie飛ばないの？Cookie自前で取得して一つ一つつけないとダメ？とか一瞬遠い目になりましたが、何のことはないmanifestへのドメイン追加が必要なだけでした。

```manifest.json
    "permissions": [
      "activeTab",
      "declarativeContent", 
      "storage", 
      "downloads", 
      "alarms", 
      "*://connect.garmin.com/"],
```

リスト取得は以下のような感じになりました。ちょっと長い。

```background.js
/**
 * Set alarm for regular download in the background.
 */
chrome.alarms.onAlarm.addListener(async function (alarm) {
  let _isStart = await getLocalStorageVal('isStart');

  if (_isStart.isStart && alarm.name == "get_activity_list") {
    var _activityId = await getLocalStorageVal("last_read_activity_id");
    var _unreadIds = await getLocalStorageVal("unread_activity_ids");
    if("unread_activity_ids" in _unreadIds && _unreadIds.unread_activity_ids != '[]') {
      // If there is an undownloaded activity list in the local storage, 
      // do not load the DL target ID additionally
      console.log('There is a list of unloaded activities');
      return;
    }

    var nextPage = true;
    var start = 0;
    var limit = 20;
    while(nextPage) {
      var _ids = await getLocalStorageVal("unread_activity_ids");
      
      var ids = [];
      if("unread_activity_ids" in _ids) {
        ids = JSON.parse(_ids.unread_activity_ids);
      }

      var olderIds = await getActivityList(start, limit);
      
      if(olderIds.length == 0) {
        // If can't get the activity list, exit
        nextPage = false;
        break;
      }

      if(!_activityId.last_read_activity_id) {
        // No activity loaded in the past (first time)
        start = start + limit;
        ids = ids.concat(olderIds);
      } else {
        if(olderIds.indexOf(_activityId.last_read_activity_id) >= 0) {
          // The activity ID loaded last time is included in the acquired list
          nextPage = false;
          ids = ids.concat(olderIds.slice(0, olderIds.indexOf(_activityId.last_read_activity_id)));
        } else {
          // No ID previously imported into the acquired list (inspection on the next page)
          start = start + limit;
          ids = ids.concat(olderIds);
        }
      }

      chrome.storage.sync.set({unread_activity_ids: JSON.stringify(ids)}, function() {
        // Save next date to local storage
      });
      
    }
  }
});
```

初回のリスト取得では一番過去の一覧まで取得して、その時点での全アクティビティIDをローカルストレージに保存してます。ちょっと乱暴（すごいたくさんのアクティビティを持つ人がこれ動かすと危険かも）なやり方なので上限値は設定した方がいいかなとは思ってますので、githubあたりに上げるときにはそれ入れます。
あと、リスト取得は毎分とかで動かれると大変うざいので、別にalarmをセットしています。最終的には1日に一回動かすのでも十分でしょう。

```background.js
/**
 * Set alarm when installing extension
 */
chrome.runtime.onInstalled.addListener(function (details) {
  console.log(details.reason);
  chrome.alarms.create("dl_fire", { "periodInMinutes": 1 });
  chrome.alarms.create("get_activity_list", { "periodInMinutes": 5 });
});
```

## アクティビティのダウンロード処理
2日目に書いたコードの焼き直しです。やや冗長な気はしてるので自分の心の中の要リファクタ一覧の末尾に追加しておきました。

```background.js
/**
 * Set alarm for activity download in the background.
 */
chrome.alarms.onAlarm.addListener(async function (alarm) {
    
  let _isStart = await getLocalStorageVal('isStart');

  if (_isStart.isStart && alarm.name == "dl_fire") {
    
    var _unreadIds = await getLocalStorageVal("unread_activity_ids");
    if(!"unread_activity_ids" in _unreadIds || 
      ("unread_activity_ids" in _unreadIds && _unreadIds.unread_activity_ids == "[]")) {
      return true;
    }

    ids = JSON.parse(_unreadIds.unread_activity_ids);

    id = ids.pop();

    if(id) {
      var url = "https://connect.garmin.com/modern/proxy/download-service/files/activity/" + id;
      var _dir = await getLocalStorageVal('directory');
      chrome.downloads.download({
        url: url, 
        filename: _dir.directory + id + '.zip'
      });
      chrome.storage.sync.set({unread_activity_ids: JSON.stringify(ids)}, function() {
        // Save updated id_list to local storage
      });
    }

    chrome.storage.sync.set({last_read_activity_id: id}, function() {
      // Save updated id_list to local storage
    });
  }
});
```

# 動かしてみる
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/221091/4cedce40-a799-67b9-b6ab-416a0c8aae51.png)

動き的にも前回とあまり違いはないです。ただひたすら一定間隔でファイルがダウンロードされるという・・・。
こんな地味な拡張機能ですが、たぶん世の中的には必要としている人はいそうな気がするので公開してみようかなと思ってます。