🦧

Dropbox / Slack / GitHub Actionsを駆使してスマホ学習が捗るrelearnという自分専用システムを作った

2022/03/27に公開

先日、たまたまこちらのツイートを拝見して「これ、自分もやりたい!!」と思い、勢いで作ってみました。
https://twitter.com/hyuki/status/1498647009355395072

日々の学びが加速するよう願いを込めてrelearnと名付けることにします。せっかくなのでシステムの紹介と開発のログを残しておこうと思います。

成果物

danimal141/relearn

全体像

ざっくりこんな感じの流れを想定しています。元ツイートではメールと書かれていましたが私は個人のSlackアカウントを持っていたのでそちらに流すようにしてみました。

アーキテクチャ

日常の中でスマホを触っている際、良いと思った投稿や画像のスクリーンショットを撮って個人のDropboxの特定のフォルダにアップロードするようにしておきます。私は個人のDropboxのルートに /assetsというフォルダを用意しそこに蓄積するようにしています。

あとは毎日決まった時間 (現状、22:00 JST)にGitHub Actions経由でrelearnシステムがキックされます。正常に動作するとDropboxからランダムに5枚が選出され、その画像の共有リンクが個人のSlackの特定のチャンネルに投稿されます。私は個人のSlackにrelearnという専用チャンネルを用意しています。

ここでrelearnされた5枚の画像はシェア完了後、Dropbox上の/assets/tmpフォルダに移され、しばらくrelearn対象から外されます。Dropboxの対象フォルダの画像が空になったら /assets/tmpフォルダから/assets直下に画像たちを復活させて再びrelearnがスタートするようになります。

開発

技術選定

まず技術選定ですが、DropboxのSDKを使いたくてこちらのSDKsにある言語のどれかでシュッと作れればいいやと考えました。多少使い慣れている言語を使いたい + 型が欲しいという気持ちがあり、SDKも最低限使えそうだったので今回はTypeScriptを利用することにしました。

環境構築

個人開発とはいえ、以下の仕組みぐらいは最低限入れておきたいです。

  • Linting
  • Formatting

そこで以下の記事を丸パクリ参考にさせていただき環境構築をしました。

ESLintとPrettierまわりの設定で古くなっている部分が一部あったり、huskyがv4ベースだったりで多少調整は必要でしたが問題なく設定できました。huskyに関しては現状最新のv7を採用しています。huskyはv5あたりで若干こじらせていたこともあり身構えていたのですが、v6以降は落ち着きを取り戻した印象です。こちらの記事を参考に設定しました。

Dropboxまわり

DropboxのDevelopersページからアプリケーションを作成する必要があります。アプリケーション作成後、App keyとApp secretが確認できれば一旦OKです。

コンソール上でGenerated access tokenという所からいい感じにアクセストークンが発行できそうな予感がしますが罠です。最近、仕様変更されて短命なアクセストークンしか発行できなくなっているので、すぐに有効期限が切れてまともに利用できないです。有効期限を気にせず使い続けるためにリフレッシュトークンを発行し、そいつを使うようにします。

リフレッシュトークンの取得方法に関してはこちらの記事が参考になりました。記事中ではPythonが使用されているのですが、TypeScriptの場合もSDKがリフレッシュトークンをサポートしてくれています。取得したリフレッシュトークンとApp key (=clientId)、App secret (=clientSecret)を使って、

const dbxClient = new Dropbox({
  refreshToken: xxx,
  clientId: xxx,
  clientSecret: xxx,
});

のようにクライアントを作成してやれば、トークンの有効期限に悩まされることなく利用できました。

あとはAPIドキュメントでフォルダ内のファイル一覧を取得するAPIや共有リンクを生成するAPI、ファイル移動するAPIなどをそれぞれ探して想定する動きを実装していきます。APIドキュメントとtypesファイルを確認すれば、どのメソッドが使えそうかあまり悩まず見つけられると思います。

Slackまわり

Dropboxの共有リンクを受け取ってSlackのチャンネルに投稿する部分ですが、こちらに書かれているような感じでIncoming Webhookを利用しました。Slack Appの作成をしてWebhook用のURLを取得するだけでOKです。

SlackもNodeのSDKがあり、@slack/webhookを使えば問題なくSlackへの投稿が実現できそうでした。

実装

