人生100年!生涯エンジニア人生!

楽しいエンジニア人生!

20時半からのConnpass(「勉強会がリモート開催になり、勉強会に参加しにくくなった件」の反響を受けて)

概要

「勉強会がリモート開催になり、勉強会に参加しにくくなった件」という記事を書きました。
kawahara-ci.hatenablog.com

その反響で多かったのは夜遅くなら参加できるというのがありました。

b.hatena.ne.jp

親世代でリモートワークの人は、子どもが寝たあとなら参加できるのでは?ということです。
そこで疑問がありました、夜遅くに開催している勉強会はあるのでしょうか??

結果として確認するサイトを作成しました。
「20時半からのConnpass」です。
nightstudygroup.netlify.app

確認する

勉強会を告知するサイトで有名なConnpassを確認する。
開催日は見ることはできるようですが、時刻までは見ることはできません。

connpass.com

検索で指定できるかな?と思いましたが、日付のみの検索でした。

connpass.com

APIを見る

何とかできないかな??
そうだ!APIを見てみよう!!
connpass.com

日付や月単位の検索はできるようです。

Google App Scriptを使う

APIがあるなら、データの取得は簡単です。
とりあえず、データを取得してみましょう!
Google Spreadsheetに持ってこれるじゃないか!
そう!Google App Script(通称GAS)なら簡単です。
ソースコードは綺麗じゃないのはご了承ください。)

// ConnpassのAPIを使ってデータを取得する
function get_connpass_data() {
  // 4ヶ月先までのパラメータを作成する
  var dt_object = [];
  for (var i = 0; i < 5; i++) {    
    var dt = new Date(new Date().setDate(1));
    var work_date =new Date(dt.setMonth(dt.getMonth() + i));
    dt_object.push(String(work_date.getFullYear()) + ('00'+(work_date.getMonth() + 1)).slice(-2));
  }
  var url_date = dt_object.join(',');

  // シートを選択しクリアする
  var sheet = set_sheet('data');
  sheet.clearContents();

  // 先頭のcolumnを設定
  column = [['event_id', 'title', 'event_url', 'started_at', 'ended_at', 'place', 'address']];
  sheet.getRange('A1:G1').setValues(column);
  var today = new Date(new Date().setHours(0, 0, 0, 0));

  // ページングの初期値
  var start = 1;

  do {
    // APIをfetchする
    var url = 'https://connpass.com/api/v1/event/?order=2&ym='+ url_date + '&count=100&start=' + start;
    var res = UrlFetchApp.fetch(url);
    var object = JSON.parse(res.getContentText()); 

    // APIのResultを取得する
    var results_returned = Number(object['results_returned']);
    var results_start = Number(object['results_start']);
    var results_available = Number(object['results_available']);

    var values = [];
    for (var i = 0; i < object['events'].length; i++) {
      var value = [];
      var has_today = false;
      
      for (var key in object['events'][i]) {
        // 取得するデータを選ぶ
        if (['event_id', 'title', 'event_url', 'address', 'place'].includes(key)) {
          value.push(object['events'][i][key]);
        }

        // 日付はローカル時間に変換する
        if (['started_at', 'ended_at'].includes(key)) {
          var dt = new Date(object['events'][i][key]);
          var words = dt.toLocaleString();
          value.push(words);
        }
        
        // 開催日時が今日より過去ならデータを取得しない
        if (['started_at'].includes(key)) {
          var dt = new Date(object['events'][i][key]);
          var words = dt.toLocaleString();
          var words_split = words.split(' ');
          if (today.getTime() <= new Date(words_split[0]).getTime()) {
            has_today = true;
          }
        }
      }

      if (has_today) {
        values.push(value);
      }
    }

    // Spreadsheetにセットする
    sheet.getRange(start + 1, 1, values.length, values[0].length).setValues(values);

    // 今日より過去なら処理終了する
    if (!has_today) {
      break;
    }
    start = start + results_returned;
  } while((results_start + results_returned) < results_available);

  // 処理完了後、開催日時でソートする
  var range = sheet.getRange(2, 1, sheet.getLastRow() - 1, sheet.getLastColumn() -1);
  range.sort(4);
}

