LCL Engineers' Blog

夜行バス比較なび(高速バス比較)・格安移動・バスとりっぷを運営する LCLの開発者ブログ

git hooksをovercommitで管理して作業効率の底上げを狙う

モバイルアプリエンジニアの山下です。

チームで開発を進める上でちょっとした"決まりごと"が存在すると思います。
例えば、LCLの開発チームには以下の"決まりごと"が存在します。

  • コミットメッセージの先頭にはYouTrackのIssue番号を付ける
  • RuboCopで設定したコーディングスタイルになるべく従う
  • masterへ直接PUSHはしない

これらを全員が心がけることで運用効率や品質を保つようにしていまが、とはいえコミットする度にIssue番号を入力したり、RuboCopのコマンドを打ったりするのは非常に面倒です。

そこで今回は「git hooks」を利用してコミットやプッシュのタイミングで自動的に実行されるようにしました。

git hooksとは

コミットやプッシュなどのgit操作をトリガーにしてスクリプトを実行する機能です。
.gitディレクトリ内にhooksという名前のディレクトリで管理されています。

例えば、コミット時にコミットメッセージに対して何かしらの処理を行うということができます。
これを利用することで手動で行っていた些細なことは自動化することができます。

しかし、git hooksは残念なことにgitで管理できません。(※ 別途、生成スクリプトを用意するなどの策はあります)
これではメンバー全員に設定してもらう必要がありますが、現実はそう浸透しません。

それを解決するためにovercommitを導入しました。

overcommitとは

https://github.com/brigade/overcommit

git hooksの管理・拡張をサポートするGemです。
.overcommit.ymlで実行するコマンドやスクリプトを管理します。
既存のgit hooksを使いまわすことも可能です。

また、あらかじめいくつかの設定が用意されています。

導入

Gitの操作と密に関係するため直接インストールします。

$ gem install overcommit

※ 既にgit hooksを利用している場合は、ファイルをバックアップしておきましょう。
overcommitをインストールする際に初期化される可能性があります。

overcommitをインストール

$ overcommit --install

それでは、さっそくコミットしてみましょう。恐らく以下の警告で失敗すると思います。

Runovercommit --signif you trust the hooks in this repository.

overcommitは、fetchしたymlやスクリプトが不正に書き換えられた場合に備えて、これらの差分が存在した際には署名コマンドを打つ必要があります。少し面倒ですが、安全面との引き換えには仕方がないです。

$ overcommit --sign

上記のコマンドを実行後に再度コミットをしてみましょう。
コミットメッセージのチェックに引っかからなければ、無事にコミットできると思います。

設定

前述の通り、デフォルト設定が存在し有効になっています。以下のコマンドで確認できます。

$ overcommit --list

以下のアスタリスク(*)が付いている項目がデフォルトで設定されている項目です。
https://github.com/brigade/overcommit#built-in-hooks

私の環境ではGitのユーザ名の制約を行う「AuthorName」で引っかかってしまい、コミットができませんでした。
ユーザ名は変えたくないので設定を無効化したいと思います。

.overcommit.ymlに以下の設定を追加します。

PreCommit:
  AuthorName:
    enabled: false

変更後は設定を反映させます。

$ overcommit --sign

これで無事にコミットできるようになりました。

RuboCopの実行

RuboCopには自動修正機能が存在します。これを実行するにはコマンドを打つ必要がありますが、今回はコミット時に毎回自動で実行するようにします。

実行したいコマンドは以下です。差分だけを対象にしています。

$ rubocop --auto-correct (git diff master --name-only)

overcommitはRuboCopを含む、様々なLintツールに対応しています。(インストールは別途必要です)
今回は対象ファイルの引数が必要なため、以下のようにcommandの項目を追加します。

PreCommit:
  RuboCop:
    enabled: true
    command: ['rubocop', '-auto-correct', '$(git diff master --name-only)']

変更後は設定を反映させます。

$ overcommit --sign

これでコミットする際に差分に対して自動でRuboCopの自動修正が実行されます。

コミットメッセージにYouTrackのIssue番号を追加

LCLではYouTrackでIssueを管理しており、ブランチ名やコミットメッセージには対応するYouTrack Issue番号を先頭に記述するようにしています。
YouTrackのIssue番号は以下のような構成になっています。

  • project-123
  • project_sub-123

前述の通り、毎回記述するのは手間がかかるため、ブランチ名を先頭に記述するスクリプトを作成しgit hooksで管理していました。
今回は、その hook を使いまわしたいと思います。

