72
67

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

【Bot】毎日Slackに良い記事を届けてくれるDaily Qiita君を作った

Last updated at Posted at 2019-01-25

追記

・2018/1/28
Dairy Qiita君 → Daily Qiita君に変更
一番大事なところミスった。。。。

・2018/1/28
deleteファンクションがうまく機能していなかったので修正

はじめに

Bot作った背景としては、同期から**「Qiitaの良い記事を自動でSlackに投げてくれたら嬉しいよね!」**と言われ、
確かに面白いと思ったので作ってみました。

何が面白いのかというと、Slack内でのチャンネルに良記事が流れてきたら、
ただのお知らせじゃなくて**「議論のきっかけ」になり、記事を読む以上のことが学べる**といったところです。

作ったもの

GAS + Qiita API + Slack APIで良記事を毎日届けてくれるBot君です。
※良記事=ストック数が3以上 && 直近1週間の中でいいね数が上位30記事 

毎日自動でQiitaの記事をスプレッドシートへ保存してくれます
スクリーンショット 2019-01-19 17.35.05.png

良い記事だけをスクリーニングしてSlackへ送ってくれます
スクリーンショット 2019-01-19 17.38.55.png

要件を決める

まずDaily Qiita君を作るに当たって以下のような要件にしました。

  • qiitaに投稿された直近1週間分の中から「いいね」が多い記事をSlackに投稿したい
  • Slackのチャネル(goチャネル、javaチャネルなど)毎にタグづけされた対象の記事を振り分けたい
  • 良記事30件くらいのデータのみをSlackに投稿したい(随時調整)
  • 不要なデータはスプレッドシートから削除したい
  • 同じ記事は送られたくない。一度送ったデータは再度Slackに送りたくない
  • 記事のいいね数/ストック数、URL情報をSlackに送りたい
  • 毎朝9時など通勤時間帯にSlackに送りたい
  • 構想から実現までのリミットは1日(個人的に時間はかけたくない)

コード

コピペしたい人用にgithubにコードを上げています。
https://github.com/yoshinorihisakawa/dairy-qiita

実装する前に処理の流れざっと考えてみました。

  • qiitaの記事を直近1週間分取得する
    • stock数 > 3 以上などにして記事数を絞る
    • 毎日起動するので、重複したデータはスプレッドシートに書き込まない
    • 最後に追記する形で記事をスプレッドシートに書き込んでいく
  • ストック数を取得する
    • ストック数を取得するには、記事のIDで再度APIを叩くしか方法はなかった
  • 取得した記事をソートする
    • 良い記事を上位30件までにしたかったのでいい記事順にソートする
  • Slackに記事を送信する
    • ソート済みなので上から順に30件Slack送る
    • タグ別にチャンネルを振り分ける
    • 送ったデータには送り済みフラグをつける
  • 直近1週間でない記事は削除する
    • データが多くなると処理も重くなるので

あとは実装あるのみ。1日で終わらせたかったので、動くことを優先させたため、
綺麗なコードとは言えません。


function main() {
  inputItems()
  getItemStock()
  sort()
  postSlackMessage()
  deleteOneWeek()
}

function inputItems() {
  const COL_TITLE_ID = 1;
  const COL_TITLE = 2;
  const COL_URL = 3;
  const COL_TAGS = 4;
  const COL_LIKE = 9;
  const COL_STOCK = 10;
  const COL_ISSEND = 12;
  const COL_GETDATE = 13;
  
  // stockが3以上 && 直近1週間前のデータ
  const API_ENDPOINT = 'https://qiita.com/api/v2/items?page=1&per_page=100&query=stocks%3A%3E6+created%3A%3E';
  // 1週間前を指定する
  const CREATED_AT = Moment.moment().add(-7,'d').format('YYYY-MM-DD');
  try {
    var res = UrlFetchApp.fetch(API_ENDPOINT + CREATED_AT);
    var json = JSON.parse(res.getContentText());
    
    var sheet = SpreadsheetApp.getActiveSheet();
    
    // 書き込み関連の処理
    json.forEach(function(item){
      lastRow = sheet.getLastRow();

      // 重複チェック 重複していた場合は処理をスキップさせる
      var ids = sheet.getRange(2, COL_TITLE_ID, lastRow -1).getValues();
      var isDuplicated = ids.some(function(array, i) {
        return (array[0] === item["id"]);
      });
      if (isDuplicated === true) {
        Logger.log(item["id"])
        return;
      }
      
      // 最後の行に追記する形で行書き込み
      rowToWrite = lastRow + 1   
      sheet.getRange(rowToWrite, COL_TITLE_ID).setValue(item["id"]);
      sheet.getRange(rowToWrite, COL_TITLE).setValue(item["title"]);
      sheet.getRange(rowToWrite, COL_URL).setValue(item["url"]);
      var tags = item["tags"]
      tags.forEach(function(tag, i){
        sheet.getRange(rowToWrite, COL_TAGS + i).setValue(tags[i]["name"]);
      });
      sheet.getRange(rowToWrite, COL_LIKE).setValue(item["likes_count"]);
      var now = Moment.moment().format('YYYYMMDD');  
      sheet.getRange(rowToWrite, COL_GETDATE).setValue(now);
    });
  } catch (ex) {
      Logger.log(ex)
  }
}

