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

楽しいエンジニア人生!

PHPカンファレンス2020に参加した感想

まだ全部見てない

リアルタイムで見たのは、以下の4つぐらいです。
土曜日のオンラインは家庭優先なのですよね・・・。

fortee.jp

fortee.jp

fortee.jp

fortee.jp

どの登壇も気付きを感じる内容で良かったです。
APIとセキュリティに偏っておりますが、この辺りは知見を増やして損はないからです。
下にざっくりと感想を書きます。

  • めもり〜さんの登壇は、前任者の遺産から始まるよくある話なのですが、そこからスクラッチで作るところ、それを許す会社環境など技術以外も見どころがありました。
  • あきのさんの登壇は、GraphQLと思いつつLighthouseの話でツボを抑えた話で、スライドは登壇で話せなかったことが多数あるのでLighthouseで開発するときは、スライドを参考にしたいしたいです。
  • ariakiさんの登壇は、Web開発では気付かないセキュリティの話で、過去にやってしまったことが出てきて、聞きながら「うっ」となってしまいました。
  • 徳丸浩さんの登壇は、非常に勉強になる!し「検索するとき上位のサイト参考にする」の辺り「公開処刑」が、物凄く面白かった!検索上位が汚れているのは身近なセキュリティ話と強く思います。

他の内容も、当然ながら気になっておりますので、できるだけ拝見したいと思っております。

今年のPHP漢字

今年はオンライン開催だけど、少しでも参加しようと思い、GMOさんが開催している「今年のPHP漢字」応募した。

www.gmo.jp

応募したツイートは、こちらです。

このツイートで、そしたら、まさかの!カンファレンス賞をもらいました。

カンファレンス賞は、下の3点だったのですが、お手製の一点物表彰状もあり嬉しかったです、それと布マスクだと行きにくい場所に鼻セレブマスクは助かります。
※名前が違うのは、よくある話なのでLGTM!

  • Oculus Quest 2
  • PHPcon2020限定コラボTシャツ
  • PHPcon2020限定カップ
  • 表彰状
  • ステッカー
  • 鼻セレブマスク

GMOさん、本当に、ありがとうございました。

まだ開封しただけです。
余裕があれば、色々してみたいですね。

このはさん

「PHPcon2020限定コラボTシャツ」はかわいいと評判なので、女の子の名前はあるのかな?とつぶやきました。

そしたら、お返事がきました。
「美雲このは」さんです!!

GMOさんが展開しているレンタルサーバーサービスの応援団長なのですね。(ムームードメインは使っているけど、ごめんなさい知らなかった)

conoha.mikumo.com

小江戸川越はコーヒーの街

この記事はCoffee Advent Calendar 2020の6日目として登録させていただきました。

小江戸川越

私が住む埼玉県川越市小江戸川越と呼ばれ江戸の風景を残す街として、池袋から40分以内で行けるという点もあり、日帰り旅行としても知られた街です。
江戸の風景とコーヒーの街というのは不釣り合いに感じるかもしれませんが、川越は江戸の風景だけじゃなく、コーヒーが似合う街なのです。

駅周辺にはチェーン店が多い

川越には多くの駅があります。
多くの人が訪れる川越駅周辺には、誰もが知るチェーン店が数多くあります。
JR川越駅の構内には「ベックス」が、駅を出ると「星乃珈琲店」「倉式珈琲店」「スターバックス」があります。
駅からちょっと離れると「ルノアール」「エクセルシオールカフェ」「タリーズコーヒー」「ドトールコーヒー」「コメダ珈琲」「珈琲館」などがあり、街散策の帰りなどに最適ですね。

絵になるコーヒーショップ

川越の魅力といえば街の風景で、街並みとコーヒーショップは絵になるのです。
古民家風なのは江戸、レンガ造りは大正、コーヒーを楽しみながら街の風景を楽しめるのです。

そんな素敵なコーヒーショップを探すのも川越探索の楽しみかと思います。
多くのサイトなどで紹介が多いので、ここでは外の雰囲気が良い2店を紹介します。

まずは、「シマノコーヒー大正館」です。
f:id:hideaki_kawahara:20201206233830j:plain:w400

ここは建物だけでも良い雰囲気だったりします。 観光名所の一番街の裏手なので気付かない人も多いので、大正浪漫夢通りも合わせて訪れると良いです。

次は、チェーン店ですが「スターバックス川越鐘つき通り店」です。

