なにをやったのか
- 子供が保育園・小学校・学習塾に通っている
- それぞれ、お知らせがWebに掲載される(日記とかも含む)
- 個別連絡はメールで届く(学年単位の緊急連絡とか)
- いちいち学校のウェブサイトで確認したりメールを検索するのが面倒なので、LINEに通知を統合した
LINEの通知はIFTTTを使う
- 保育園や小学校、当然ながらRSSのような便利ソリューションは存在しない
- なので、Firebase Functionsでポーリングすることにした
- Firebaseの認証処理がスキップできる(デフォルトでAdmin権限が割り当て)られるので、各種処理が楽になる
- 基本的に httpトリガー を使って、任意のタイミングで呼び出せるようにする
- 関数の言語はなんでもいいけど、自分はGolang 1.13を使用した。
- スクレイピング処理には
github.com/PuerkitoBio/goquery を使用。
- Webサイトの構造は当然サイトごとに異なるので頑張って個別対応。
- Firebase Functions自体にもスケジューリング機能はついているけど、 Cloud Scheduler でKickするほうが再設定やデバッグが楽なので、スケジューリングは設定していない
- 学校のCMSに負荷をかけるのは本意ではない(し、そこまでWebページの更新頻度は高くない)ので、cronは1時間に1回行ってる.
キャッシュ処理
- IFTTTのWebHookにはキャッシュがないので、単純にCron実行すると同じ通知が何度も流れてしまう
- そのため、 Firestore に通知済みのキャッシュを放り込んで、キャッシュが存在したらHookをスキップするようにする
通知とキャッシュのサンプルコード
- 通知のタイトル・本文・リンクURLの組み合わせをMD5化して、それをキャッシュキーにする
- キャッシュヒットしたら、それは通知済みなのでスキップ
var firebaseApp *firebase.App
var firestoreClient *firestore.Client
var webhook iftttWebhook.IFTTT
func init() {
if app, err := firebase.NewApp(context.Background(), nil); err != nil {
panic(err)
} else if fs, err := app.Firestore(context.Background()); err != nil {
panic(err)
} else {
firebaseApp = app
firestoreClient = fs
}
webhook = iftttWebhook.New("割り当てられたAPIキー")
}
func md5sum(s string) string {
sum := md5.Sum([]byte(s))
return hex.EncodeToString(sum[:])
}
func notifyWebPageLink(ctx context.Context, title string, body string, link string) {
uid := md5sum(title + "/" + body + "/" + link)
ref := firestoreClient.Collection("ifttt").Doc("web-notify").Collection("cache").Doc(uid)
if _, err := ref.Get(ctx); status.Code(err) != codes.NotFound {
log.Println(fmt.Sprintf(" * Exist Document: %v", ref.Path))
return
}
log.Println(fmt.Sprintf(" * Put Document: %v", ref.Path))
log.Println(fmt.Sprintf(" * Title: %v", title))
log.Println(fmt.Sprintf(" * Body: %v", body))
if _, err := ref.Create(ctx, map[string]interface{}{
"created_at": time.Now(),
"title": title,
"body": body,
"link": link,
}); err != nil {
log.Println(fmt.Sprintf("Firestore write failed: %v", err))
}
webhook.Emit("通知ID", title, body, link)
}
func CronHoikuenWeb(w http.ResponseWriter, r *http.Request) {
うまいことスクレイピング()...
w.WriteHeader(http.StatusNoContent)
}
func CronShogakkouWeb(w http.ResponseWriter, r *http.Request) {
うまいことスクレイピング()...
w.WriteHeader(http.StatusNoContent)
}
GmailからIFTTTへの通知戦略
- 塾も入退室時にメールで通知してくれる
- 保育園や小学校は緊急連絡(遠足延期とか)がメールで来る
- なので、メールもLINEにGOだ。
GASをcronで呼び出す
- Firebase Functionsだと、自分のGmailへのアクセスが面倒(認証とか)
- なので、Google Apps Scriptから通知を飛ばすことにした
- GASはGmail APIを使うとOAuthが自動的に開始されるので、認証がらく。
- 認証状態を保持したまま、Cronを走らせられる
- 設定項目もユーザーフレンドリー(cron構文使わなくてよい)
- メールはGoogleインフラなので、1分に1回ポーリングしても大丈夫そう
- リアルタイム性を重視したいならpub/subに流して〜とかあるけど、面倒な割にそこまでする必要はない
- gmail検索条件をそのまま利用できるので、
from:example@example.com newer_than:1h とかできるし、めちゃくちゃ複雑なクエリも作れる
- けど俺はシンプルにfromと直近のn時間のみでクエリを投げた
- LINE通知は嫁さんにも届くので、間違ってもFANZAの課金通知を飛ばすわけにはいかねぇんだよ!!
UrlFetchApp.fetch の投げ先は、Functionsで作ったhttpトリガー.
- POSTリクエストでパラメータを受け取り、それをそのままさっきの
notifyWebPageLink() に流すようにした
func WebHookFromGmail(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
bodyBytes, err := ioutil.ReadAll(r.Body)
if err != nil {
panic(err)
} else {
log.Printf("read body: %v", string(bodyBytes))
}
var request map[string]string
if err = json.Unmarshal(bodyBytes, &request); err != nil {
panic(err)
} else {
log.Printf("parsed: %v", request)
}
title := request["title"]
body := request["body"]
link := request["link"]
notifyWebPageLink(ctx, title, body, link)
w.WriteHeader(http.StatusNoContent)
}
function onCron() {
notifyGmail("gmailの検索条件", "通知タイトル", "リンクURL")
}
function notifyGmail(query, from, link) {
GmailApp.search(query).forEach(function(thread){
thread.getMessages().forEach(function(message) {
console.log("============================");
console.log(message.getSubject());
console.log(message.getBody());
notifyExternalWebHook(from, message.getSubject() + " :: " + message.getBody(), link);
});
});
}
function notifyExternalWebHook(title, body, link) {
var requestBody = JSON.stringify({
title : title,
body: body,
link: link,
});
console.log(requestBody);
var options =
{
method : "POST",
payload : requestBody,
};
UrlFetchApp.fetch("自分のエンドポイントURL", options);
}
最後に
- セキュリティもくそもない書き捨てコードなので、何かあったら自己責任で頑張ろうね!
- あと、IFTTTのWebHookにキャッシュつけてくんねかな!!