function getItemStock() {
  const COL_TITLE_ID = 1;
  const COL_TITLE = 2;
  const COL_URL = 3;
  const COL_TAGS = 4;
  const COL_LIKE = 9;
  const COL_STOCK = 10;
  const COL_ISSEND = 12;
  const COL_GETDATE = 13;
  
  var sheet = SpreadsheetApp.getActiveSheet();
  lastRow = sheet.getLastRow();
  
  var ids = sheet.getRange(2, COL_TITLE_ID, lastRow -1).getValues();
  const API_ENDPOINT_REPLACE = 'https://qiita.com/api/v2/items/{id}/stockers?page=1&per_page=100';
  // 100以上は取得しない。100以上のは送る
  for(var i = 0; i < ids.length; i ++) {
    var API_ENDPOINT = API_ENDPOINT_REPLACE.replace('{id}',ids[i]);
    
    try {
      var res = UrlFetchApp.fetch(API_ENDPOINT);
      var js = JSON.parse(res.getContentText());
      var stockNum = Object.keys(js).length;
      sheet.getRange(i + 2, COL_STOCK).setValue(stockNum);
    } catch (ex) {
      Logger.log(ex)
    }
    
  }
}

function sort() {
  const COL_LIKE = 9;
  const COL_STOCK = 10;
  var sheet = SpreadsheetApp.getActiveSheet();
  var lastRow = sheet.getLastRow();
  var lastCol = sheet.getLastColumn();
  sheet.getRange(2, 1, lastRow, lastCol).sort({column: COL_STOCK, ascending: false});
  sheet.getRange(2, 1, lastRow, lastCol).sort({column: COL_LIKE, ascending: false});
}

function postSlackMessage() { const COL_TITLE_ID = 1;
  const COL_TITLE = 2;
  const COL_URL = 3;
  const COL_TAGS = 4;
  const COL_LIKE = 9;
  const COL_STOCK = 10;
  const COL_ISSEND = 12;
  const COL_GETDATE = 13;
  
  var sheet = SpreadsheetApp.getActiveSheet();
  
                             
  var token = PropertiesService.getScriptProperties().getProperty('SLACK_ACCESS_TOKEN');
 
  var slackApp = SlackApp.create(token);
                             
  var keyValue = { 'Go': '#com_golang', 
                   'アジャイル': '#z_agile',
                   'CI': '#z_cicd',
                   'JavaScript': '#z_javascript',
                   'Ruby': '#z_ruby',
                   'AWS': '#z_aws',
                   'docker': '#z_docker',
                   'Java': '#z_java',
                 };
                             
  for(var i = 2; i < 31; i ++) {
    // その行がすでに送りずみならcontinue
    var isSend = sheet.getRange(i, COL_ISSEND).getValue()
    if (isSend === true) {
      continue;
    }
    // タグ1から5まで
     for(var t = 0; t < 5; t ++) {
       var tag = sheet.getRange(i, t + COL_TAGS).getValue()
       Object.keys(keyValue).forEach(function(kv){
         //valueを配列にする
         var ary = kv.split(',');
         
         var exist = ary.some(function(str, i, data) {
           return (str === tag);
         });
         
         if (exist) {
           var title = sheet.getRange(i, COL_TITLE).getValue()
           var url = sheet.getRange(i, COL_URL).getValue()
           var like = sheet.getRange(i, COL_LIKE).getValue()
           var stock = sheet.getRange(i, COL_STOCK).getValue()
                  
           var options = {
             channelId: keyValue[kv], //チャンネル名
             userName: "daily qiita君", //投稿するbotの名前
             message: title + "\n" + url + "\n" + "いいね数 = " + like  + " / ストック数 = " + stock  //投稿するメッセージ
           };
           
           slackApp.postMessage(options.channelId, options.message, {username: options.userName});
   
           // 最後に送りずみマークのtrueをつける
           sheet.getRange(i, COL_ISSEND).setValue(true);
           
           return;
         }
       });
     }
  }                   
}

