オードリー・タンの感染追跡システムをつくる(Github Copilotの感想も)

こんにちは。フリーランス・コンサルタント&エンジニアの 九保すこひ です。

さてさて、(この記事を書いている)最近は少し落ち着いたものの、やはりコロナウィルスがまだまだ怖かったりします。

正直なところ、今回の内容はもっと前にやってみようと思っていたのですが、もしかすると「不謹慎だろ❗」と思われるかも、、、と思いためらってました。

ズバリその機能とは・・・・・・

オードリー・タンがつくったショートメッセージの感染追跡システム

です。

オードリー・タン(唐鳳)さんは台湾の「天才デジタル担当大臣」のことで、なんとAppleの顧問をしていたとき、時給が1ビットコイン(当時で約5〜6万円だそうです)だったそうです。もう一度書きますが、これ「時給」です😳

ということで、今回は少しでも天才の一端に触れたいとの思いから似た機能をLaravel + SMSでつくってみることにしました。

ぜひ何かの参考になりましたら嬉しいです。😄✨

※ ちなみに話題のAIコーディングサポート「Github Copilot」が使えるようになったので、今回は極力Copilotが提案するコードを優先してみることにしました。(Copilotの感想は「おわりに」をご覧ください👍)

「オードリー・タンは、
構想から3日でつくったそうです
天才には勝てない・・・😂」

開発環境: Laravel 8.x、Twilio

どんなシステムなのか?

今回の感染調査システムは、流れでいうと次のようになります。

  1. お店にあるQRコードを読み取る
  2. SMS 送信画面が表示される(ここに場所 ID が書いてる)
  3. 送信

たったこれだけでOKです。

そして、このデータを貯めておき(オードリー・タンのシステムでは28日間だけ保存)その中からもし感染者が判明したら「あなたがいた場所から感染者がでました。体調はいかがですか?」と連絡できるわけです。

ちなみに、このシステムのメリットとしては、次のとおりです。

  • とにかく操作がシンプル
  • お店に個人情報が渡ることはない
  • SMS(ショートメッセージ)なのでほぼ全ての携帯から使える
  • QRコードが読めない場合でも、手入力でOK

シンプル&強力なシステムですね。

それでは、実際に作業をしていきましょう。

Twilio の準備をする

今回の機能は、SMSTwilioで受信することにします。
そのため、Twilioの設定から見ていきましょう。

まずは Twilio に登録し、ログインしてください。
コンソールが表示されたら、画面左上にあるTwilioマークをクリックします。

すると、アカウント(プロジェクト)の画面になりますので、「Create New Account」ボタンをクリックします。

アカウント名を入力して「Verify」ボタンをクリックします。

すると、本人確認用のSMSTwilioから送られてくるので、その認証コードを以下のフォームに入力して「Submit」ボタンをクリックします。

これで、アカウントは作成されますが、そのページにある「どんな使い方をするのか?」も以下のように入力して「Get Started with Twilio」ボタンをクリックしてください。

上から、

  • SMS
  • Identity & Verification
  • With code
  • PHP
  • No, I want to use my own hosting service

そして、ページ移動した先にはLaravelとの連携で必要になる、

  • ACCOUNT SID
  • AUTH TOKEN

があるので、それぞれこの文字列を.envへセットしておきます。

.env

TWILIO_ACCOUNT_SID=(ここに取得した ACCOUNT SID)
TWILIO_AUTH_TOKEN=(ここに取得した AUTH TOKEN)

そして、専用の電話番号をつくることになりますが、今回はテストなのでトライアル用の電話番号を用意します。

では、同じページにある「Get a trial phone number」ボタンをクリックします。

すると、候補の電話番号が表示されるので以下2点を確認してから「Choose this Number」をクリックします。

  • アメリカの番号かどうか
  • SMS が使えるかどうか

完了すると、取得した電話番号を表示してくれますのでこれも.envにセットしておいてください。

TWILIO_FROM=*******************

