忘却曲線を底上げするタスクを自動で登録する機能

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

さてさて、私は長年エンジニアとして活動をしてきたわけですが、それとは別に今の時代の人が「少なくとも基本は知っておいたほうがいい」知識があると考えてます。

それが・・・・・・

マーケティング知識

です。

正直なところ、私は少し前にLTVLife Time Value = お客さんが生涯いくら使うか)を知ったりと、マーケティングはまったくの素人です。

なので、YouTubeでいい動画を探していたところ「令和の虎 🐯」に登場する社長さんが公開しているすばらしいものをいくつか発見し「これはぜひ繰り返し見よう❗」と考えました。

※ この動画ホント有料級です(ステマじゃないです。念のため😄)

しかし、そのときある単語も思い出しました。

それは・・・・・・

忘却曲線

です。

簡単に言うと、「何分、何時間、何日たったらどれぐらい忘れちゃうの❓」がわかるものなのですが、だったらこれを逆手に取って「以下のように Google タスクを登録しておけば完璧じゃん❗」と考えたんですね。

  • 翌日(1日後)にもう一回復習
  • 1週間後(7日後)にもう一回復習
  • 2週間後(7日後)にもう一回復習
  • 1ヶ月後(30日後)にもう一回復習
  • 2ヶ月後(60日後)にもう一回復習

※ もちろん個人差はあると思います。

そこで❗

今回はこの「忘却曲線を底上げするタスクの登録」ができる機能をつくってみます。(・・・というのも、Google タスクは複製ができないようで、一個一個登録するのがめんどうだったんですね😂)

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

「↑↑↑ インフルエンザに感染したときの
食事はまさに
こんなカンジでした😫」

開発環境: Laravel 10.x、PHP 8.2

やりたいこと

正直なところ、Google Cloudでサービスアカウントを作ればシンプルに実装できるんでしょうが、今回は「誰でもログインすれば Google タスクへアクセスできる」ようにしたかったので、あえてOAuthで実装しました。(結果、時間がめちゃくちゃかかりました…😂)

では、今回も楽しんでやっていきましょう❗

Google Cloud で OAuth が使えるようにする

プロジェクトをつくる

Google Cloudにログインしてまずは専用のプロジェクトをつくります。

ページ左上にあるプロジェクトが表示されているボタンをクリックしてください。

すると、モーダルが表示されるので右上にある「新しいプロジェクト」をクリック。

プロジェクト名を適当に入力し「作成」ボタンをクリックします。

すると、新しいプロジェクトが作成されます(10秒ほど時間がかかるかもです)ので、再度ページ左上にあるプロジェクトから選択してください。

Google Tasks API を有効にする

では、今回使うGoogle Tasks APIを有効にしておきます。

ページ左メニューから「API とサービス > 有効な API とサービス」をクリック。

検索ボックスに「tasks」と入力し、エンターキーを押す。

すると、検索結果が表示されるので、「Google Tasks API」をクリック。

「有効にする」ボタンをクリックしてください。

OAuth 同意画面をつくる

では、次にOAuth2を利用するための設定をしていきます。

API とサービス > OAuth 同意画面」へ移動します。

すると以下のようなフォームが表示されるので、「外部」を選択して作成してください。

さらに以下のようなフォームが表示されるので、必要な項目を入力して保存します。

そして次はスコープです。
今回はCloud Tasks APIで制限をかけます。

スコープを追加または削除」ボタンをクリックします。

すると、ページ右側にスコープの一覧が表示されるのでその中のスコープの「手動追加」に「https://www.googleapis.com/auth/tasks」を入力し「テーブルに追加」ボタンをクリックします。

テーブルに表示されるようになるので、チェックを入れて更新ボタンをクリックしてください。

なお、その他のスコープの場合は以下から確認できます。

📝 参考ページ: Google API の OAuth 2.0 スコープ

では、先ほどと同じく保存ボタンをクリック。

