ハトネコエ Web がくしゅうちょう

プログラミングやサーバー・Web制作、チームマネジメントなど得た技術のまとめ

rubocop-rails で to_time が怒られなかったので調査した

1. オプションで指定しない限り to_time は怒られない

コードレビューをしていたら

"2020-01-01 10:00".to_time

のようなコードがあり、
「そこは .in_time_zone の方がいいですよ」とコメントしようとして、「あれ?」と思いました。

なんで「 Time.nowTime.zone.now にしなさい」と
いつもは怒ってくれる rubocop-rails が反応してくれていないのでしょう?

そこでドキュメントを確認すると、
Rails/DateAllowToTimefalse の設定に変更しない限り、
to_time の使用を怒ることはない
とわかります。

2. to_time を使うことの問題

これは少し不思議です。
to_time を使ってしまっては、 config.time_zone で設定している
Railsアプリケーション側のタイムゾーンでなく、システム側(OS側)のタイムゾーンを見に行ってしまいます。

root@9f373dcf6715:/app# export TZ=America/Los_Angeles
root@9f373dcf6715:/app# bundle exec rails c

[1] pry(main)> Time.now
=> 2022-06-15 08:23:08.607609413 -0700
[2] pry(main)> Time.zone.now
=> Thu, 16 Jun 2022 00:23:16.122905467 JST +09:00

[3] pry(main)> "2020-06-01 10:00".to_time
=> 2020-06-01 10:00:00 -0700
[4] pry(main)> "2020-06-01 10:00".in_time_zone
=> Mon, 01 Jun 2020 10:00:00.000000000 JST +09:00

[5] pry(main)> Date.current.to_time
=> 2022-06-16 00:00:00 -0700
[6] pry(main)> Date.current.in_time_zone
=> Thu, 16 Jun 2022 00:00:00.000000000 JST +09:00

Rails/Date の cop は、
Date.today でなく Date.current を使え」だとか、
Time.now でなく Time.zone.now を使え」だとか、
システム側のタイムゾーンでなくアプリケーション側のタイムゾーンを使うよう統一を求めるものです。

であれば、 to_time は使うべきでないはずです。

3. 原因は DateTime にあり

ここでドキュメントに戻りましょう。

AllowToTime is true by default to prevent false positive on DateTime object.

とあります。
DateTimeオブジェクトにおける誤検知を防ぐため」……? どういうことでしょう。

その答えはこちらの issue にあります。

https://github.com/rubocop/rubocop-rails/issues/288

要約すると、DateTimeクラスのときに、
すでにアプリケーション側のタイムゾーン情報を持つことが出来て、
そのタイムゾーン情報は to_time しても引き継がれるから問題ないはずだ、という主張です。

[1] pry(main)> DateTime.current
=> Thu, 16 Jun 2022 00:50:18 +0900
[2] pry(main)> DateTime.current.to_time
=> 2022-06-16 00:50:23.70027614 +0900

なるほど、たしかにこのことを考えると、
to_time メソッドの使用を必ず怒るという挙動だと、余計なおせっかいになってしまうことがありますね。

# ちなみに DateTime.now はシステム側のタイムゾーンを使ってしまう
[3] pry(main)> DateTime.now
=> Wed, 15 Jun 2022 08:50:45 -0700
[4] pry(main)> DateTime.now.to_time
=> 2022-06-15 08:50:52.038531225 -0700

4. しかし DateTime は deprecated

……なのですが、2020年9月の Ruby のコミット
https://github.com/ruby/date/commit/58ca6e6a3ee20c72a77266e0f74920b12a06ee9d にて、
DateTime は deprecated(非推奨) になりました。

話の発端: https://bugs.ruby-lang.org/issues/15712

というわけなので、もう AllowToTimefalse をデフォルトにしてもいいのではと思いまして、
issue を作っておきました。
https://github.com/rubocop/rubocop-rails/issues/715

