# 20230125_slackのメッセージが3ヶ月間で表示されなくなるのを技術で解決する
(例:20210901_LT会を開催しました)
###### tags: `ブログ記事`
- [x] 公開(ブログ公開担当者がいじるやつ)
太字斜体で書いてある内容を埋めて行ってください.
文章,画像は太字斜体の下の行に入れてください.
最初に書く時はREADMEを読んだら読むといいと思います.
<br>
## 表示されない情報
***書いた人の名前(自己紹介文と同じ名前)***
{
藤崎 竜成
}
***記事の簡単な説明(検索した時にタイトルの下に出てくる文章)***
}
<br>
## 表示される部分
***サムネイル画像***
{

}
***カテゴリ***
以下の中から該当しそうなカテゴリを選択してください
※一つだけ選択してください
- [ ] 対外活動
- [x] 活動の様子
- [ ] メンバーの趣味
***タグ***
以下の中から該当しそうなカテゴリを選択してください.当てはまる物がない場合は適宜追加してください.
言語
- [ ] HTML
- [ ] CSS
- [x] Python
- [x] Go
- [ ] Ruby
- [ ] JavaScript
- [ ] TypeScript
- [ ] Dart
- [ ] Rust
- [ ] Kotlin
- [ ] Swift
フレームワーク・ライブラリ
- [ ] Ruby on rails
- [x] Vue.js
- [ ] Nuxt.js
- [ ] React.js
- [ ] Next.js
- [ ] Gin
- [ ] Flluter
ツール
- [ ] GitHub
- [ ] ターミナル
- [ ] WSL
- [ ] Ubuntu
- [x] Docker
- [ ] Raspberry Pi
- [ ] Figma
分野
- [ ] Web-design
---
***以下に本文を記載してください***
# はじめに
こんにちは。NUTMEGの藤崎です。先日NUTMEG内でLT会が行われ、slack無料プランのメッセージが3ヶ月間で表示されなくなるのをweb+slackAPIを利用してwebアプリを作成し解決した話をしました。今回はその話をもう少し具体的に話していきます。
# 開発背景
slackの無料プランでは、3ヶ月間でメッセージ表示されなくなってしまいます。自分達NUTMEGや学祭実行委員会内では、3ヶ月前のメッセージも遡りたいこともありますが、利用しているslackは無料プランなので見ることができません。私たちNUTMEGや学祭実行委員会では、予算の関係上slackの有料プランを利用することが困難です。
そのため、有料プランを使用せずに、3ヶ月前のslackを遡る方法としてslackAPI + web技術を使い解決できると考え開発を開始しました。
# 開発人数と開発期間
本プロジェクトは12月から開始し、個人開発で行っていました。
現在は、興味あるメンバーがいて二人で進めています。
# 構成と選定理由
今回利用した技術、選定理由としては以下のようになります。
## SlackBot
**Bolt for Python** <img src="https://i.imgur.com/rgxeY4e.png" width="30">
チャンネルのメッセージの監視してくれるbotと収集するロジックはBolt for Pythonを利用しました。
Bolt for PythonはSlack 連携アプリを作るための公式フレームワークです。BoltはWebAPIを利用して簡単にメッセージなどの情報を取得することが簡単にできます。
今回は簡単に早く開発したいのもあり、普段から使い慣れているPythonで書くことのできるBoltを採用しました。
## バックエンド
**Golang** <img src="https://i.imgur.com/dM8utQ1.png" width="60">
バックエンドはGolangを利用しました。また、APIサーバーはEchoを利用しました。EchoはWebアプリケーションフレームワークであり、3分ほどでAPIサーバーを構築しやすく、普段から使い慣れているので採用しました。
DBからデータを取ってきて、可視化するだけを考えるとバックエンドは不必要そうですが、slackbot, バックエンド, フロントエンドの三層にして、監視、ロジック、可視化の責務を分けることで今後の保守が楽になると考え、導入しました。
## フロントエンド
**Vue** <img src="https://i.imgur.com/hxPCi2K.png" width="30">
フロントエンドはVue.jsを採用しました。私は普段はバックエンドを基本的に書いているので、フロントエンドはあまり得意ではなかったのですが、Vueはフロント初心者でも扱いやすいということで採用しました。
## DB
**MongoDB** <img src="https://i.imgur.com/jmwMeKf.png" width="33">
データベースはMongoDBを採用しました。MongoDBはNoSqlなので、json形式でデータを保存することが可能です。
slack APIを利用して返ってくるresponseは結構複雑なjsonであり、正規化したりするのは大変だと考えました。そこで取得したjsonをそのままDBに入れられるNoSqlを採用することでこの問題を解決しました。
## インフラ
**NUTMEG Cloud** <img src="https://i.imgur.com/kpnQ11B.png" width="30">
デプロイ先には、NUTMEGのインフラチームが開発しているNUTMEG Cloudを採用しました。NUTMEG CloudとはNUTMEGで運用しているオンプレミスサーバーのことです。複数のミニPCを用いて製作してあり、NUTMEGのプロダクトは基本的にここにデプロイされます。
NUTMEGクラウドをもっと詳しく →[NUTMEG クラウド](https://docs.google.com/presentation/d/e/2PACX-1vQiXSXvk4nYba0lhzgyWuvbgXbqtQbRIW7RfrFsiShCHgs4tOw5sckmG1y-ZqijIyEySKrRnyw4t1xw/embed?start=false&loop=false&delayms=3000)
# 構成図と流れ
## 構成図
イメージに表すと以下のようになります。

## 流れ
大まかな流れは以下のようになります。
①botがいるチャンネルで誰かがメッセージを投稿する
②botがチャンネルを監視しており、投稿されたメッセージをMongoDBに保存する
③誰かが、本プロダクトにアクセスする。
④フロントがAPI requestを行う
⑤バックエンドがDBから必要なデータを取り出し、データを整形する
⑥整形されたデータをフロントに返す
⑦フロントによるメッセージの可視化
# 苦戦した点
## Golangにおけるデータの整形
このプロダクトで最も苦戦したところは、MongoDBから取り出したデータから必要なデータだけに整形するところでした。
MongoDBにはslack APIのレスポンスがそのまま入っています。
DBには以下のようなデータが入っています。[slack API message.channelsより引用](https://api.slack.com/methods/conversations.history)
```
{
"token": "one-long-verification-token",
"team_id": "T061EG9R6",
"api_app_id": "A0PNCHHK2",
"event": {
"type": "message",
"channel": "C024BE91L",
"user": "U2147483697",
"text": "Live long and prospect.",
"ts": "1355517523.000005",
"event_ts": "1355517523.000005",
"channel_type": "channel"
},
"type": "event_callback",
"authed_teams": [
"T061EG9R6"
],
"event_id": "Ev0PV52K21",
"event_time": 1355517523
}message.channels
```
しかし、実際に必要なデータは、`event`key内の`value`だけでした。そのためMongoDBから取り出したデータから`event`key内の要素だけを取り出し、新しく連想配列に入れていこうとしましたが、なかなかうまくいきませんでした。
原因はmongoDB内で保存されているデータが[bson形式](https://bsonspec.org/)だからです。そのため取り出して新しい変数に入れたり、連想配列に格納する際にはキャストする必要がありました。
実際に連想配列に格納するメソッドです。
```
func (u *mongoDBUsecase) FetchData() []map[string]string {
docs, err := u.repository.AllCollection()
usersInfoMap, err := u.repository.GetUserInfo()
channelsInfoMap, err := u.repository.GetChannelInfo()
usersInfo := usersInfoMap[0]
channelsInfo := channelsInfoMap[0]
var data []map[string]string
for _, v := range docs {
var m = make(map[string]string)
eventTs := v["event"].(bson.M)["event_ts"].(string)
channel := v["event"].(bson.M)["channel"].(string)
text := v["event"].(bson.M)["text"]
threadTs := v["event"].(bson.M)["thread_ts"]
user := v["event"].(bson.M)["user"]
m["eventTs"] = eventTs
channelName := channelsInfo[channel]
m["channelName"] = channelName.(string)
m["channelId"] = channel
if threadTs != nil {
m["threadTs"] = threadTs.(string)
}
if text != nil {
m["text"] = text.(string)
}
if user != nil {
userName := usersInfo[user.(string)]
m["user"] = userName.(string)
}
data = append(data, m)
}
if err != nil {
panic(err)
}
return data
}
```
もしかしたら、もう少し上手にする方法があるかもしれませんが、本プロダクトではここが一番困難だと感じましたが、改めて、golangの型定義などを学び直すことになりとても勉強になりました。
# 実際のプロダクト
現在はこんな感じになっています。
サイドバーにはNUTMEGにあるチャンネル一覧があり、クリックするとそのチャンネルの今までのメッセージが見れるページに飛びます。(一部プライバシーのため黒塗りしています。)

# 今後の展望
まだ完成していない点がいくつかあります。
- スレッド機能の作成
- ログイン機能
- HOMEのようなページ
## スレッド機能
現在はスレッドにあるメッセージもそのまま並べられているだけです。今後はスレッド機能をつけて、会話の流れを見やすくしたいと考えています。

## ログイン機能
ログイン機能がないため、現在誰でもアクセスできる状態です。そのためログイン機能をつけてNUTMEGだけが見れるようにしていきたいと考えています。
## Home画面
メイン機能はほぼできていますが、HOME画面のようなものがないので、簡単でいいので作成していきたいと思います。
優先順としては、HOME→ログイン機能→スレッド機能くらいで考えています。
# 感想
久しぶりに個人開発をしてみました。最近勉強しているGolangを利用してwebアプリケーションを1から作れたのはいい経験だと思います。またMongoDBやBoltなどの初めて触る技術も多く、とても楽しんで開発できました。また、フロントエンドはほぼ未経験でしたが、先輩に聞きつつ開発を進め、理解度を深めることができました。さらにフロントエンドを触ることで、いつもフロント陣がどのようなことを考えて開発しているのか少しは理解できたのではないかと思っています。
まだ開発段階ではありますが、完成に向けて頑張ります!!
また、この記事の技術のところでデタラメをいってたら申し訳ありません。
# 最後に
最後まで読んでいただき、ありがとうございました。
LT会で利用した資料とgithubのリンクも添付するので、宜しければ、こちらもご覧ください。
## [nutmeg-slack](https://github.com/NUTFes/nutmeg-slack)<img src="https://i.imgur.com/aLSFkyP.png" width="50">
## LT会で使用した資料
<iframe src="https://docs.google.com/presentation/d/e/2PACX-1vSZEz_HEp1XTRjDEfxIGOPuRlfiI5-LutnmP05-K8CaXUToat7-VCSCEhjaJg9Jv-LIv-cMc3yZIri2/embed?start=false&loop=false&delayms=3000" frameborder="0" width="640" height="480" allowfullscreen="true" mozallowfullscreen="true" webkitallowfullscreen="true"></iframe>