⚠ ご注意: 先頭の「+」は不要です

※ なお、この状態は「トライアル」ですので、いくつか制限があります。お気をつけください。

では、最後に取得した番号に「ウェブフック」を設定します。
このウェブフックは、さっき取得した番号にメッセージが届いたらその情報をこれから作成するLaravel側のページに通知してくれるものです。

つまり、流れとしては次のようになります。

  1. 誰かが SMS を送信
  2. Twilio が受信
  3. あらかじめセットしておいたURLに自動でアクセス
  4. Laravel側で送信者の電話番号や情報(今回は店 ID)を取得する

では、ページ左側にあるメニューから「Phone Numbers > Manage > Active numbers」をクリックします。

すると、電話番号の一覧が表示されるので、先ほど取得した番号をクリック。

ページ最後の方の「Messaging > A MESSAGE COMES IN」にこれからアクセスすることになるURLをセットして「Save」ボタンをクリックします。

※ セットするURLは、https://(あなたのドメイン)/sms_report/webhookにしてください。

これで、Twilioの設定は完了です。

パッケージをインストールする

では、実際の作業に移る前にTwilioが用意してくれているパッケージをインストールしておきましょう。

以下のコマンドを実行してください。

composer require twilio/sdk

必要なファイルを用意する

では、ここからはLaravel側の作業になります。
まずは必要になる以下4種類のファイルを作成します。

  • マイグレーション
  • モデル
  • Seeder
  • コントローラー

では、以下のコマンドを実行してください。

まずは「場所」を管理する部分です。
このデータを元にしてQRコードをつくることになります。

php artisan make:model Place -ms

すると、「モデル」「マイグレーション」「Seeder」の3ファイルが作成されます。

そして次に、SMSが送信された報告データを管理する部分です。

php artisan make:model ReportHistory -m

こちらは「モデル」と「マイグレーション」の2ファイルが作成されます。

では、それぞれファイルの設定をしていきましょう。

マイグレーションの設定

では、まずはマイグレーションからです。