f:id:hideaki_kawahara:20201206234232j:plain 20 Starbucks stores to visit in 2020にも取り上げられるほどの有名店です。

stories.starbucks.com

観光名所である時の鐘の近くにあり、一見するとスターバックス?と思えるほど、スターバックスに見えないです。
スターバックスが観光名所になってしまうのは凄いです。
夕暮れは特に絵になります。
川越はコーヒーショップですら絵になるのです!

他にも、色々とあるのですが、今回は、これだけの紹介にします。
一時期は、大混雑してましたが、今は落ち着いておりますので、観光とともに、川越でカフェを楽しむのはいかがでしょうか?

Google Authenticator 3.1.0 で別のデバイスに移行する機能が追加されたので試してみる

概要

iOSが14.2にアップデートされたとき、Google Authenticatorが起動できないという不具合が2020年11月13日ぐらいから発生しておりました。

なお、「Google Authenticator 起動しない 14.2」とかで検索すると対処方法が多数あります。

そんな経緯があって、Google Authenticatorから他の認証アプリに乗り換えている人も居ますが、そもそもGoogle Authenticatorは、デバイス変更に対応していないので、他の認証アプリを使用している人も多いのが現実です。

そんな、iPhoneGoogle Authenticatorが3.1.0にアップデートした際にデバイス変更に対応したので試してみました。

試してみる

アプリケーションをアップデートします。

f:id:hideaki_kawahara:20201203125448j:plain

移行元デバイスで起動してから、右上の…をタップすると「アカウントのエクスポート」出てきます

f:id:hideaki_kawahara:20201203130032j:plain

移行元デバイスで、アカウントのエクスポートを実行すると、この画面が出てきます
それによると最大10個のアカウントをエクスポートできるようです。 なお、エクスポートしてからアカウントを消して再度行えば全てエクスポートできます。

f:id:hideaki_kawahara:20201203130042j:plain:w300

移行元デバイスで、デバイス自体の認証(パスコード、指紋認証、Face ID認証)を行うとQRコード表示されます
このQRコードを移行先のデバイスで読み取ります

f:id:hideaki_kawahara:20201203130054j:plain:w300

移行先のデバイスGoogle Authenticatorを起動してQRコードを読み取ると、インポートが完了します
f:id:hideaki_kawahara:20201203132038j:plain

移行元デバイスに戻り、QRコードの下にある「完了」をタップすると「エクスポートしたアカウントの削除」になります
アカウントが10個以上無く、移行元デバイスのフルリセットをするのであれば閉じるでも良いです。

f:id:hideaki_kawahara:20201203130100j:plain

まとめ

移行は意外と簡単でした。
他の認証アプリは、マスターアカウントを登録してクラウド経由で一気に移行するのですが、Google Authenticator 3.1.0はクラウド経由しないので若干手間ですが、マスターアカウントがクラウドで狙われるというリスクを回避できるので、企業などではGoogle Authenticator 3.1.0を利用するのもありだと思います

蛇足ですが、Google Authenticator 3.1.0の登場がiPhone 12が登場する前だったらとか、iOS14.2の不具合が無ければよかったねーとか思ってしまいます。

長年苦しめられてきた、ドコモメールの駄目な仕様が、iOS14のお陰で予定より早く消えそうで嬉しい!

iOS14の仕様変更

iOS14の仕様変更で、ドコモメールの駄目な仕様に設定されているとメール送信できないことになりました。

ドコモのアナウンスです。
service.smt.docomo.ne.jp

auのアナウンスです。
www.au.com

この駄目な仕様は「2連続のドット「..」が含まれていたり、アットマーク前にドット「.@」が含まれているメールアドレス」のことで、これはRFC5321で決めたことに違反しております。

RFC5321の4.1.2. Command Argument Syntaxに仕様が記載されていますが、難解なので..や.@のメールアドレスはRFC違反だと思ってください。

RFC 5321 - Simple Mail Transfer Protocol

歴史的な経緯

RFC違反のメールアドレスは、ドコモのiモードメール(現在のドコモメール)が発祥の地で、迷惑メールの多いドコモでは、RFC違反のメールアドレスにはパソコンからのメールが届かないということで、それが迷惑メールが来ないメールアドレスとして広まってしまいました。

それだけで終わると思われた、RFC違反のメールアドレスは、auに伝播します。

2006年6月6日に、auはメールアドレスの文字数を30文字に拡張すると同時に、RFC違反仕様が盛り込まれてしまいました。