すぐには変わらないと思うので、
ひとまずは .rubocop.yml の設定を付け足すことで対応したいと思います。

原因がわかってスッキリしました。

QQEnglishのレッスン予約完了メールをGoogleカレンダーに反映させる【Google Apps Script】

オンライン英会話スクール QQEnglish を始めてみました。
始めてみて困ったのが、予約したレッスンを Google カレンダーに登録できる機能がないこと!

Google カレンダー登録用リンク、
かんたんに置けるはずなので置いてほしいところですが、ないものは仕方ありません!
自分で用意しましょう!!

1. まずは出来上がったコードから

Gmail で来たメールに沿って Google カレンダーに登録する、というのは
多くの人がやっているようで、私は以下の記事を参考にしました。

GASを使ってGmailにきた予定をGoogle Calendarに自動で登録する | AABrain

これをベースにしながら、以下のような Google Apps Script を書いてみました。

GmailGoogle カレンダーの権限が必要なので、
トリガーを設定する前に、まずは一度 Google Apps Script エディターから手動実行をして権限の許可 をするようお願いします。

【QQEnglish のレッスン予約完了メールを元に、Googleカレンダーのスケジュールを作成】

2. 指定した文字列を含む件名のメールを抽出

ポイントは

const targetHours = 1;
const subject = "【QQEnglish】レッスン予約完了";
const query = (`newer_than:${targetHours}h subject:"${subject}"`);
const threads = GmailApp.search(query);

のところでしょうか。

Gmail の検索で特定のメールを引っ張ってくる必要があるので、 件名を指定して取得します。
ただ、予約完了のメールの件名が変わる可能性が将来的にあるのがつらいところ……。

newer_than:1h で、1時間以内のメールのみ取得するようにしています。
ここは後述する重複スケジュール作成の防止があるので、もっと長い時間にしても大丈夫です。

クエリには他にもいろいろなものがあります。 ↓
https://support.google.com/mail/answer/7190?hl=ja
from:noreply@notify.qqeng.com などを加えてもいいかもしれませんね。

const threads = GmailApp.search(query);
const messagesEachThreads = GmailApp.getMessagesForThreads(threads);