database/migrations/****_**_**_******_create_places_table.php

// 省略

public function up()
{
    Schema::create('places', function (Blueprint $table) {
        $table->id();
        $table->string('name')->comment('場所名');
        $table->timestamps();
    });
}

database/migrations/****_**_**_******_create_report_histories_table.php

// 省略

public function up()
{
    Schema::create('report_histories', function (Blueprint $table) {
        $table->id();
        $table->unsignedBigInteger('place_id')->comment('場所ID');
        $table->string('phone_number')->comment('送信者の電話番号');
        $table->timestamps();

        $table->foreign('place_id')->references('id')->on('places');
    });
}

Seeder の設定

次に、テストデータをDBに用意するためのSeederです。

<?php

namespace Database\Seeders;

use App\Models\Place;
use Illuminate\Database\Seeder;

class PlaceSeeder extends Seeder
{
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
        for($i = 1; $i <= 10; $i++) {

            $place = new Place();
            $place->name = 'テスト場所名 '. $i;
            $place->save();

        }
    }
}

Seederはつくっただけでは有効になりませんので、Laravel側へ登録しておきましょう。

database/seeders/DatabaseSeeder.php

public function run()
{
    // 省略

    $this->call(PlaceSeeder::class);
}

モデルの設定

そして、モデルの設定です。

app/Models/Place.php

// 省略

class Place extends Model
{
    use HasFactory;

    protected $appends = [
        'qrcode_contents'
    ];
    
    // Accessor
    public function getQrcodeContentsAttribute()
    {
        $sending_phone_number = env('TWILIO_FROM'); // 本来は config に入れるべきです

        return 'smsto:+'. $sending_phone_number .':'. $this->id;
    }
}

なお、コメントにも書いていますが、env()はコード中に書かずconfig/services.phpの中に書くべきです。キャッシュなどが使えるようになりますので。今回はテストなのでお許しを。m(_ _)m

コントローラーをつくる

では、コントローラーをつくっていきます。
以下のコマンドを実行してください。

php artisan make:controller SmsReportController

app/Http/Controllers/SmsReportController.php

<?php

namespace App\Http\Controllers;

use App\Models\Place;
use App\Models\ReportHistory;
use Illuminate\Http\Request;
use Twilio\Security\RequestValidator;
use Illuminate\Support\Facades\Validator;

class SmsReportController extends Controller
{
    public function place()
    {
        $places = Place::all();

        return view('sms_report.place', compact('places'));
    }

    public function webhook(Request $request)
    {
        $rules = [
            'Body' => ['required', 'exists:places,id']
        ];

        if(Validator::make($request->all(), $rules)->fails()) {

            // ここでエラー処理
            abort(500, 'Place not found..');

        }

        $signature = $request->header('X-Twilio-Signature');
        $url = $request->fullUrl();
        $values = $request->all();
        $auth_token = env('TWILIO_AUTH_TOKEN'); // 本来は config に入れるべきです
        $validator = new RequestValidator($auth_token);

        if($validator->validate($signature, $url, $values)) { // 本当に Twilio からのウェブフックなのかチェック

            $report_history = new ReportHistory();
            $report_history->place_id = $request->Body;
            $report_history->phone_number = $request->From;
            $report_history->save();

            // 実際には、ここで完了メッセージを返すなどする

        } else {

            abort(500, 'Validation failed..');

        }
    }
}

ビューをつくる

先ほどコントローラーの中で指定したビュー「sms_report.place」を作成します。

resources/views/sms_report/place.blade.php

<html>
<head>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.1/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div id="app">
    <div class="container">
        <div class="row">
            <div class="col-3" v-for="place in places">
                <div class="p-5">
                    <div :id="`qrcode-${place.id}`" class="mb-1"></div>
                    <div v-text="place.name"></div>
                </div>
            </div>
        </div>
    </div>
</div>
<script src="https://unpkg.com/vue@3.1.1/dist/vue.global.prod.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.min.js"></script>
<script>

    Vue.createApp({
        data() {
            return {
                places: @json($places)
            }
        },
        mounted() {

            Vue.nextTick(() => {

                this.places.forEach(place => {

                    const selector = `#qrcode-${place.id}`;
                    const target = document.querySelector(selector);

                    new QRCode(target, {
                        text: place.qrcode_contents,
                        width: 128,
                        height: 128,
                        colorDark : "#000000",
                        colorLight : "#ffffff",
                        correctLevel : QRCode.CorrectLevel.H
                    });

                });

            });

        }
    }).mount('#app');

</script>
</body>
</html>

ルートをつくる

では、実際にブラウザからコントローラーへアクセスできるようにルートをつくっておきましょう。

use App\Http\Controllers\SmsReportController;

// 省略

Route::get('sms_report/place', [SmsReportController::class, 'place']);
Route::post('sms_report/webhook', [SmsReportController::class, 'webhook']);

CSRF の設定を変更する

Laravelには、CSRF攻撃を防ぐ機能が初期状態で用意されていますが、これはウェブフックにも適用されることになります。

しかし、そうなるとウェブフックからデータ受信ができなくなってしまうので先ほどつくったウェブフック用のルートを解除します。

app/Http/Middleware/VerifyCsrfToken.php

<?php

namespace App\Http\Middleware;

use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken as Middleware;

class VerifyCsrfToken extends Middleware
{
    /**
     * The URIs that should be excluded from CSRF verification.
     *
     * @var array
     */
    protected $except = [
        'sms_report/webhook', // 👈 ここを追加しました
    ];
}

テストしてみる

では、これまでのコードをサーバーへアップしてテストしましょう。

(ローカルではウェブフックが聞かないのでngrokなどが必要ですが、設定がめんどうなのでサーバーを使いました😂)

では、まずは「https://******/sms_report/place」にブラウザでアクセスしてみましょう。