次にテストユーザーです。
+ ADD USERS」ボタンをクリックします。

すると、ページ右側にテストユーザーを入力フォームが表示されるので、ご自身のGMailアドレスを入力して追加してください。

そして、また保存ボタンをクリック。

これでOAuth同意画面の作業は完了です。

※ なお、この時点でテスト環境となっているので本番として公開する場合はアプリを公開する設定が必要になります。(今回は省略します)

認証情報をつくる

そして、次に認証情報ページへ移動します。

ページ移動するので、「+ 認証情報を作成 > OAuth クライアント ID」をクリックします。

ページ移動した先で「アプリケーションの種類」を「ウェブ アプリケーション」にし、その他必要な情報を入力します。

なお、ここで少し厄介なのが「承認済みのリダイレクト URI」です。

というのも、私はよく開発環境として「brushup.test」のようなドメインを使うのですが、これは受け付けてくれません。(.comなどのようなメジャーなドメインでないといけません)

そのため、こういったケースでは「brushup-test.com」というようなドメインをhostsファイルに登録し、ブラウザのアクセスを強制的にローカルに向かうようにして開発をしています。

※ 私の環境はLinuxなので/etc/hostsへ登録する形になります。

これでOAuthのクライアントも作成できました。

では、APIアクセスに必要な以下2つのキーを取得しておきましょう。

  • クライアントID
  • クライアントシークレット

一覧ページに戻ったら、登録したクライアントをクリックします。

すると、以下のように2つのキーがあります。

2つのキーは忘れないように.envへ登録しておきましょう。

.env

# Google Cloud OAuth
GOOGLE_CLIENT_ID=***************
GOOGLE_CLIENT_SECRET=***************

また、config()を使ってアクセスしたいので、services.phpへ登録しておきましょう。

config/services.php

<?php

return [

    // 省略

    'google' => [
        'client_id' => env('GOOGLE_CLIENT_ID'),
        'client_secret' => env('GOOGLE_CLIENT_SECRET'),
    ],

];

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

では、composerGoogle Cloudが用意してくれているパッケージをインストールしておきます。

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

composer require google/apiclient

Authorization code を受け取るリダイレクト用URLをつくる

では、続いてOAuth2認証の際に「一旦移動した Google から返ってくるページ」をつくります。

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

php artisan make:controller OAuthCallbackController

すると、ファイルが作成されるので中身を以下のようにします。

app/Http/Controllers/OAuthCallbackController.php

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Str;

class OAuthCallbackController extends Controller
{
    public function __construct()
    {
        $this->client_id = config('services.google.client_id');
        $this->client_secret = config('services.google.client_secret');
    }

    public function receive(Request $request)
    {
        $refresh_token = $this->getRefreshTokenByAuthorizationCode($request->code);

        if(Str::length($refresh_token) === 0) {

            abort(500, 'Failed to get refresh token.');

        }

        $request->session()->put('refresh_token', $refresh_token);

        return redirect()->route('home.index');
    }

    private function getRefreshTokenByAuthorizationCode(string $authorization_code): string
    {
        $url = 'https://oauth2.googleapis.com/token';
        $redirect_uri = route('oauth.callback.receive');
        $params = [
            'code' => $authorization_code,
            'grant_type' => 'authorization_code',
            'redirect_uri' => $redirect_uri,
            'client_id' => $this->client_id,
            'client_secret' => $this->client_secret,
        ];

        try {

            $response = Http::asForm()->post($url, $params);
            $response_data = $response->json();

        } catch (\Exception $e) {

            throw $e;

        }

        return $response_data['refresh_token'] ?? '';
    }
}

タスクを登録する部分をつくる

では、タスクを登録する(メインの)部分です。

なお、今回はログイン機能は不要にしたいので、「アクセストークン」の取得に必要な「リフレッシュトークン」はsessionで管理するようにします(危険なのでブラウザ側へは一切出しません!)

