QiitaAdventCalendarのRSSを集めてSlackに投稿する

この投稿は 「<a href="http://qiita.com/advent-calendar/2016/gas" target="_blank">Google Apps Script Advent Calendar 2016</a>」12日目の投稿です。

今日は
　<b>GoogleAppsScript</b>でQiitaのRSSフィードを複数読み込んで、<b>Slack</b>に投稿する
話を書きます。

## きっかけ

12月はadventカレンダーで色々なエントリを沢山読めて楽しいですよね。
でも今年は、去年に比べてカレンダーの数が多くなった気がします。<A href="http://qiita.com/advent-calendar" target="_blank">Advent Calendar</A>のトップを見ると

| 年度 | カレンダー数 |
|:--|--:|
|2016   | 509  |
|2015   | 377  |
|2014   | 214  |
|2013   | 82  |
|2012   | 29  |

年々、カレンダーの数が増えているんですね。

読みたいものが増えるとその分だけ、労力が必要になるので、意識の低い自分は「追いかける」のが辛くなってきました。チェックするのを忘れてたり、何が読みたかったのか自体を忘れてたり。。。

普通に考えれば、RSS購読したり何か工夫すれば良いのですが、

- 興味があるカレンダーをまとめて読めたい
- 読みたいのは昨日の投稿だけ
- まとめた結果を自分用のslackチャンネルに投稿させたい
- 新聞の朝刊のように翌朝に結果をPush通知させて通勤電車で見たい

と思い、GASで前日分の投稿をまとめるスクリプトを作りました。

# コード

処理の流れは

　指定カレンダーのRSSフィードを読み込み前日分の投稿だけを集める
　自分用のslackチャンネルに毎朝9時に投稿する

です。

実際にslack投稿されるメッセージはこんな感じにしました。