k-tai.watch.impress.co.jp

なぜ、auは盛り込んでしまったのか?今となっては推測でしかないですが、2006年10月24日から番号ポータビリティ(通称MNP)が開始されたからとも言われております。
MNP後はMNPする前のキャリアメールは使用できなくなるので、携帯電話会社を乗り換えた後も、ドメインは異なっていても同じローカルパート(@の左側)を使用したいという需要に答えたのではないか?と推測します。

k-tai.watch.impress.co.jp

その後、ドコモが2009年4月1日から、RFC違反メールアドレスを新規登録をやめる方針に切り替わりました。

www.nttdocomo.co.jp

ドコモのメールアドレス変更のページには以下の記載があります。

また、2009年4月1日以降、「.」は「..」などのように連続で使用することや@マークの直前で使用することはできなくなりました。

この変更は2009年9月1日から始まるSPモードのためかと推測されます。

www.nttdocomo.co.jp

この対応で、RFC違反メールアドレスが減ると思われましたが、意外としぶとく残りました。
ガラケー同士はもちろん、携帯電話会社提供のメールアプリはRFC違反のメールアドレスが使えてしまうからです。
そして、iOS13までのメールアプリとメッセージアプリも、RFC違反のメールアドレスが使えていました。

このため、ドコモ同士や携帯電話会社間ならRFC違反のメールアドレスの送受信は可能なのです。
2020年の今ままで、11年間もRFC違反のメールアドレスは残ってしまったのです。

まとめ

今回iOS14の仕様変更で、RFC違反のメールアドレスが設定されているとメール送信ができないことは、大変喜ばしいことです。

アプリが対応してくれればRFC違反のメールアドレスはガラケーのみになるし、2026年3月31日にドコモのiモードが終了するので、このときにRFC違反のメールアドレスがこの世から消え去るのです!
メルマガ配信をしていた自分としては、本当に早く消えて去ってほしいと心から願うのでした。

おまけ

GmailRFC違反のメールアドレスを送信してみましょう!

macOSにDefaultで入っているPostfixを有効にして、Gmailにリレーするように設定をして試してみます。

% echo 'Test mail' | mail -s test test..@example.com
2020-11-16 00:15:31.961215+0900 0x1ff82a   Info        0x0                  67732  0    smtp: A44B369D557: to=<test..@example.com>, relay=smtp.gmail.com[64.233.189.108]:587, delay=1.3, delays=0.02/0.02/1.1/0.17, dsn=5.1.3, status=bounced (host smtp.gmail.com[64.233.189.108] said: 553-5.1.3 The recipient address <test..@example.com> is not a valid RFC-5321 553 5.1.3 address. o132sm15287203pfg.100 - gsmtp (in reply to RCPT TO command))

not a valid RFC-5321 と返答があることが確認できますね。

なお、コマンドラインで送信するとき、かなり気を付けないといけなくて、ちょっとしたことで危険な事になりかねないのです。

これは大丈夫ですがバッククォートを含んだメールアドレスをコマンドラインで送信すると、バッククォートの中を実行した結果が反映されてしまいます。
下の例だと、macOSで動かしているのでOSの名前Darwinに変換されています。
ちなみに、バッククオートが入ったメールアドレスはRFC違反ではないです。

% echo 'Test mail' | mail -s test test`uname`@example.com
2020-11-16 00:12:37.105535+0900 0x1fe590   Info        0x0                  66990  0    smtp: 82F9369D4E7: to=<testDarwin@example.com>, relay=smtp.gmail.com[64.233.189.109]:587, delay=2.6, delays=0/0/1.7/0.92, dsn=2.0.0, status=sent (250 2.0.0 OK  1605453157 gx24sm15347740pjb.38 - gsmtp)

同様に、ハイフンが入ったメールアドレスはRFC違反ではないですが、オプションと誤認します。

% echo 'Test mail' | mail -s test '-test@example.com'
mail: illegal option -- t
Usage: mail [-EiInv] [-s subject] [-c cc-addr] [-b bcc-addr] [-F] to-addr ...
       mail [-EHiInNv] [-F] -f [name]
       mail [-EHiInNv] [-F] [-u user]
       mail -e [-f name]
       mail -H

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でデータを取得するだけで良かったのですが、ついついノリでやってしまいました。(反省はしてない)
見える化できたので、今後は何かできないか考えましょう。