以下のコマンドを実行してコントローラーを作成してください。

php artisan make:controller HomeController

するとファイルが作成されるので、中身を以下のようにします。

app/Http/Controllers/HomeController.php

<?php

namespace App\Http\Controllers;

use Carbon\Carbon;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Str;
use Google_Client;
use Google_Service_Tasks;
use Google_Service_Tasks_Task;

class HomeController extends Controller
{
    public function __construct()
    {
        $this->client_id = config('services.google.client_id');
        $this->client_secret = config('services.google.client_secret');
    }

    public function index(Request $request)
    {
        $refresh_token = $request->session()->get('refresh_token');
        $access_token = $this->getAccessToken($refresh_token);

        if(Str::length($access_token) === 0) { // Google で認証が必要な場合は、リダイレクト

            $redirect_url = route('oauth.callback.receive');
            $callback_url = 'https://accounts.google.com/o/oauth2/v2/auth?'.
                'scope='. urlencode(Google_Service_Tasks::TASKS) .'&'.
                'prompt=consent&'.
                'access_type=offline&'.
                'include_granted_scopes=true&'.
                'response_type=code&'.
                'state=state_parameter_passthrough_value&'.
                'redirect_uri='. urlencode($redirect_url) .'&'.
                'client_id='. $this->client_id;
            return redirect($callback_url);

        }

        return view('home.index'); // 注意: セキュリティの観点から、アクセストークンはビューに渡さない
    }

    public function store(Request $request)
    {
        // 注意: バリデーションは省略しています

        $refresh_token = $request->session()->get('refresh_token');
        $access_token = $this->getAccessToken($refresh_token);

        $client = new Google_Client();
        $client->setAccessToken($access_token);
        $client->setScopes(Google_Service_Tasks::TASKS);
        $service = new Google_Service_Tasks($client);

        $title = $request->title;
        $base_dt = Carbon::parse($request->base_date_time);
        $adding_days = [
            1, // 翌日
            7, // 1週間後
            14, // 2週間後
            30, // 1ヶ月後
            60, // 2ヶ月後
        ];

        foreach ($adding_days as $index => $adding_day) {

            $no = $index + 1;
            $task_dt = $base_dt
                ->copy()
                ->addDays($adding_day);
            $note = $no .'回目のタスク(全'. count($adding_days) .'回)';

            $new_task = new Google_Service_Tasks_Task();
            $new_task->setTitle($title);
            $new_task->setNotes($note);
            $new_task->setDue($task_dt);

            try {

                $service->tasks->insert('@default', $new_task);

            } catch (\Exception $e) {

                throw $e;

            }

            sleep(1); // 1秒待つ

        }

        return redirect()->route('home.index')->with('success_message', 'タスクを追加しました');
    }

    private function getAccessToken(?string $refresh_token): string
    {
        if(Str::length($refresh_token) === 0) {

            return '';

        }

        $url = 'https://oauth2.googleapis.com/token';
        $response = Http::asForm()
            ->post($url, [
                'client_id' => $this->client_id,
                'client_secret' => $this->client_secret,
                'grant_type' => 'refresh_token',
                'refresh_token' => $refresh_token,
            ]);

        return $response->json('access_token', '');
    }
}

※ ちなみに、直接Httpクライアントで送信している部分がありますが、パッケージに入ってるかもです。(ドキュメントどこにあるのかな?🤔)

ビューをつくる

では、先ほどのコントローラーの中で利用していたビューをつくっていきます。
以下のコマンドを実行してください。

php artisan make:view home.index

するとファイルが作成されるので中身を以下のように変更します。

resources/views/home/index.blade.php

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>忘却曲線を底上げするタスク登録</title>
    <link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