![slackimage3.GIF](https://qiita-image-store.s3.amazonaws.com/0/154139/e36d6a77-8b4c-c25f-f5f7-1bfe0c7d046f.gif)

コードはこうなりました。

```　.js
// -------------------------------------------------------------
// Slackに前日分の「Qiita Advent Calendar」エントリをまとめて投稿する
// -------------------------------------------------------------
function postSlackChannel_qiitaAdventCalendar(){

  // slackのINCOMINGURL
  var slackIncomingUrl = "https://hooks.slack.com/services/XXXXXXXXX/XXXXXXXXXX/XXXXXXXXXXXXXXXXXXXXXXXX";

  // slackのチャンネルを指定
  var slackChannelId = "XXXXXXXXX";
  
  // 対象カレンダーの指定
  var calendars = new Array("vscode","csharp","communication","productmanager","hodgepodge","muscle","se","kosodate-engineer","getwild","ouch-hack","slack","docker","git","machine-learning","electron","free_zankoku");

  // 対象の日付（昨日）
  var now = new Date();
  var targetDateD = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1).getDate();

  // QiitaのRSSから対象エントリを取得
  var result = getQiitaAdventCalendarEntries(targetDateD, calendars);

  // エントリがあればSlackに投稿
  if (result.entryCount == 0){
    Logger.log("Really!?");
  }else{
    Logger.log("result.entryCount:" + result.entryCount);
    // slack投稿用の配列に入れる
    var atts = [];
    for (var idx=0; idx<=result.entryData.length-1; idx++) {
      var entry = result.entryData[idx];
      var ptext = "";
      var fback = "";
      if (idx==0){ 
        ptext = "<" + result.calendarTopUrl + "|" + result.calendarTopName + "> Day " + targetDateD;
        fback = result.calendarTopName + " Day " + targetDateD;
      }
      atts.push({title : "<" + entry.href + "|" + entry.title + ">",
                 color : "#64c914",
                 pretext :  ptext,
                 fallback : fback,
                 thumb_url : entry.authorImage,
                 fields : [ { title:"Calendar", value:"<" + entry.calendarUrl + "|" + entry.calendarTitle +">", short:true},
                            { title:"Author", value:entry.author, short:true}]
                })      
    }
    Logger.log("atts.length:" + atts.length);
    if (atts.length > 0){
      // slackにメッセージを投稿する
      postSlackMessages(slackIncomingUrl, slackChannelId, "AdventCalendar", "", ":page_facing_up:", "", atts);
    }
  }
}  

// ---------------------------------------------------------------------------
// Qiita Advent Calendar2016から指定日＆指定カレンダーのエントリを取得する
// ---------------------------------------------------------------------------
var getQiitaAdventCalendarEntries = function(targetDay, targetCalendars){

  var result = {};
  var datas  = [];
  var dataidx = 0;
  
  // QiitaAdventCalendarの情報
  var calendarTopUrl  = "http://qiita.com/advent-calendar/2016/";
  var calendarUrlBase = "http://qiita.com/advent-calendar/2016/{calandar}";
  var calendarName    = "Advent Calendar 2016";

  result['calendarTopUrl']  = calendarTopUrl;
  result['calendarTopName'] = calendarName;
  
  // カレンダーループ
  for(var i=0; i < targetCalendars.length; i++){

    // リクエスト
    var calendarUrl = calendarUrlBase.replace("{calandar}", targetCalendars[i]);
    var xml = UrlFetchApp.fetch(calendarUrl + '/feed').getContentText();

    // パース
    var atom = XmlService.getNamespace('http://www.w3.org/2005/Atom');
    var re   = XmlService.getNamespace('http://purl.org/atompub/rank/1.0');
    var doc  = XmlService.parse(xml);
    var root = doc.getRootElement();
    var calendarTitle   = root.getChild('title', atom).getText().replace(" Advent Calendarの投稿 - Qiita","");
    var calendarEntries = root.getChildren('entry', atom);

    // エントリループ
    for(var idx=0; idx < calendarEntries.length; idx++){
      // adventカレンダー日付を取得
      var rank = calendarEntries[idx].getChild('rank', re).getText();;
      // adventカレンダー日付が対象日ならデータを取得
      if (rank == targetDay){
        dataidx++;
        var published  = calendarEntries[idx].getChild('published', atom).getText().replace("T"," ").replace("+09:00","");
        var author     = calendarEntries[idx].getChild('author'   , atom).getChild('name', atom).getText();
        var href       = calendarEntries[idx].getChild('link'     , atom).getAttribute('href').getValue();
        var etitle     = calendarEntries[idx].getChild('title'    , atom).getText();
        var econtent   = calendarEntries[idx].getChild('content'  , atom);
        // qiita以外の投稿は「content」タグの中にタイトルが入っているケースあり
        if (econtent){ 
          if (econtent.getAttribute('type').getValue() == "text"){
            etitle = econtent.getText();
          }
        }
        // プロフィール画像を取得
        var authorImage = getSlackProfileImage(author);
        // データを配列に退避
        datas.push({author:author, authorImage:authorImage, title:etitle, href:href, 
                    calendarDate:rank, calendarUrl:calendarUrl, calendarTitle:calendarTitle});
      }
    }
  }
  result['entryData']  = datas;
  result['entryCount'] = dataidx;
  return result;
}

// -------------------------------------------------------------
// Qiita画像を取得する
// -------------------------------------------------------------
var getSlackProfileImage = function(profileId){
  var imageurl = "";
  try {
    var html = UrlFetchApp.fetch("http://qiita.com/" + profileId,{muteHttpExceptions:true}).getContentText();
    var matchs = html.match(/<div class="newUserPageProfile_avatar"><img alt="" src="(.*)" \/><\/div><div class="newUserPageProfile_header">/);
    if (matchs){ 
      imageurl = matchs[1]; 
    }
  } catch (e) {
  }
  return imageurl;
}

// -------------------------------------------------------------
// slackにメッセージを投稿する
// -------------------------------------------------------------
var postSlackMessages = function(incurl, channel, username, text, icon_emoji, icon_url, attachments){
  var payload = {
      "channel"    : channel,
      "username"   : username,
      "text"       : text,
      "icon_emoji" : icon_emoji,
      "icon_url"   : icon_url,
      "attachments": attachments,
      "link_names" : 0};
  var options = { "method":"post", "contentType":"application/json", "payload":JSON.stringify(payload) };
  var result = UrlFetchApp.fetch(incurl, options);
}
```


コードの説明

- Slackへの投稿はIncomingWebHookを使用
- 見たいカレンダーのリストを内部で指定
- 画像が欲しかったので、RSSフィードには載っていないプロフィール画像を別途取得
- slackの<A href="https://api.slack.com/docs/message-attachments">attachments</A>パラメータで、配列形式で投稿。
- カレンダーの指定ミスとか、想定外の事態に弱いと思います。

スケジュール登録はgoogleappsscriptメニューの「リソース-> 現在のプロジェクトのトリガ」から「日タイマー／午前8-9時」にセットしました。

# 終わり

GASは簡単に作れるので「情報収集ツール」として使うと便利です。

今回のスクリプト以外でも、普段の生活でGAS＆SLACKを使っているので、
機会があればどこかで紹介したいと思います。

最後までご覧になっていただき、ありがとうございました。