今回やりたいことはざっくり以下のような感じです。

  • Dropboxの/assetsフォルダ上の画像ファイルの取得
  • 上記画像ファイルの共有リンク生成
  • 上記共有リンクをSlackに投稿
  • 共有した画像ファイルを/assets/tmpフォルダに移動
  • /assetsフォルダ上に共有できる画像ファイルがない場合は/assets/tmpフォルダから一部を復元してrelearnを再開する

特にDropboxまわりは細かい処理が多かったので、Dropbox、SlackそれぞれのSDKをラップするクラスをAdapterとして用意しました。そしてデザインパターンでいうFacadeのような役割をするクラスExecutorを用意して、そいつが各Adapterを使ってメインの処理を呼び出すようにしました。

import DbxAdapter from "../dropbox/adapter";
import SlackAdapter from "../slack/adapter";

export default class Executor {
  private dbxAdapter: DbxAdapter;
  private slackAdapter: SlackAdapter;

  constructor(dbxAdapter: DbxAdapter, slackAdapter: SlackAdapter) {
    this.dbxAdapter = dbxAdapter;
    this.slackAdapter = slackAdapter;
  }

  public async relearn(): Promise<number> {
    const paths = await this.dbxAdapter.getTargetPaths();
    if (paths.length === 0) {
      // There is no target which we can relearn
      // Revive assets
      const status = await this.dbxAdapter.reviveSharedFiles();
      // Try relearning next time
      return status;
    }

    const links = await this.dbxAdapter.getSharedLinks(paths);

    // Send shared links to Slack
    for await (const link of links) {
      await this.slackAdapter.send(link);
    }

    // Evacuate the shared files
    const status = await this.dbxAdapter.evacuateRelearnedFiles(paths);
    return status;
  }
}

まあ偉そうに書いておいてテストすらまともに書いていないんですが...

エントリポイントからExecutor経由でメインの処理が呼ばれます。イメージはこんな感じです。

import { Dropbox } from "dropbox";

import DbxAdapter from "./dropbox/adapter";
import SlackAdapter from "./slack/adapter";
import Executor from "./relearn/executor";

(async () => {
  // 省略
  const slackAdapter = new SlackAdapter(xxx);
  const dbxAdapter = new DbxAdapter(xxx);
  const executor = new Executor(dbxAdapter, slackAdapter);

  const status = await executor.relearn();
  console.log(status);
})();

あとはこのエントリポイントがGitHub Actionsから定期的に叩かれるようになれば完成です。

基本的に

on:
  schedule:
    - cron: '0 13 * * *'

のようにスケジュールの設定をしておけばよいのですが、DropboxやSlack関連で必要な環境変数を読み込んでやる必要があります。

環境変数はGitHub画面上のActions secrets (settings > secrets > actions)からセットしておき、GitHub Actions側から以下のように利用します。

env:
  DBX_CLIENT_ID: ${{ secrets.DBX_CLIENT_ID }}
  DBX_CLIENT_SECRET: ${{ secrets.DBX_CLIENT_SECRET }}
  DBX_REFRESH_TOKEN: ${{ secrets.DBX_REFRESH_TOKEN }}
  DBX_TARGET_PATH: ${{ secrets.DBX_TARGET_PATH }}
  SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}

必要な環境変数は.env.templateに定義されています。ローカル開発を行う場合はローカルに.envを用意して必要な環境変数を記載してください。

以上で毎日relearnのシステムが定期実行され、個人のSlackチャンネルにランダムにスクショが投稿されてくるようになりました。こちらを見る限り、無料枠でも2000分/月利用できそうなのでまあ課金の必要もなかろうと思っていますw

まとめ

実際にやってみると分かるのですが、シンプルな割に結構役に立っています。実運用としてはただ日常スマホでスクショを撮るだけなので継続も苦ではないです (自分はiPhoneの背面タップでスクショを撮れるように設定しています)。

  • Twitterやはてブを定期的に徘徊する
  • 良さげな投稿を背面タップでスクショ => Dropboxにアップロード
  • 毎日勝手にrelearn経由で再学習を強制される

みたいな学習サイクルが自然と回ります。

danimal141/relearnをForkしてもらって、環境変数まわりや定数部分だけいじってもらえれば気軽にカスタマイズして使っていただけると思うので、ぜひお試しください。

Discussion