</head>
<body class="bg-gray-100">
<div id="app" class="flex flex-col items-center justify-center min-h-screen p-4">
    <div class="w-full max-w-md bg-white rounded-lg shadow-md p-6">
        <!-- フラッシュメッセージ -->
        @if (session('success_message'))
            <div class="bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded relative mb-6" role="alert">
                <strong class="font-bold">成功!</strong>
                <span class="block sm:inline">{{ session('success_message') }}</span>
            </div>
        @endif
        <h1 class="text-2xl font-bold text-center text-gray-700 mb-6">&#x1F3C3; 忘却曲線を底上げするタスク登録</h1>
        <form method="POST" action="{{ route('home.store') }}">
            @csrf
            <div class="mb-4">
                <label for="title" class="block text-gray-700 text-sm font-bold mb-2">タイトル</label>
                <input type="text" id="title" name="title" class="block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring focus:border-blue-300">
            </div>
            <div class="mb-4">
                <label for="dueDate" class="block text-gray-700 text-sm font-bold mb-2">基準になる日時(最初に勉強した日時)</label>
                <input type="datetime-local" id="dueDate" name="base_date_time" class="block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring focus:border-blue-300">
            </div>
            <div class="mb-6 p-3 bg-blue-100 rounded">
                <p class="text-sm text-gray-700">登録されるタスクは、以下のとおりです:</p>
                <ul class="list-disc list-inside text-sm mt-2">
                    <li>翌日</li>
                    <li>1週間後</li>
                    <li>2週間後</li>
                    <li>1ヵ月後</li>
                    <li>2ヵ月後</li>
                </ul>
            </div>
            <button type="submit" class="w-full bg-blue-500 hover:bg-blue-600 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline">タスク登録</button>
        </form>
    </div>
</div>
</body>
</html>

テストしてみる

では、実際にテストしてみましょう❗

まずブラウザで(テスト用に登録した)「https://brushup-test.com」へアクセスします。(👈 繰り返しになりますが、hostsでローカル環境にアクセスすることになります)

すると・・・・・・

自動でリダイレクトされて、以下のようなアカウントを選択するページになります。(ログインしていない場合はログインフォームかもしれません)

そして、テストユーザーとして登録したものを選択すると・・・・・・

アプリがまだテスト段階のため、確認が出ますが、続行&続行ボタンでOKです。

すると、またしても自動でリダイレクトして今回のフォームが表示されますので、中身を以下のように入力して送信してみましょう。

うまくいくでしょうか・・・・・・

はい❗
少し時間がかかりましたが、うまく完了メッセージが表示されました。

では、ホントに「Google タスク」に登録されているかチェックしてみましょう。(私はいつも「Googleカレンダー」でタスクを見ています)

どうなるでしょうか・・・・・・

はい❗

他にもいろいろとタスクがあるので省略していますが、確かに上記のようなタスクが5つ登録されていました。

すべて成功です😄✨

企業様へのご提案

今回の重要な点は、OAuth2を使ってGoogleタスクにアクセスしているところです。

つまり、ひとつのアカウントだけでしか使えないわけでなく、ログインをした人、各個人の「Googleサービス」へアクセスができるというわけです。

そのため、何かアイデアがあればウェブサイトへ公開し(もちろん事前の説明などは必要ですが)必要なデータを貯めていくというシステムをつくることができます。

もしそういった内容をご希望でしたらいつでもお気軽にご相談ください。

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

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

おわりに

ということで、今回は「Googleタスク」と忘却曲線をからめた機能をつくってみました。

実際使ってみたところ、便利だったので自分用に使うか、それこそもっと「ブラッシュアップ」してウェブ上に公開しようかなと考えています。

それにしても、姪っ子から感染したインフルエンザがこんなに強力だとはおもいませんでした(笑)

常に深酒してるようなボヤケた景色、必ずたちくらみする高熱、指すような喉の痛み、そして初体験の全身筋肉痛・・・

控えめに言ってこの状態でブログを書いたのは自分を褒めてやりたいです。

みなさん、体だけはお気をつけください。

ではでは〜❗

「2人でマリオやりすぎたね😂」

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