プロジェクトルート直下のbinディレクトリでhooksフォルダを作成し、更にcommit-msgファイルを作成します。
これからは独自のhookはhooksフォルダ内に追加していきます。

独自のhookを実行させるためには、実行権限を付与する必要があります。

$ chmod +x ./bin/hooks/commit-msg

commit-msgに今回の処理内容をを記載します。

#!/usr/bin/env ruby

# コミットメッセージを取得
msg_file = ARGV[0]
commit_msg = File.read(msg_file, encoding: Encoding::UTF_8)

# 最後の`/`以降の文字列を取得
youtrack_issue_number = `git branch | grep "*"`.sub(/^\*\s/, '').match(/([^\/]+)$/)[0].chomp

# YouTrack Issue番号を追加
commit_msg = "#{youtrack_issue_number} #{commit_msg}" unless commit_msg.include?(youtrack_issue_number)

# コミットメッセージを上書き
File.write(msg_file, commit_msg)

そして、overcommit.ymlに設定を追加します。

CommitMsg:
  CustomScript:
    enabled: true
    required_executable: './bin/hooks/commit-msg'

設定を反映させます。 今回はカスタムスクリプトなのでファイルを指定して署名する必要があります。

$ overcommit --sign commit-msg

以上で、コミットするとコミットメッセージの先頭にブランチ名が追加されると思います。
ブランチ名をIssue番号にすれば要件を満たすことができます。

masterへ直接PUSHできないようにする

先ほど作成したhooksディレクトリにpre-pushファイルを追加し、実行権限を付与します。

$ chmod +x ./bin/hooks/pre-push

ここではPUSH先がmasterを向いている場合、エラーメッセージを流すようにします。

#!/bin/sh

while read local_ref local_sha1 remote_ref remote_sha1
do
  if [[ "${remote_ref##refs/heads/}" = "master" ]]; then
    echo "masterへPUSHしないでください。"
    exit 1
  fi
done

overcommit.ymlに設定を追加します。

PrePush:
  CustomScript:
    enabled: true
    required_executable: './bin/hooks/pre-push'

設定を反映させます。

$  overcommit --sign commit-msg

masterへPUSHするとエラーメッセージが表示されれば成功です。

終わりに

Gitの操作は頻繁に行うので、同じことはなるべく自動化すると塵も積もって大きな時間短縮になると思います。 特に、自分だけで留めるのではなくチームを巻き込んで効率化することで効果は更に大きくなります。

今後は、画像圧縮や特定のファイルのコミット制限などの機能も追加していきたいと考えています。
ルーチンがある方は導入してみてはいかがでしょうか。

DFP Key-Value ターゲティングを利用して、広告配信を制御する

Webエンジニアの森脇です。 今回は、DFPでの広告配信について紹介します。

DFPとは

DoubleClick for Publishers(DFP)は、Googleが提供している広告配信プラットフォームです。

多機能なので詳しくは、以下のドキュメントをご覧ください。

https://support.google.com/dfp_premium/answer/6022000?hl=ja&ref_topic=7519088

Key-Valueターゲティングとは

DFPでは地域・デバイス等の条件を、広告配信の条件として利用できますが、 サイト独自の条件(年齢、性別、コンテンツなど)は、DFPでは判断できません。 Key-Value ターゲティングを使用すると、DFPで判別できない独自の条件を定義できます。

たとえば、gender = male と定義すると、男性をターゲットとして配信制御ができます。この他にもサイト内での行動に合わせて、任意にKey/Valueを設定することも可能です。 なお、ユーザー個人を特定できるデータ(名前、住所、ユーザー ID など)をDFPへ渡すことは禁止されています。

本記事では、DFP管理画面の設定については、本記事では言及しませんので、以下のドキュメント等をご確認ください。

https://support.google.com/dfp_premium/answer/177381

Key/Valueの設定方法

JavaScriptで、Key/Valueを設定します。以下の例では、envというkeyに対して、productionを設定しています。開発環境の場合には、developmentを設定すれば、環境に応じた広告配信制御が可能です。

googletag.pubads().setTargeting('env', ["production"])

複数のkeyを設定することも可能です。

googletag.pubads().setTargeting('key1', ["value1"])
googletag.pubads().setTargeting('key2', ["value2"])

1 つのキーに複数の値を設定するには、カンマ区切りで設定します。