//同じ名前のシートがなければ作成
function set_sheet(name){
  var sheet = SpreadsheetApp.getActive().getSheetByName(name)
  if(sheet)
    return sheet

  sheet=SpreadsheetApp.getActiveSpreadsheet().insertSheet();
  sheet.setName(name);
  return sheet;
}

完成したら、トリガーを設定して1時間毎にデータを取得します。

データを確認する

Google Spreadsheetのフィルターを使って確認する。
うーん、このままだと物足りないな。
ノーコード開発でやってみよう!!

glide.を使ってみました。

www.glideapps.com

ポチポチするだけでサイトが作れました。
pink-leg-5741.glideapp.io

見た目は良いのですが、無料枠はデータ量に制限があるとか、抽出条件の仕方が謎だったりして、納得できませんでした。

SpreadsheetをAPI

それならば先程のGoogle SpreadsheetをAPI化してAPI側がデータを抽出すれば良いのでは?ということで、再びGASを使います。

function doGet(e) {
  //var time_parameter = e.parameter.time;

  var callback = e.parameter.callback;
  
  var sheet = SpreadsheetApp.getActiveSheet();
  var maxRow = sheet.getLastRow();
  var maxColumn = sheet.getLastColumn();
  
  // 一気にデータを取得してJson形式に変換する
  var row = sheet.getRange(1,1, maxRow, maxColumn).getValues();
  var data = parse2Json(row);
  data = data.filter(v => v);
  
  var res = ContentService.createTextOutput(callback + '(' + JSON.stringify(data) + ')');
  res = res.setMimeType(ContentService.MimeType.JAVASCRIPT);
  
  return res
}

// Json形式に変換する
function parse2Json(values) {
  return values.map(function (value, i, arr) { 
    var obj = {};
    if(i === 0) return;
    for(var j = 0 ; j < value.length ; j++) {
      if ('started_at' != arr[0][j]) {
        obj[arr[0][j]] = value[j];
      } else {
        if (Utilities.formatDate(value[j], 'Asia/Tokyo', 'HHmm') < 2030) {
          return;
        } else {
          obj[arr[0][j]] = value[j];
        }
      }
    }
    return obj; 
  }).splice(1,values.length - 1);
}

GitHubはこちらです。

github.com

Netlify化

GASをAPI化したら、あとは表示するだけです。
(Connpass APIを直接叩いても良いのですが・・・)

netlifyとjQueryでサクっと作りました。
www.netlify.com