function deleteOneWeek() {
  const COL_GETDATE = 13;
  // 1週間前のデータは削除
  var DELETED_AT = Moment.moment().add(-8,'d').format('YYYYMMDD');
  var sheet = SpreadsheetApp.getActiveSheet();
    
  // 値の比較
  var lastRow = sheet.getLastRow();
  var dates = sheet.getRange(2, COL_GETDATE, lastRow -1).getValues();
  var row = 0
  dates.forEach(function(date, i){
    if (DELETED_AT == date.toString()) {
      sheet.deleteRow(row + 2);
      row = row - 1
    }
    row = row + 1
  });
}

daily-qiita君をあなたの環境で動かすには?

1. Googleのアドオン追加でGASを使えるようにします。
以下のURLを参照
https://developers.google.com/apps-script/

2. スクリプト × スプレッドシートの準備
スプレッドシート準備: Googleドライブ > 新規 > Googleスプレッドシート
スクリプト準備: Googleスプレッドシートのツール > スクリプトエディタ

3. ライブラリの追加
Moment: 時刻を扱うライブラリ
https://tonari-it.com/gas-moment-js-moment/

SlackApp: slackを扱うライブラリ
https://qiita.com/soundTricker/items/43267609a870fc9c7453

4. スプレッドシートに雛形を用意
手書きでもいいですが、こちらのスクリプトを貼って、動かせばスプレッドシートに雛形が完成します。


function init() {
  // シートの名前作成
  const COL_TITLE_ID = 1;
  const COL_TITLE = 2;
  const COL_URL = 3;
  const COL_TAGS1 = 4;
  const COL_TAGS2 = 5;
  const COL_TAGS3 = 6;
  const COL_TAGS4 = 7;
  const COL_TAGS5 = 8;
  const COL_LIKE = 9;
  const COL_STOCK = 10;
  const COL_ISSEND = 12;
  const COL_GETDATE = 13;

  var sheet = SpreadsheetApp.getActiveSheet();
  //シート初期化
  //sheet.clear();
  sheet.getRange(1, COL_TITLE_ID).setValue("記事ID");
  sheet.getRange(1, COL_TITLE).setValue("タイトル");
  sheet.getRange(1, COL_URL).setValue("URL");
  sheet.getRange(1, COL_TAGS1).setValue("タグ1");
  sheet.getRange(1, COL_TAGS2).setValue("タグ2");
  sheet.getRange(1, COL_TAGS3).setValue("タグ3");
  sheet.getRange(1, COL_TAGS4).setValue("タグ4");
  sheet.getRange(1, COL_TAGS5).setValue("タグ5");
  sheet.getRange(1, COL_LIKE).setValue("いいね数");
  sheet.getRange(1, COL_STOCK).setValue("ストック数");
  sheet.getRange(1, COL_ISSEND).setValue("status");
  sheet.getRange(1, COL_GETDATE).setValue("取得日");
}

5. コードをコピペ
そのまま貼り付けたらおっけー!!
https://github.com/yoshinorihisakawa/dairy-qiita

6. スラックBotのAPIの登録
https://qiita.com/ykhirao/items/3b19ee6a1458cfb4ba21

7. 定期実行設定
https://tonari-it.com/gas-trigger-set/

注意

・初回の承認手続き
承認の要求が出てきたときは以下の手順で
https://www.virment.com/step-allow-google-apps-script/

最後に

GASを使えば結構簡単に実現したいことを実現できると今更ながら思いました。
他にもおもしそうなことがあれば、思うだけではなく実現していきたいです!

72
67
5

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
72
67

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?