googletag.pubads().setTargeting("key",["value1","value2");

ページを更新せずに広告を再読み込みする場合

Ajaxを利用した等のページでは、ページを更新せずに広告を再読込したいときがあります。 その場合は、key/valueをセットして、refresh関数を呼び出すことで再読込することで、DFPに値を送信できます。

googletag.cmd.push(function() {
  googletag.pubads().setTargeting('key1', ["value1"])
  googletag.pubads().refresh();   
});

まとめ

エンジニアが広告配信にどこまで関わるかは、各社の状況によって異なると思いますが、 どのような仕組みで広告配信が行われているか知っておくと、色々な課題の解決に役立ちます。

広告周りは用語が難解で、普段から携わっていないと取っ付きにくいですが、公式ドキュメントを読めば理解できるため、今後の定期的にキャッチアップしていきたいと思います。

GASでラクをする技術(Gmail・Google Calendar・RSS編)

モバイルアプリエンジニアの山下です。
LCLでは業務や情報収集の中で定期的な作業を行う際にGoogle Apps Script(以下、GAS)を利用した自動化をしています。

GASとは、クラウド上でスクリプトを実行できるサービスです。スプレッドシートをはじめ、Googleが提供している色々なサービスと連携することができます。
又、外部のサービスでも提供されているAPIを利用して操作することができるため幅広い用途で使えます。

そして、今回は以下の3つの活用方法を紹介したいと思います。

  • Gmailの本文から値を取得してスプレッドシートに書き込む
  • Google CalendarのスケジュールをChatWorkに投稿する
  • 毎朝、前日分のRSSをChatWorkに投稿する

Gmailの本文から値を取得してスプレッドシートに書き込む

この操作は、Fabricのデイリーサマリーを集計するスクリプトで扱っています。
Fabricの管理画面では、Crashlyticsなどの情報が過去90日間しか遡れないためスプレッドシートへ書き溜めるようにしています。

スクリプトの処理の流れは以下です。事前にGmailで対象のメールにラベルを付ける必要があります。

  1. Gmailから未読のFabricのデイリーサマリーのメールを取得
  2. メールから日付と本文中にある特定の値を取得
  3. 日付と値をスプレッドシートの最後に書き込み
  4. 取得したメールを既読に変更

それでは実装方法を紹介していきます。

実装

1. Gmailから未読のFabricのデイリーサマリーのメールを取得

Gmailを操作するにはGmailAppクラスを使います。
スレッドを取得するメソッドはいくつか存在しますが、今回は未読を条件に含めたいためsearchメソッドを使います。

var start = 0;                              // 参照を始めるポジション
var max = 20;                               // 取得するメールの最大件数
var threads = GmailApp.search('label:Fabric is:unread', start, max); 

取得したスレッドは配列になっているため、ループを回して取得します。
スレッドからはgetMessagesメソッドでメッセージ(メール)を取得できます。

for( var n in threads ) {
  var thread = threads[n];
  var messages = thread.getMessages();

  // メッセージを取得    
  for (m in messages) {
      var message = messages[m];
  }
}

2. メールから日付と本文中にある特定の値を取得

メッセージも配列で渡されるので同じようにループを回して取得します。
メッセージに対してはgetPlainBodyメソッドでHTML要素を除いたbodyの文字列を取得できるため、今回はこれを利用して値を取得します。

Class GmailMessage  |  Apps Script  |  Google Developers

var message = messages[m];
var date = Utilities.formatDate(message.getDate(), 'Asia/Tokyo', 'yyyy/MM/dd');
var body = message.getPlainBody();

取得したbodyの文字列から正規表現で特定の値を抜き出します。
抜き出したい文字列は以下のようになっています。

    Crash-Free Users:
      Yesterday: 99.8%
      Delta: ▲0.1%

上の形に対応した処理をメソッド化して、以下のように取得します。

function myFunction() {
...
  for (m in messages) {
      ...
      var body = message.getPlainBody();
      // 正規表現で値を取得
      var crashFreeUsers = fetchData(body, 'Crash-Free Users:');
  }  
}

function fetchData(string, prefix) {
  var reg = new RegExp(prefix + '\\W+Yesterday:\\W+.*?\\d.+');
  var data = string.match(reg)[0]
    .replace(prefix, '')
    .replace('\\W+', '')
    .replace('Yesterday:', '')
    .replace('\\d.+', '')
    .replace(/[ \n\r]/g, '');
  return data;
}

3. 日付と値をスプレッドシートの最後に書き込み

ここまでで日付と値は取得できたので次にスプレッドシートへ書き込みます。
まず、取り扱うスプレッドシートを取得します。スプレッドシートの取得方法には2パターンあります。

スプレッドシートがGASのスクリプトに紐付いているパターン

var spreadSheet = SpreadsheetApp.getActiveSpreadsheet();
var sheet = spreadSheet.getSheetByName('sheet1'); 

スプレッドシートがGASのスクリプトに紐付いていないパターン

var sheetId = "<スプレッドシートのID>";
var spreadSheet = SpreadsheetApp.openById(sheetId);
var sheet = spreadSheet.getSheetByName('sheet1'); 

本記事では触れませんが別途ツールを使うことでGASもバージョン管理ができます。しかし、そのツールを使う場合は、対象のスクリプトが他サービスと紐付いていない必要があるため、後々管理をする予定であれば紐付けない実装することをおすすめします。

話が逸れましたが、次にスプレッドシートの最後の行に書き込みを行います。
これはappendRowメソッドを使うことで最後の行の計算を行わず、純粋に追記することができます。

...
for (m in messages) {
    ...
    sheet.appendRow([
      date,
      crashFreeUsers,
    ])
}

4. 取得したメールを既読に変更

最後にメッセージに対してmarkReadメソッドを呼んで、メールを既読にします。

...
for (m in messages) {
    ...
      message.markRead();
}

全体コード

全体コードを載せておきます。必要な箇所を書き換えればそのまま使用できます。

var SHEET_ID = "<スプレッドシートID>";

function myFunction() {
  var sheet = getSheet('<シート名>');
  
  var threads = getThreads('label:Fabric is:unread');  // "Fabric"ラベルが付与された未読メール取得
  var length = threads.length;  // スレッド数を取得
  
  // 昇順に登録するために反転
  threads.reverse();
  
  for( var n in threads ) {
    var thread = threads[n];
    var messages = thread.getMessages();

    // メッセージを取得    
    for (m in messages) {
      var tmpArr = new Array();

      var message = messages[m];
      var date = Utilities.formatDate(message.getDate(), 'Asia/Tokyo', 'yyyy/MM/dd');
      var body = message.getPlainBody();

      var dailyActiveUsers = fetchData(body, 'Daily Active Users:');
      var dailyNewUsers = fetchData(body, 'Daily New Users:');
      var monthlyActiveUsers = fetchData(body, 'Monthly Active Users:');
      var crashFreeUsers = fetchData(body, 'Crash-Free Users:');
      var sessions = fetchData(body, 'Sessions:');
      var timeInAppPerUser = fetchData(body, 'Time in App per User:');
      
      /*
      * スプレッドシートに挿入するデータ列数
      * [
      *   Date,
      *   Daily Active (k),
      *   Daily New Users,
      *   Monthly Active Users, 
      *   Crash-free Users (%),
      *   Total Sessions (k),
      *   Time in App per User (m)
      * ]
      */      
      sheet.appendRow([
        date,
        dailyActiveUsers,
        dailyNewUsers,
        monthlyActiveUsers,
        crashFreeUsers,
        sessions,
        timeInAppPerUser
      ])

      // メールを既読に変更
      message.markRead();
    }
  }
}

// 正規表現で値を取得
function fetchData(string, prefix) {
  var reg = new RegExp(prefix + '\\W+Yesterday:\\W+.*?\\d.+');
  var data = string.match(reg)[0]
    .replace(prefix, '')
    .replace('\\W+', '')
    .replace('Yesterday:', '')
    .replace('\\d.+', '')
    .replace(/[ \n\r]/g, '');
  return data;  
}

// メールのスレッドを取得
function getThreads(filter) {
  var start = 0;                              // 参照を始めるポジション
  var max = 20;                               // 取得するメールの最大件数
  return GmailApp.search(filter, start, max); // 今回は未読を条件に含めたいためsearchメソッドを使う
}

// スプレッドシートを取得
function getSheet(sheetName) {   
  var spreadSheet = SpreadsheetApp.openById(SHEET_ID);
  return spreadSheet.getSheetByName(sheetName); 
}

最後にトリガーをセットすることで、毎日自動でスプレッドシートが更新されます。

Google CalendarのスケジュールをChatWorkに投稿する

エンジニアチームでは、リモート勤務や半フレックスタイム制で始業時間を柔軟に調整できる環境となっています。そのため、対面での朝会は行わず、当日のタスクやスケジュールは始業後にChatWorkの専用のチャンネルへ投稿するようにしています。

投稿するためには毎朝当日のタスクをAM/PMに分けて書き出す必要があるのですが、私はGoogle Calendarに数日後のタスクを予め書き込んでいるため、それを元に自動で投稿するようにしています。

スクリプトの処理の流れは以下です。

  1. Google Calendarから当日のスケジュールを取得
  2. 内容を確認(MTGや業務時間外スケジュールを除く)
  3. ChatWork用に投稿内容を加工
  4. ChatWorkに投稿

事前準備

ChatWorkを利用する方はAPIトークンが必要です。

取得方法

APIトークン発行ページを開き、パスワードを入力後に表示されるAPI Tokenをコピーします。

実装

1. Google Calendarから当日のスケジュールを取得

Google Calendarを操作するにはCalendarAppクラスを使います。
getEventsForDayメソッドで指定日のイベントを取得できます。

  var today = new Date();
  var calendar = CalendarApp.getCalendarById('<カレンダーID>'); 
  var events = calendar.getEventsForDay(today); 

2. 内容を確認(MTGや業務時間外スケジュールを除く)

MTGや在宅勤務などのイベントは共有しないため除外します。

上で取得したイベントは配列になっているため、ループを回して個別に取得します。
getTitleメソッドでイベントのタイトルを取得し、除外キーワードが含まれているか確認します。

Class CalendarEvent  |  Apps Script  |  Google Developers

function myFunction() {
  ...
  // イベントの数だけ繰り返し
  for(var i = 0; i < events.length; i++) {
    var title = events[i].getTitle();
    
    // 除外キーワードを含む場合は処理を飛ばす
    if(hasIgnoreKeyword(title)) {
      continue;
    }
    
    // do something
  }
}

// 除外キーワード判定
function hasIgnoreKeyword(title) {
  var ignore_keywords = ["MTG", "モブプロ", "在宅勤務"];
  var i = ignore_keywords.length;
  while(i--) {
    if (title.indexOf(ignore_keywords[i]) !== -1) {
      return true;
    }
  }
  return false;
}

3. ChatWork用に投稿内容を加工

ChatWorkへは、以下の形式になるように投稿します。

f:id:lcl-engineer:20180314120511p:plain

イベントはもちろん時系列になっているので、文字列を追加するだけです。

var amflg = true;
var pmflg = true;

// 当日のイベントを取得
var events = getTodayEvent(); 

// チャットワークに送る文字列
var body = "[info][title]本日やること[/title]"  

// イベントの数だけ繰り返し
for(var i = 0; i < events.length; i++) {
  var title = events[i].getTitle();
  var desc = events[i].getDescription();
  var startTime = events[i].getStartTime();
  var endTime = events[i].getEndTime();

  // 除外キーワードを含む
  if(hasIgnoreKeyword(title) {
    continue;
  }

  // AM/PMの表記を追加
  if(startTime.getHours() <= 12 && amflg) {
    body += "-AM-\n";
    amflg = false;
  } else if (startTime.getHours() > 12 && pmflg) {
    body += "\n-PM-\n";
    pmflg = false;
  }
  // 項目を追加
  body += '■ ' + title + '\n';
  // 詳細を追加
  if(desc != '') {
    body += '・ ' + desc + '\n';
  }
}

body += '[/info]';

4. ChatWorkに投稿

最後にChatWorkへ投稿します。事前準備したAPIトークンと投稿先のチャンネルIDをセットします。

var payload = {
  'body': body
};

var options = {
  'method': 'post',
  'headers': { 'X-ChatWorkToken': <APIトークン> },
  'payload': payload
};

UrlFetchApp.fetch('https://api.chatwork.com/v2/rooms/' + <ルームID> + '/messages', options);

全体のコード

上記の処理に加え、休日と時間の判定を加えた全体コードを貼ります。

var CALENDAR_ID = '<Google Calendar ID>'
var CHATWORK_TOKEN = '<APIトークン>';
var CHATWORK_ROOM_ID = <ルームID>;
var IGNORE_KEYWORDS = ["MTG", "モブプロ", "在宅勤務"];

function myFunction() {
  if(isHoliday()) {
    return;
  }
  
  var amflg = true;
  var pmflg = true;
  
  // 当日のイベントを取得
  var events = getTodayEvent(); 
  
  // チャットワークに送る文字列
  var body = "[info][title]本日やること[/title]"  
  
  // イベントの数だけ繰り返し
  for(var i = 0; i < events.length; i++) {
    var title = events[i].getTitle();
    var desc = events[i].getDescription();
    var startTime = events[i].getStartTime();
    var endTime = events[i].getEndTime();
    
    // 除外キーワードを含む or 19時以降の予定は飛ばす
    if(hasIgnoreKeyword(title) || startTime.getHours() > 19) {
      continue;
    }
    
    // AM/PMの表記を追加
    if(startTime.getHours() <= 12 && amflg) {
      body += "-AM-\n";
      amflg = false;
    } else if (startTime.getHours() > 12 && pmflg) {
      body += "\n-PM-\n";
      pmflg = false;
    }
    
    // 項目を追加
    body += '■ ' + title + '\n';
    
    // 詳細を追加
    if(desc != '') {
      body += '・ ' + desc + '\n';
    }
  }
  
  body += '[/info]';
  
  postChatWork(body);
}

// ChatWorkへ投稿
function postChatWork(body) {
  var payload = {
    'body': body
  };

  var options = {
    'method': 'post',
    'headers': { 'X-ChatWorkToken': CHATWORK_TOKEN },
    'payload': payload
  };

  UrlFetchApp.fetch('https://api.chatwork.com/v2/rooms/' + CHATWORK_ROOM_ID + '/messages', options);
}

// Google Calendarの当日のイベントを取得
function getTodayEvent() {
  var today = new Date();
  var calendar = CalendarApp.getCalendarById(CALENDAR_ID); 
  return calendar.getEventsForDay(today); 
}

// 休日判定
function isHoliday() {
  var today = new Date();
  
  // 土日判定
  var weekInt = today.getDay();
  if(weekInt <= 0 || 6 <= weekInt){
    return true;
  }
  
  // 祝日判定
  var calendarId = "ja.japanese#holiday@group.v.calendar.google.com";
  var calendar = CalendarApp.getCalendarById(calendarId);
  var todayEvents = calendar.getEventsForDay(today);
  if(todayEvents.length > 0){
    return true;
  }
  
  return false;
}

// 除外キーワード判定
function hasIgnoreKeyword(title) {
  var i = IGNORE_KEYWORDS.length;
  while(i--) {
    if (title.indexOf(IGNORE_KEYWORDS[i]) !== -1) {
      return true;
    }
  }
  return false;
}

以上でトリガーを設定することで毎朝自動でチャットが投稿されます。

毎朝、前日分のRSSをChatWorkに投稿する

エンジニアはそれぞれ方法で情報収集をしていますが、チャットへ流すことで話題となり話が膨らみやすくなったり、個人の収集媒体ではキャッチアップできなかった場合に備えて情報を抑えやすい環境にしています。

RSS用のURLをスプレッドシートに書き込むことで、毎朝GASで前日分の記事を取得してChatWorkへ投稿されます。

スクリプトの処理の流れは以下です。

  1. スプレッドシートから読み込み
  2. RSS取得
  3. ChatWork用に投稿内容を加工
  4. ChatWorkに投稿

今回は対象のRSS URLに加え、APIトークンとRoom IDもsheetから取得しようと思います。

事前準備

まず、スプレッドシートに必要な情報を書き込みます。

f:id:lcl-engineer:20180314120423p:plain

ChatWorkを利用する方はAPIトークンが必要です。
前節でAPIトークンを取得済の方はそのまま使いまわせます。

取得方法

APIトークン発行ページを開き、パスワードを入力後に表示されるAPI Tokenをコピーします。

実装

1. スプレッドシートから読み込み

getRangeでセルを指定し、getValueで値を取得します。

var spreadSheet = SpreadsheetApp.openById(<シートID>);
var sheet = spreadSheet.getSheetByName(<シート名>); 

// APIトークンとRoom IDを取得
var token = sheet.getRange("B1").getValue();
var room_id = sheet.getRange("B2").getValue();

次にURLを取得します。A列5行目以降を全て読み込みます。読み込んだ後は不要な配列を削除するために一手間加えています。

var values = sheet.getRange("A" + ":" + 5).getValues();
var urls = new Array();
// 配列を整理
for (var i = 0; i < values.length; i++) {
  if (i >= startIndex) { 
    if(values[i] != null && values[i] != "") {
      urls.push(values[i]);
    }
  }
}

2. RSS取得

RSSの取得はfetchPostAtYesterdayメソッドで行います。 URLが不正だったり、アクセスに不具合が生じるとエラーになるためtry-catch文で制御します。

function myFunction() {
  // 前日の日付を取得
  var yesterday = getYesterday();  
  
  // 前日の記事を取得
  var entries = new Array();
  urls.forEach(function(url) {
    try {
      var posts = fetchPostAtYesterday(url[0], yesterday)[0]
    } catch(e) {
      Logger.log(e);
    }
    
    if (posts != null) {
      entries.push(posts);
    }
  });
}

/**
 * feedURLから記事情報を取得
 * @param {String} feedUrl
 * @return {Array.<*>}
 */
function fetchPostAtYesterday(feedUrl, yesterday) {
  var response = UrlFetchApp.fetch(feedUrl);
  var rssXML   = response.getContentText();
  var document = XmlService.parse(rssXML);
  var root     = document.getRootElement();
  var ns_rss   = XmlService.getNamespace('http://purl.org/rss/1.0/');
  var ns_dc    = XmlService.getNamespace('dc', 'http://purl.org/dc/elements/1.1/');
  var ns_atom  = XmlService.getNamespace('http://www.w3.org/2005/Atom');
  var ns_rdf   = XmlService.getNamespace('http://www.w3.org/1999/02/22-rdf-syntax-ns#');
  var rootTagName = root.getName().toLowerCase();
  var entries = [];
  // 記事のリストを取得
  switch (rootTagName) {
    case 'rdf': // 1.0
      entries = root.getChildren('item', ns_rss);
      break;
    case 'feed': // atom
      entries = root.getChildren('entry', ns_atom);
      break;
    case 'rss': // 2.0
      entries = root.getChild('channel').getChildren('item');
      break;
    default:
      return false;
  }
  return entries.map(function(entry, index) {
    var title;
    var link;
    var pubDate;
    switch (rootTagName) {
      case 'rdf': // 1.0
        title   = entry.getChild('title', ns_rss).getText();
        link    = entry.getChild('link', ns_rss).getText();
        pubDate = entry.getChild('date', ns_dc).getText();
        break;
      case 'feed': // atom
        title   = entry.getChild('title', ns_atom).getText();
        link    = entry.getChild('link', ns_atom).getAttribute('href').getValue();
        // pubDate = entry.getChild('published', ns_atom).getText(); // 普段はこっちの方を利用する
        pubDate = entry.getChild('updated', ns_atom).getText(); // 今回はQiita用にupdatedを利用する
        break;
      case 'rss': // 2.0
        title   = entry.getChild('title').getText();
        link    = entry.getChild('link').getText();
        pubDate = entry.getChild('pubDate').getText();
        break;
    }
    // 投稿日が昨日なら
    var entryDate = Utilities.formatDate(new Date(pubDate), 'Asia/Tokyo', 'YYYY-MM-dd');
    if (yesterday == entryDate) {
      return {
        'title': title,
        'link': link
      }
    }
    return '';
  }).filter(function(item) {
    return item !== '';
  });
}

// 前日の日付を取得
function getYesterday() {
  var date = new Date();
  date.setDate(date.getDate() - 1);
  return Utilities.formatDate(date, 'Asia/Tokyo', 'YYYY-MM-dd');
}

RSS取得の処理は以下の記事を参考にさせていただきました。
TypetalkとGoogle App Scriptを組み合わせて、RSSのBOTを作ってみる。

3. ChatWork用に投稿内容を加工

例のごとく、読みやすいように加工していきます。今回は以下のように投稿します。

f:id:lcl-engineer:20180314120401p:plain

// 記事が存在すればChatWorkへ投稿
if (entries.length != 0) {
  var body = '[info][title]' + yesterday + 'に公開された記事[/title]\n';
  for(var e in entries) {
    body += entries[e].title;
    body += '\n';
    body += entries[e].link;
    body += '\n\n';
  }
  body += '[/info]';
}

4. ChatWorkに投稿

最後にChatWorkへ投稿します。前節を参考にしてください。

全体のコード

上記に加え、はてなブックマーク数の取得しています。

var SHEET_ID = "<シートID>";
var CHATWORK_TOKEN = '';
var CHATWORK_ROOM_ID = null;

function myFunction() {
  var sheet = getSheet('rss');
  // スプレッドシートから情報を取得
  CHATWORK_TOKEN = sheet.getRange("B1").getValue();
  CHATWORK_ROOM_ID = sheet.getRange("B2").getValue();
  var urls = getColumValues(sheet, "A", 5);

  // 前日の日付を取得
  var yesterday = getYesterday();  
  
  // 前日の記事を取得
  var entries = new Array();
  urls.forEach(function(url) {
    try {
      var posts = fetchPostAtYesterday(url[0], yesterday)[0]
    } catch(e) {
      Logger.log(e);
    }
    
    if (posts != null) {
      entries.push(posts);
    }
  });

  // 記事が存在すればChatWorkへ投稿
  if (entries.length != 0) {
    var body = '[info][title]' + yesterday + 'に公開された記事[/title]\n';
    for(var e in entries) {
      body += entries[e].title;
      
      // はてブ数を取得
      var hatena_count = fetchHatenaBookmarkCount(entries[e].link)
      if(hatena_count != '') {
        body += " / はてブ数: " + hatena_count;
        if(hatena_count >= 50) {
          body += "⭐";
        }
      }
      
      body += '\n';
      body += entries[e].link;
      body += '\n\n';
    }
    body += '[/info]';
    
    postChatWork(body);
  }
}

/**
 * feedURLから記事情報を取得
 * @param {String} feedUrl
 * @return {Array.<*>}
 */
function fetchPostAtYesterday(feedUrl, yesterday) {
  var response = UrlFetchApp.fetch(feedUrl);
  var rssXML   = response.getContentText();
  var document = XmlService.parse(rssXML);
  var root     = document.getRootElement();
  var ns_rss   = XmlService.getNamespace('http://purl.org/rss/1.0/');
  var ns_dc    = XmlService.getNamespace('dc', 'http://purl.org/dc/elements/1.1/');
  var ns_atom  = XmlService.getNamespace('http://www.w3.org/2005/Atom');
  var ns_rdf   = XmlService.getNamespace('http://www.w3.org/1999/02/22-rdf-syntax-ns#');
  var rootTagName = root.getName().toLowerCase();
  var entries = [];
  // 記事のリストを取得
  switch (rootTagName) {
    case 'rdf': // 1.0
      entries = root.getChildren('item', ns_rss);
      break;
    case 'feed': // atom
      entries = root.getChildren('entry', ns_atom);
      break;
    case 'rss': // 2.0
      entries = root.getChild('channel').getChildren('item');
      break;
    default:
      return false;
  }
  return entries.map(function(entry, index) {
    var title;
    var link;
    var pubDate;
    switch (rootTagName) {
      case 'rdf': // 1.0
        title   = entry.getChild('title', ns_rss).getText();
        link    = entry.getChild('link', ns_rss).getText();
        pubDate = entry.getChild('date', ns_dc).getText();
        break;
      case 'feed': // atom
        title   = entry.getChild('title', ns_atom).getText();
        link    = entry.getChild('link', ns_atom).getAttribute('href').getValue();
        // pubDate = entry.getChild('published', ns_atom).getText(); // 普段はこっちの方を利用する
        pubDate = entry.getChild('updated', ns_atom).getText(); // 今回はQiita用にupdatedを利用する
        break;
      case 'rss': // 2.0
        title   = entry.getChild('title').getText();
        link    = entry.getChild('link').getText();
        pubDate = entry.getChild('pubDate').getText();
        break;
    }
    // 投稿日が昨日なら
    var entryDate = Utilities.formatDate(new Date(pubDate), 'Asia/Tokyo', 'YYYY-MM-dd');
    if (yesterday == entryDate) {
      return {
        'title': title,
        'link': link
      }
    }
    return '';
  }).filter(function(item) {
    return item !== '';
  });
}

// 前日の日付を取得
function getYesterday() {
  var date = new Date();
  date.setDate(date.getDate() - 1);
  return Utilities.formatDate(date, 'Asia/Tokyo', 'YYYY-MM-dd');
}

// スプレッドシートを取得
function getSheet(sheetName) {   
  var spreadSheet = SpreadsheetApp.openById(SHEET_ID);
  return spreadSheet.getSheetByName(sheetName); 
}

// 指定したカラムの値を取得
function getColumValues(sheet, columnName, startIndex) {
  var values = sheet.getRange(columnName + ":" + columnName).getValues();
  // 配列を整理
  var result = new Array();
  for (var i = 0; i < values.length; i++) {
    if (i >= startIndex) { 
      if(values[i] != null && values[i] != "") {
        result.push(values[i]);
      }
    }
  }
  return result;
}

// はてなブックマーク数を取得
function fetchHatenaBookmarkCount(url) {
  if(url == '') {
    return '';
  }
  
  var hatena_api_url = "http://b.hatena.ne.jp/entry.count?url="
  try {
    return UrlFetchApp.fetch(hatena_api_url + encodeURIComponent(url)).getContentText();
  } catch (e) {
    Logger.log(e);
  }
   return '';
}

// ChatWorkへ投稿
function postChatWork(body) {
  var payload = {
    'body': body
  };

  var options = {
    'method': 'post',
    'headers': { 'X-ChatWorkToken': CHATWORK_TOKEN },
    'payload': payload
  };

  UrlFetchApp.fetch('https://api.chatwork.com/v2/rooms/' + CHATWORK_ROOM_ID + '/messages', options);
}

最後にトリガーを設定します。 以上で毎朝、前日分のRSSを一覧で投稿されるようになります。

最後に

簡単なサービスの連携であればIFTTTやZapierで十分ですが、GASのハードルも高くないので運用してみてはいかがでしょうか。