灰色の部分は消していますが、全てQRコードです。
では、この中から1つ選んでQRコードを読み取ってみましょう。

すると、SMS送信アプリが開きますので、そのまま送信します。

スマホから実行するのはたっだこれだけです。本当にシンプルですよね。キャッチコピーが「たった5秒で完了する」になる理由も分かります。

それでは、ウェブフックが本当に成功しているかデータベースを確認してみましょう。

すると・・・・・・??

はい❗
うまくreport_historiesテーブルにデータが追加されました。

成功です✨😄👍

実際には、もし感染が判明したらこの人のデータを元にして、

  • 近い時間
  • 同じ場所にいた

人たちへ連絡することになります。
お疲れ様でした😄✨

企業様へのご提案

今回のシステムを使うと、「いつ、誰が、どこにいたか」をとても簡単な操作で把握することができるようになります。

そのため、お客様やスタッフの方々の行動を把握し、より理解するために活用できるのではないかと考えています。

なお、応用させますとQRコードは固定にする必要はなく、数秒ごとに変更させることもできますので、「現地到着の報告」などの使いみちにも使えるんじゃないでしょうか。

もしこういった機能をご希望でしたら、いつでもお気軽に「お問い合わせ」からご連絡ください。

お待ちしております。😄✨

開発のご依頼お待ちしております
開発のご依頼はこちらから: お問い合わせ
どうぞよろしくお願いいたします! by 九保すこひ

おわりに

ということで、今回はオードリー・タンさんに尊敬の念を込めて似た機能をつくってみました。

なお、今回はLaravelでつくりましたが、実際に国家レベルで実装する場合はより高速に動くnode.jsとかでつくる方がいいかもしれません。(PHPも相当早くなりましたけどね👍)

GitHub Copilot の感想

そして、今回はじめて本格的に「Github Copilot」を使ってコーディングしてみました。

せっかくなので、最後にその感想をまとめてみたいと思います。

まず、使ってみた感想ですが「すばらしい❗」の一言です。
おおっ、ここまでやるか😳」の連発でした。

そのため、今回のコードにはCopilotの提案したコードが複数入っています。

特に「1ヶ所だけ違うけどほぼ同じコードの繰り返し」の場合は完璧に対応してくれました。

ただし❗気をつけなきゃ、と思ったのは、

Copilotは「プログラムに慣れた人」のもの

ということでした。

つまり、「おしい!」や「違う違う、そうじゃない…🎤」が少しでも入ってくる可能性があるので、あまり手放しで提案コードを信じてはいけないと感じました。(システムは少しでも何かあると問題なので…)

そのため、Copilotの利用が増えれば「うーん、それっぽいからヨシッ!」と現場ネコみたいになりやすいので、今後世の中のコード内にはバグが増える可能性もあるんじゃないかと考えています。(コードではなくて論理的なバグです)

つまり、現状ではCopilotは、「熟練の親方につく、有望な弟子」ぐらいの存在で考えた方がいいかもしれません。
↓ ↓ ↓

親方「おう、なかなか腕上がってきたな👍」
弟子「ありがとうございます😁」

的な。

また、リアルタイムでコードが提案されるので、脳の思考が中断されることになり、「あれ何だったっけ❓」が連発することになったのも意外なデメリットの1つでした。(おそらく慣れれば問題はなくなると思います)

ただ、これから精度があがってきたら開発の世界では「使うのが当たり前」になってもおかしくないと思いました。確実に効率があがると思いますので。

そうなってくると、いつか有料になるかもとも思いましたね。

GitHubはマイクロソフトに買収され、こういったところのマネタイズも考えてたりするのかな、なんて考えてました。

ともかく、Copilotはもう少し使ってみたいと思います。
皆さんもぜひ試してみてくださいね。

ではでは〜❗

「早くコロナ禍が
完全収束しますように!」

このエントリーをはてなブックマークに追加       follow us in feedly