messagesEachThreads.forEach(messages => {
  messages.forEach(message => {
    const messageSubject = message.getSubject();
    if (!messageSubject.includes(subject)) return;

GmailApp.search で取得できるものはスレッド一覧なので、
そこからさらに、スレッドごとのメール一覧を取得してきて、
各メールの件名をチェックするというところまでやって初めて、
一致する件名のメールを取得できるという仕組み
に注意が必要です。

3. Google カレンダーにスケジュール登録

GASを使ってGmailにきた予定をGoogle Calendarに自動で登録する | AABrain の記事にあるコードを使わせてもらいつつ、
場所の設定は必要ないので location のオプションは削除しつつ、
同じ時間に同じタイトルのスケジュールがすでにあるなら作らない ようにしました。

これがなんであるかというと、 Gmail がレッスン予約のメールを同じスレッドに入れてしまうことがあるためです。

私の場合、4/19 21:00 と 4/20 8:00 に送られた予約完了メールが同じスレッドに入っていました。
そうなると、4/20 8:00 のメールだけ見てほしいのに、4/19 21:00 のメールも見てしまって、一度作ったスケジュールがもう一度作られてしまいます。
それを防ぐため、重複登録の防止処理を入れています。

4. 便利になりました!

これで、レッスン予約をすると勝手に Google カレンダーに予定登録されるようになりました! とっても便利です。
問題は、メールの件名や文章が変わるとこのコードが動かなくなることですね……。

レッスンの開始時間や先生の名前などは、メールの文章から正規表現で取ってきているので……。
限界があるので、QQEnglish さんが機能追加してくれるのが一番ありがたいですね!

esa から Notion へ API を駆使して移行した話

esa から Notion へと死ぬほどがんばって移行したので、その記録を残しておきます。

esa にあった画像やコメントなども、API を駆使して移行しています。

1. 前置き

後述しますが、このやり方は完璧でない部分があります。
一部のデータが消えるという覚悟が必要です。

また、Notion は多機能で便利ですが、
esa に比べると検索機能はやや弱いです。

多機能ゆえ、TODOリストなども統合する都合でページ数が多くなり、
結果、求めるページが検索結果から見つけづらかったり、検索結果に出てこないことがあります。

書き心地や情報の探しやすさで言えば esa に軍配が上がると個人的には思います。
(あと、今回の移行で思ったけど esaAPI が親切。Notion API はベータ版だったってこともあるけど出来ること少なすぎてストレスフルだった)

移行・検証期間は長めに取ると良いでしょう。
Notion が内部的にはマークダウンじゃない影響で、他のドキュメントツールへの移行作業は難しそうに感じますし。

2. Notion のマークダウンインポートの罠

Notion には Import 機能があります。

f:id:nekonenene:20220314235002p:plain
Notion の Import 機能

esa には Markdown エクスポート機能があります。

f:id:nekonenene:20220314235126p:plain
esa のエクスポート機能

つまり、この2つを合わせれば、簡単にインポートできる!!!

・・・と思いますよね?

私もそう思っていました。
ところがそう上手くはいきません。

罠1. フォルダ単位でしかインポートできない

これは実際に Notion の Import 機能を使ってみると気付くことなのですが、
インポートするマークダウンファイルはフォルダ単位でしか選べません

1フォルダを選んで、再帰的にディレクトリを掘っていって取り込む、なんてことは出来ません。
エクスポートした esa のフォルダ構成は、元の階層通りになっているので、そのファイルすべてを取り込んでいくのは至難の業(わざ)です。

罠2. GitHub Flavored Markdown を解釈しない

esa は GFM こと GitHub Flavored Markdown にほぼ準拠で書けます。
なので、改行のために文末にスペースを2つ置く必要はありませんし、
URLをそのまま記載してもリンクになってくれます。

しかし、Notion の Import 機能では残念ながら GitHub Flavored Markdown として解釈しないので、
esa のときにあった改行は消えてしまうし、URL文字列はリンクじゃなくなってしまいます
コードブロックの言語設定も消えます。

罠3. テーブルの中の画像やリンクが消える

これは今回の移行作業後に気付いたのですが、
テーブルの中に画像が貼り付けてある場合、消えて、ただの空欄になります

Notion の Import 機能で、テーブルは Notion で言うところの「データベース」というものに変換されるのですが、
これへの変換が癖のある感じで、画像が消えたり文字へのリンクが消えたり残念なことになります……困る……。

罠4. Notion は見出し3までしかない

Notion は見出しが3までしかありません。
ですので、esa での ### , #### , ##### の見出しはインポート後、全て同じ大きさの見出しになります。

人によってはマークダウンで書く記事の見出しを ##### から書き始めるので、
そういう人の記事は移行後にやや残念な感じになります。
まあ、上3つの罠に比べれば些細(ささい)なことですね。

他には、箇条書きの内側に記されたコードブロックが正しくパースされないという細かなバグもあります。

3. HTML インポートを使おう

そんなこんなで、esa から Notion への移行は、想像ほど簡単におこなえません……。

罠1と罠2は致命的です。
(罠3も致命的ですが、これを回避するには Notion のインポート機能を使わずに API を介してページを作っていくしかなさそう)

これらの問題を解消するために esa-dumper-for-notion というものを書きました! ↓

READMEを読めばなんとなくわかると思いますが、
罠1と罠2の問題を解消するために、
1フォルダにすべての記事のHTMLファイルを書き出すという処理をおこなっています。

上部に esa の記事IDや作成者・作成時刻などを書きつつ、
下部にはコメント一覧を書き出してくれます。

これがおこなえたのは、
esaAPI が、記事やコメントの Markdown だけでなく、レンダリング時の HTML まで返してくれるからです。esaAPI 素晴らしいです!

HTMLファイルで書き出したあとにインポートすれば、
esa で改行されていた箇所やリンクがしっかりと再現されます。

f:id:nekonenene:20220315004350p:plain
Notion の Import 機能(HTMLでインポートする)

なお、5000以上ある記事を一度にインポートしようとしたら途中でエラーを吐いたので、
2回に分けてインポートしました。

4. notion-py をフル活用する

『Notion(非公開)APIで画像をアップロードする』API%E3%81%A7%E7%94%BB%E5%83%8F%E3%82%92%E3%82%A2%E3%83%83%E3%83%97%E3%83%AD%E3%83%BC%E3%83%89%E3%81%99%E3%82%8B)の記事を書いた neneka さんのフォークした notion-py を利用しました。

Notion API の公式ライブラリとしては notion-sdk-js があるのですが、
Notion API (2022/03/03 にようやくベータ版から正式版になった)では Notion への画像アップロードがおこなえません

esa にアップロードされていた画像を Notion にアップロードし直さないと、
esa の解約時に画像が消えてしまいます。

裏道的な手段で画像アップロードをおこなう notion-py があるおかげでギリギリなんとかなりました。

インポートした記事たちを加工する esa-to-notion-py がこちらです! ↓

このアプリケーションによって、

  • esa にアップロードされた画像を Notion に上げ直し
  • リンクURLを esa から Notion のものに変換
    (※移行後に気付いたのだけど、テーブル内にあるリンクURLを変換できていない)
  • 3. の手順で1フォルダにしてからインポートしたので、階層構造を再び作成する
    (※一度おこなうとやり直しが効かないので注意)

の3つをおこなうことができます。

記事数によりますが、時間はけっこうかかります
私はサーバーを建てて nohup で実行させました。

nohup python esa-image-to-notion 1234567890abcdef1234567890abcdef > esa-image-to-notion.log 2> error.log &

みたいな。

5. 今回の対応で移行できなかった点

今までに記載したことの繰り返しも含めて、移行できなかったポイントをまとめます。

  • テーブル内のアレコレ
    • テーブル内の画像が消えます
    • テーブル内の文字列に対するリンクがリンクでなくなる場合があります
    • 考慮不足により、テーブル内にある esa の記事URLを Notion の記事URLに書き換えることが esa-to-notion-py でおこなえません
    • 『罠3. テーブルの中の画像やリンクが消える』の項で記載しましたが、テーブルの移行はデータベースに変換されるぶん、つらみが多い。より普通のテーブルに近いシンプルテーブルに関しても縛りが多すぎるし……
  • esa にアップロードした画像以外のファイル
    • notion-py でアップロードできない関係で、画像以外のPDFやzipなどは移行できていません
  • コードブロックの言語設定

他にもなにかあったかもしれませんが、
単純に Markdown インポートをおこなうよりはだいぶいい感じになりました。

6. おわりに

めっっっちゃしんどかった!!!

事前調査では 2. で書いたように「マークダウンのインポートできるんだ、楽勝じゃ〜〜ん」って思っていたので、
そのぶん、上手く行かないところが次々と発覚していったときの絶望がヤバかったです。

Notion は世界中で使われてるから API もしっかりしてるでしょ〜、って思っていたら
驚くほど使いにくくて、Notion API を使って記事を書くのはそうとうしんどそうに感じました。

2022/03/03 に正式版になってから多少改善が入ったそうなので、自分が見たときよりは少しマシかな、と思っていま見てみましたが、やっぱり厳しそうです……。
Paragraph block にマークダウンテキストを渡したら解釈してくれれば少し楽なんだけど、Rich text object を書かなくちゃいけないのはつらい……。

Notion がファイル保管庫として使われるのを避けるために永遠に搭載されないかもだけど、
画像やファイルをアップロードするAPIエンドポイントも欲しいなあ……。
Notion API が今後良くなっていくことに期待したいです。