Mステで好きなアーティストを見逃さないためのプッシュ通知サービス

# はじめに

たまの音楽番組で好きなアーティストのライブだけ見たいという要望があったので（僕）、見逃し防止Pushツールをつくりました。

# キーワード
・firebase
・ServiceWorker
・golang（beego）
・サブドメイン

# 成果物
[livepush（ライブプッシュ）](https://livepush.shijimi.work/)
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">好きなミュージシャンの出演タイミングをプッシュ通知してくれるサービスかんせい！<a href="https://t.co/IFsfP9MW2P">https://t.co/IFsfP9MW2P</a> <a href="https://t.co/MpfCSRKM7U">pic.twitter.com/MpfCSRKM7U</a></p>&mdash; playtag551 (@playtag551) <a href="https://twitter.com/playtag551/status/1238484724756049920?ref_src=twsrc%5Etfw">March 13, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
試しに「脱力タイムズ」で試した動画が↑にでてるはず。
うまく反応しました(^^)
<br/>
バックグラウンドでも動作するのがPWAのいい所！
逆にiphoneで利用できないのがつらい点です。
swiftでアプリにして、apple storeに登録しよ（macないけど）

# イメージ
嵐の曲だけ見たいとき
![20200315_184146000_iOS.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/343913/d976b6e4-d2f9-0cce-f548-d065bcae0301.png)
詳細
![20200315_185911000_iOS.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/343913/da26bf52-576b-8e61-0849-5c07db3bdf24.png)

# 前提
・TwitterAPIの登録
・firebaseの登録
・golangでのサーバ起動
・ドメイン取得済み

# TwiiterAPIとの連携

[Go言語でTwitterAPIを利用する(anacondaの利用)](https://qiita.com/dahiyu/items/7ffd6ee0b2afa0ea46bd)
golang側からTwitterAPIを叩きます。
取得したツイートに特定のキーワードが含まれているかを確認し、プッシュ通知を送るか否かを判定します。

```controller.go
// twitter-apiに接続
func GetTwitterApi() *anaconda.TwitterApi {
	api := anaconda.NewTwitterApiWithCredentials(beego.AppConfig.String("your-access-token"), beego.AppConfig.String("your-access-token-secret"), beego.AppConfig.String("your-consumer-key"), beego.AppConfig.String("your-consumer-secret"))
	return api
}

// ツイートを直近1000件分取得する
func GetTwitter(keyword string) anaconda.SearchResponse {
	api := GetTwitterApi()
	v := url.Values{}
	v.Set("count", "1000")
	searchResult, _ := api.GetSearch(keyword, v)
	return searchResult
}

// 取得したツイートを厳選する
func NormalizeTwitter(searchResult anaconda.SearchResponse) (res []anaconda.Tweet) {
	for _, tweet := range searchResult.Statuses {
        // リツイートは除外
		if tweet.RetweetedStatus == nil {
			t, _ := time.Parse("Mon Jan 2 15:04:05 -0700 2006", tweet.CreatedAt)
            // タイムスタンプの整形
			sec := time.Since(t).String()
			sec = strings.Split(sec, ".")[0]
			tweet.CreatedAt = sec + "s ago"
			res = append(res, tweet)
		}
	}
	return res
}

// push通知判定用ツイートを抽出
func judgeTwitter(searchResult []anaconda.Tweet) (res []anaconda.Tweet) {
    // 最新30秒のツイートを対象
	delayTime := 30
	for _, tweet := range searchResult {
		sec := strings.Split(tweet.CreatedAt, "s")[0]
		if !strings.Contains(sec, "m") {
			secInt, _ := strconv.Atoi(sec)
			if secInt < delayTime {
				res = append(res, tweet)
			}
		}
	}
	return res
}

// プッシュ通知を送信するか判定
func JudgePush(searchResult []anaconda.Tweet) bool {
	n := 0
	threshold := 2
    // 「欅坂はじまったよ！！」みたいなツイートに反応
	Judgewords := [...]string{"始まり", "はじまり", "始まっ", "はじまっ", "始まる", "はじまる", "きそう", "来そう", "きた", "来た", "キタ", "もうすぐ"}
	for _, tweet := range searchResult {
		for _, word := range Judgewords {
			if strings.Contains(tweet.FullText, word) {
				n++
				break
			}
		}
	}
	fmt.Println("hit count:", n)
	var push bool = false
	if n >= threshold {
		push = true
	}
	return push
}
```

# Push通知

[Progressive Web Apps (PWA) 学習者のメモ その2 (プッシュ通知とFCM)](https://qiita.com/TakeshiNickOsanai/items/cbb0247cd9a893dc0a6b)

golangのサーバがfirebaseにリクエストを投げており、firebaseがTokenの登録されているServiceWorkerにプッシュ通知を送信しているようです。
なお、TokenはクライアントからxmlhttprequestのPOSTで送受信しています。

```controller.go
// tokenベースでpush通知を送信する
func SendToToken(RegistrationToken string, SearchWord string) {
	ctx := context.Background()
	opt := option.WithCredentialsFile("/serviceAccountKey.json")

	app, err := firebase.NewApp(ctx, nil, opt)
	if err != nil {
		log.Fatalln(err)
		return
	}

	client, err := app.Messaging(ctx)
	if err != nil {
		log.Fatalln(err)
		return
	}

	message := &messaging.Message{
		Notification: &messaging.Notification{
			Title: SearchWord + " will start !!",
			Body:  "そろそろ始まりそうですよ。",
		},
		Token: RegistrationToken,
	}

	response, err := client.Send(ctx, message)
	if err != nil {
		log.Fatalln(err)
	}
	fmt.Println("Successfully sent message:", response)
}

```
```firebase-messaging-sw.js
importScripts('https://www.gstatic.com/firebasejs/5.10.1/firebase-app.js');
importScripts(
  'https://www.gstatic.com/firebasejs/5.10.1/firebase-messaging.js'
);

self.addEventListener('fetch', function(event) {});

firebase.initializeApp({
  messagingSenderId: '1018●●●●●●●●'
});

const messaging = firebase.messaging();
//バックグラウンドでのプッシュ通知の受信
messaging.setBackgroundMessageHandler(function(payload) {
  console.log(
    '[firebase-messaging-sw.js] Received background message ',
    payload
  );
  // Customize notification here
  var notificationTitle = payload.notification.title; // タイトル
  var notificationOptions = {
    body: payload.notification.body, // 本文
    icon: payload.notification.icon // アイコン
  };

  return self.registration.showNotification(
    notificationTitle,
    notificationOptions
  );
});

```
```index.tpl
<script>
// フォアグラウンドでのプッシュ通知の受信
messaging.onMessage(payload => {
        // console.log('Message received. ', payload);
        var notificationTitle = payload.notification.title; // タイトル
        var notificationOptions = {
          body: payload.notification.body, // 本文
          icon: payload.notification.icon // アイコン
        };

        showNotification(notificationTitle, notificationOptions);
      });
</script>
```

# サブドメインにリバースプロキシ
[conohaのVPS + nginx+お名前.comで取得したドメイン&サブドメインの設定](https://qiita.com/kosukeKK/items/feb6993eecf824c9f8a3)
ここでは、steveltn/https-portal:1によるdockerコンテナがすでに起動しており、今回作成したWebアプリコンテナで公開中のポートに接続するようにします。


注意点としては、同一のdocker-compose.yml内での接続方法は、
[docker で全自動 Let's encrypt](https://qiita.com/kuboon/items/f424b84c718619460c6f) のようにアプリ名をlinksして接続すればよいのですが（このときポート開放は127.0.0.1:8080:8080でも、8080:8080でもよい）、


別々のdocker-compose.ymlの場合は、ポートを8080:8080のようにフルアクセスできるようにし、ファイアーウォールも許可する必要がありました。
さらに、https-portalコンテナでは、DOMAINS内で、livepush.shijimi.work -> http://dockerhost:8080 のようにhostPCのポートを指定する必要があります。
例えば、ここでhttp://127.0.0.1:8080 のように設定しても良さそうなものですが、おそらくlocalhostがhttps-portalコンテナのことと捉えられている気がします。

# まとめ
想像以上にfirebase周りで苦戦しました。
これで、ゲリラ系の放送には対応できそうです！！
[前回の記事](https://qiita.com/playTag55/items/1766c097502763888579)で紹介したドラマ・映画のSNS（[shijimi](https://www.shijimi.work/)）もヨロシクオネガイシマス！