SNS 上の誹謗中傷を証拠として保全する Web サービスを作っています。
中核の機能は1つです。検知した投稿の、実物のスクリーンショットを自動で撮る。
ここは代替がききませんでした。API から取れたテキストを整形して「証拠カード」の画像を生成する案も作ったのですが、それは証拠にならないという判断で捨てました。あとから自由に書ける画像は、何も証明しません。
結論を先に書きます。第三者のラッパーはいつか死にます。そして死んだとき、失敗は例外ではなく「それらしい画像」として返ってきます。
最初の方式は、相手側に拒否された
Cloudflare の Browser Rendering で撮る、から始めました。配線は完成しました。撮れませんでした。
- X はヘッドレスブラウザをブロックする。 タイムアウトで終わる
- YouTube は Trusted Types の CSP でスクリプト注入を拒否する。 コメントを描画させる手段が無い
どちらも「こちらの実装が悪い」のではなく、相手がそう作っているものです。ここで Cloudflare 単独では不可能と診断して、実ブラウザ+ボット回避を持っているサービス(ScreenshotOne)に切り替えました。
これで両方撮れるようになりました。X は投稿ページを直接開いてツイート要素を切り出す。YouTube は &lc= 付きの URL を開いて、該当コメントの要素だけを撮る。
要素スクショにはひとつ罠があって、selector_algorithm=clip は fold より下にある要素だと空画像を返します。 セレクタは合っているのに中身が無い、という形で失敗するので、最初は原因が分かりませんでした。
ytd-comment-thread-renderer:has(a[href*="lc=ID"])
動いていたパラメータが、ある日 400 を返し始めた
取得日時を日本時間で表示させたくて、time_zone: Asia/Tokyo を付けていました。
ある日から全リクエストが 400 になりました。 撮影が全滅しました。
サービス側が対応タイムゾーンを限定したためでした。こちらは何も変えていません。
診断が即座にできたのは、エラー本文を DB に記録していたからでした。rawPayload.screenshotError に生のレスポンスを入れてあったので、開けば理由が書いてありました。これが無ければ「なぜか撮れない」から始めることになります。
対処は、そのパラメータを外すこと。表示は GMT のままにして、法的に重要な「取得日時」は自分側で JST として記録する形に変えました。表示は相手のサービスの都合で変わりますが、記録は変わりません。
いちばん悪かったのはこれ
X の撮影に、twitframe という第三者の埋め込みラッパーを使っていました。
このドメインが廃止されていました。
何が起きたか。撮影自体は成功します。HTTP も 200 で返ります。画像も保存されます。中身が Cloudflare のブロックページになっていただけです。
つまり、誹謗中傷の証拠として、エラー画面を保存していました。
失敗として落ちてくれれば気付きます。落ちませんでした。「それらしい画像が保存されている」状態は、目視で一覧を眺めても異常に見えません。証拠として提出する段になって初めて分かります。
原因は2つ重なっていました。
- 第三者ラッパーに依存していた。 ドメインが死んでもこちらには通知が来ない
-
ignore_host_errors: trueを付けていた。 これは「相手側がエラーを返しても撮影を続行する」という指定で、ブロックページを成功として保存することを許す設定です
ignore_host_errors は、多少崩れても撮れたほうが嬉しい用途なら妥当です。証拠を扱う機能では、この設定は害にしかなりません。
X の取得を公式の埋め込みに切り替えました。
platform.twitter.com/embed/Tweet.html?id=
第三者のラッパーは、その人がやめた時点で終わります。公式の埋め込みは、そのサービスが自分の都合で維持します。同じ「他人のもの」でも、維持される理由があるかどうかが違います。
速くしたら、証拠が付かなくなった
スキャンが遅かったので、撮影を同期処理からバックグラウンドへ移しました。速くなりました。
保存した証拠にスクリーンショットが付かなくなりました。
証拠として保存する処理が「スキャンの時点でスクショは取得済み」という前提でデータを引き継いでいたためです。前段を非同期にした瞬間、その前提が崩れます。
直し方は2段構えにしました。
- スキャンは即座に応答を返し、撮影は後ろで走らせる
- 証拠として保存する瞬間に、スクショの有無を確認して、無ければその場で取る
あわせて、撮り逃した候補は次のスキャンで拾い直すようにしました。
同期処理を非同期にするときは、「後段が同期を前提にデータを引き継いでいる箇所」を必ず洗う。 速度だけ見て切り替えると、こういう形で静かに壊れます。
まとめ
- 第三者のラッパーはいつか死ぬ。 公式の埋め込みを使う
- 死んだとき、失敗は例外ではなく「それらしい画像」として返ってくる
-
ignore_host_errorsのような「多少おかしくても続行する」指定は、扱っているものが証拠なら害にしかならない - 外部サービスのパラメータは変わる。 生のエラー本文を残しておくと、変わった日に即座に分かる
- 相手の表示に依存する値(タイムゾーン)と、自分で記録すべき値(取得日時)を分ける
- 同期を非同期にしたら、後段の前提を洗う
「撮れた」と「証拠が撮れた」は別でした。前者は HTTP 200 で分かりますが、後者は中身を見ないと分かりません。
踏んだ落とし穴だけを書いています。 同じところで止まった人の時間が少しでも減れば十分です。
他の記事も同じ形で、実際に測った数字と、直した手順だけを置いています。
(この話が出てきたのは Wisp という個人開発のアプリです)