<!doctype html>
<html lang="ja">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>20時半からのconnpass</title>
    <meta name="description" content="20時半からのconnpassを表示します。" />
    <meta property="og:url" content="https://nightstudygroup.netlify.app/" />
    <meta property="og:title" content="20時半からのconnpass" />
    <meta property="og:type" content="website">
    <meta property="og:description" content="20時半からのconnpassを表示します。" />
    <meta property="og:image" content="https://res.cloudinary.com/profile-card/image/upload/l_text:Sawarabi%20Gothic_40_bold:20%E6%99%82%E5%8D%8A%E3%81%8B%E3%82%89%E3%81%AECompass%0A%E4%BD%9C%E6%88%90%E8%80%85%EF%BC%9A@sapi_kawahara/v1592844332/600x315.png">
    <meta name="twitter:card" content="summary_large_image" />
    <meta name="twitter:site" content="@sapi_kawahara" />
    <meta property="og:site_name" content="20時半からのconnpass" />
    <meta property="og:locale" content="ja_JP" />
    <link rel="canonical" href="https://nightstudygroup.netlify.app/" />

    <!-- Bootstrap CSS -->
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/5.0.0-alpha1/css/bootstrap.min.css" integrity="sha384-r4NyP46KrjDleawBgD5tp8Y7UzmLA05oM1iAEQ17CSuDqnUK2+k9luXQOfXJCJ4I" crossorigin="anonymous">

    <script src="https://code.jquery.com/jquery-3.5.1.min.js" integrity="sha256-9/aliU8dGd2tb6OSsuzixeV4y/faTqgFtohetphbbj0=" crossorigin="anonymous"></script>
  </head>
  <body>
    <script>
      $.ajax({
        type: 'GET',
        url: 'APIのURL',
        dataType: 'jsonp',
        jsonpCallback: 'myCallback',
        success: function(json){
        var len = json.length;
        var count = 0;
        for(var i=0; i < len; i++){
          var started_at = new Date(Date.parse(json[i].started_at));
          var ended_at = new Date(Date.parse(json[i].ended_at));
          started_at = started_at.toLocaleString();
          ended_at = ended_at.toLocaleString();
          if ((count % 2) == 0) {
            var card_pattern1 = `<div class="row row-cols-1 row-cols-md-2 g-4">
  <div class="col">
    <div class="card bg-light mb-3">
      <div class="card-body">
        <h5 class="card-title text-truncate">${json[i].title}</h5>
        <p class="card-text">開催日時:${started_at}<br>終了日時:${ended_at}</p>
        <p class="card-text">開催会場:${json[i].place}<br>開催場所:${json[i].address}</p>
        <a href="${json[i].event_url}" target="_blank" rel="noopener noreferrer" class="card-link btn btn-info btn-lg btn-block">${json[i].event_url}</a>
      </div>
    </div>
  </div>
`;
          } else {
            var card_pattern2 = `<div class="col">
    <div class="card bg-light mb-3">
      <div class="card-body">
        <h5 class="card-title text-truncate">${json[i].title}</h5>
        <p class="card-text">開催日時:${started_at}<br>終了日時:${ended_at}</p>
        <p class="card-text">開催会場:${json[i].place}<br>開催場所:${json[i].address}</p>
        <a href="${json[i].event_url}" target="_blank" rel="noopener noreferrer" class="card-link btn btn-info btn-lg btn-block">${json[i].event_url}</a>
      </div>
    </div>
  </div>
</div>
`;
            $("#connpass-data").append(card_pattern1 + card_pattern2);
          }
          count++;
        }
        if ((count % 2) == 1) {
          $("#connpass-data").append(card_pattern1);
        } 
      }
    });
    $(document).ajaxStop(function() {
      $(".spinner-border").remove();
    });
    </script>
    <div class="container">
      <p class="h1">20時半からのconnpass</p>
      <p class="h6"><a href="https://kawahara-ci.hatenablog.com/entry/2020/10/31/Twenty_thirty_Connpass" target="_blank" rel="noopener">これを作成した経緯</a>です。</p>
      <p class="h6">ご要望は <a href="https://twitter.com/sapi_kawahara" target="_blank" rel="noopener">@sapi_kawahara</a> までお願います。</p>
      <div class="d-flex justify-content-center">
        <div class="spinner-border text-primary" style="width: 5rem; height: 5rem;" role="status">
          <span class="sr-only">Loading...</span>
        </div>
      </div>
      <span id="connpass-data"></span>
    </div>
  </body>
</html>

そして出来たのは、こちらです。
「20時半からのConnpass」です。
nightstudygroup.netlify.app

まとめ

JavaScriptが苦手でも、何とか作れました。
こうやって見える化をすると、20時半以降開催の勉強会は多くないですね。
見える化するだけなら、最初のGoogle Spreadsheetを使ってGoogle App Scriptでデータを取得するだけで良かったのですが、ついついノリでやってしまいました。(反省はしてない)
見える化できたので、今後は何かできないか考えましょう。