[GoogleAppsScript] Qiita記事のアクセス数をLINEに通知する


# はじめに
LINE Notifyを使用してQiita記事のアクセス数（QiitaView）を通知するBOTを作成しました。
![1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50563/6b9b5528-cdcc-f089-db9a-43d0e25bc2dd.png)

GoogleAppsScript（GAS）にて1日1回通知プログラムを実行します。そのプログラムでは、[Qiita API](https://qiita.com/api/v2/docs)からその日のアクセス数を取得して、[LINE Notify](https://notify-bot.line.me/ja/)に通知します。

```plantuml
@startuml
GoogleAppsScript -> GoogleAppsScript: 1日1回起動
activate GoogleAppsScript
  GoogleAppsScript -> "Qiita API": GET /authenticated_user/items
  activate "Qiita API"
    GoogleAppsScript <-- "Qiita API": ユーザー記事一覧
  deactivate "Qiita API"
  GoogleAppsScript -> "Qiita API": GET /items
  activate "Qiita API"
    GoogleAppsScript <-- "Qiita API": ユーザー記事
  deactivate "Qiita API"
  GoogleAppsScript -> GoogleAppsScript: 当日のアクセス数を算出して、昇順に並び替える
  GoogleAppsScript -> GoogleAppsScript: 累計アクセス数の棒グラフ画像を作成する
  GoogleAppsScript -> "LINE Notify": ...POST /notify
deactivate GoogleAppsScript
activate "LINE Notify"
  "LINE Notify" -> "LINE Notify": 対象のユーザーに通知
deactivate "LINE Notify"
@enduml
```

機能としては下記の通りです。

- 当日分の記事アクセス数をランキング形式で通知する。
- 累計アクセス数を棒グラフ画像を通知する。

# LINE Notify
- [詳しくはこちら](https://qiita.com/yun_bow/items/cc1f3c45c2d86e39c6f7)

# GoogleAppsScript
1. GoogleAppsScriptを開いて、以下のスクリプトを実装します。`LINE_NOTIFY_TOKEN`にLINE Notifyで発行したトークンを指定する。
2. `SSID_QIITA_VIEW`にスプレッドシートのIDと`SSN_QIITA_VIEW`にシート名を指定する。
3. `QIITA_TOKEN`にQiita APIで発行したトークンを指定する。

```JavaScript:main.gs
/**
 * QiitaViewBOT
 */
const LINE_NOTIFY_TOKEN = '*****'; // LINE NOTIFYのアクセストークン
const SSID_QIITA_VIEW = '*****'; // QiitaのスプレッドシートのID
const SSN_QIITA_VIEW = 'QiitaVIEW'; // Qiitaのスプレッドシートのシート名
const QIITA_TOKEN = '*****'; // Qiitaトークン
const TITLE_MAX_LENGTH = 40; // 記事タイトル最大長
const WEEKDAY = ["日", "月", "火", "水", "木", "金", "土"];

let spreadsheet = SpreadsheetApp.openById(SSID_QIITA_VIEW);
let sheet = spreadsheet.getSheetByName(SSN_QIITA_VIEW);

/**
 * メイン処理
 */
function main() {
    try {
        addViewCount();

        let itemList = getItemList();

        for (let i in itemList) {
            let item = itemList[i];
            item.increase = 0;
            if (0 < item.list.length) {
                let list = [].concat(item.list);
                item.increase = list.pop().viewCount - list.pop().viewCount;
            }
        }

        itemList.sort((a, b) => {
            return (a.increase > b.increase) ? -1 : 1;
        });

        let nowDt = new Date();
        let dt = Utilities.formatDate(nowDt, 'Asia/Tokyo', `MM/dd(${WEEKDAY[nowDt.getDay()]})`);
        let message = `\n今日のQiitaViewだよ!!\n\n--- ${dt} ----\n\n`;
        for (let i = 0; i < 5; i++) {
            let item = itemList[i];
            let list = [].concat(item.list);
            let data = getQiitaItem(item.id);
            message += `${parseInt(i) + 1}: +${item.increase} (${list.pop().viewCount} views)\n`
            message += `${omit(data.title)}\n\n`;
        }

        let range = sheet.getRange(sheet.getLastRow(), 2, sheet.getLastRow(), sheet.getLastColumn());
        let chart = sheet.newChart().addRange(range).setChartType(Charts.ChartType.COLUMN).setOption('title', 'QiitaViews').build();
        let imageBlob = chart.getBlob().getAs('image/png').setName(`chart.png`);

        console.log(message);
        sendLineNotify(message, imageBlob);

    } catch (e) {
        console.error(e.stack);
    }
}

/**
 * Qiita View数を取得して、スプレッドシートに保存する
 */
function addViewCount() {
    let idList;
    let lastRow = sheet.getLastRow();
    if (0 < lastRow) {
        idList = sheet.getRange(1, 2, 1, sheet.getLastColumn()).getValues()[0];
    }

    let timestamp = Utilities.formatDate(new Date(), 'Asia/Tokyo', 'yyyy-MM-dd');
    let viewCountList = [timestamp];
    let itemList = getQiitaItemList();
    let addCount = 0;
    let lastColumn = 0;
    if (0 < lastRow) {
        lastColumn = sheet.getLastColumn() - 1;
    }

    for (let i in itemList) {
        let item = itemList[i];
        item = getQiitaItem(item.id);

        let index = 0;
        let isExist = false;

        if (0 < lastRow) {
            for (let j in idList) {
                let id = idList[j];
                if (id == item.id) {
                    isExist = true;
                    index = parseInt(j);
                }
            }
        }
        if (!isExist) {
            index = lastColumn + addCount;
            addCount++;
        }
        sheet.getRange(1, (index + 2)).setValue(item.id);
        viewCountList[index + 1] = item.page_views_count;
    }
    sheet.appendRow(viewCountList);
}

/**
 * スプレッドシートのデータを取得する
 */
function getItemList() {

    let itemList = [];
    let lastRow = sheet.getLastRow();
    let lastColumn = sheet.getLastColumn();
    let rowList = sheet.getRange(1, 1, lastRow, lastColumn).getValues();

    for (let i = 1; i < lastColumn; i++) {
        let id = rowList[0][i];
        let viewList = [];

        for (let j = 1; j < lastRow; j++) {
            viewList.push({
                createDt: rowList[j][0],
                viewCount: rowList[j][i]
            });
        }
        itemList.push({
            id: id,
            list: viewList
        });
    }
    return itemList;
}

/**
 * 文字列を省略する
 * @param {String} str 
 */
function omit(str) {
    if (TITLE_MAX_LENGTH < str.length) {
        return str.slice(0, TITLE_MAX_LENGTH) + '...';
    } else {
        return str;
    }
}

/**
 * ユーザー記事一覧を取得する
 * @return JSON
 */
function getQiitaItemList() {
    let url = `https://qiita.com/api/v2/authenticated_user/items?page=1&per_page=100`;
    let options = {
        'method': 'get',
        'headers': {
            'Authorization': `Bearer ${QIITA_TOKEN}`
        },
    };
    let response = UrlFetchApp.fetch(url, options);
    return JSON.parse(response.getContentText('UTF-8'));
}

/**
 * ユーザー記事を取得する
 * @param {String} id
 * @return JSON
 */
function getQiitaItem(id) {
    let url = `https://qiita.com/api/v2/items/${id}`;
    let options = {
        'method': 'get',
        'headers': {
            'Authorization': `Bearer ${QIITA_TOKEN}`
        },
    };
    let response = UrlFetchApp.fetch(url, options);
    return JSON.parse(response.getContentText('UTF-8'));
}

/**
 * LINEにメッセージを送信する
 * @param {String} message メッセージ
 * @param {Object} blob 画像ファイル
 */
function sendLineNotify(message, blob) {
    let url = 'https://notify-api.line.me/api/notify';
    let options = {
        'method': 'post',
        'headers': {
            'Authorization': `Bearer ${LINE_NOTIFY_TOKEN}`
        },
        'payload': {
            'message': message,
            'imageFile': blob
        }
    };
    let response = UrlFetchApp.fetch(url, options);
    return JSON.parse(response.getContentText('UTF-8'));
}
```
- `addViewCount`関数では、記事一覧を取得してから、記事詳細を取得しています。スプレットシートに累計アクセス数を保存します。
- `main`関数では、スプレットシートから取得した当日と前日の累計アクセス数から、当日分のアクセス数を算出します。昇順に並び替えて上位5件を纏めます。累計アクセス数の棒グラフ画像を作成します。

# 参考リンク
- [Qiita API v2 の概要(非公式)](https://qiita.com/tag1216/items/b0b90e30c7e581aa2b00)
- [GASでQiita APIを使ってView・いいね・ストック数の一覧を取得する](https://qiita.com/tksnino/items/7e6a587ff6e1817490ad)

# さいごに
ソースコードをGitHubに公開しています。

- [ソースファイルはこちら](https://github.com/yunbow/GoogleAppsScript-LINE-QiitaView)

以上です。