Chat Applicationを作ってみる With GO and Pusher

#ChatApplicationを作ってみる
ハッカー初心者の自分に必要なのは開発経験！
ということでまずとりあえずチャットアプリを作ってみる。
Pusherのドキュメントをとりあえず真似て、そのあとに色々いじってみることになりそう。

間違いやアドバイスなどフィードバックがあればコメントしてくれるとありがたいです！！

##使用するお供たち
 - **GO** 今回はむかしにちょっとだけかじったGOを探り探りで使ってみる。GOのいいところといえばGopherくんが可愛い！![download.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/412949/34519d45-71d9-20f4-4d38-b00eedd8bb40.jpeg)

 - **Pusher** WebSocketに対してはPusherを使ってみる
 - **HTML/CSS/JS** フロントの実装

##始める前にーWebSocketとはなんぞ？
ということで少し調べてみる
###WebSocketとは
WebSocketはネットワーク用通信規格の一つ。
Ajaxのおいて頻繁に発生するデータのやりとりに対し、XHRはブラウザからのサーバへの要求手段にすぎず、サーバからのプッシュに難があるらしい。

そこで誕生したのがWebSocketで、サーバとクライアントのコネクションを専用のプロトコルを使い双方間通信を実現するらしい。

##Pusherとは

WebSocketに対応するAPIは色々あると思います。代表的なのはSocketioなどだと思うのですが、一度使ったことがあるので今回はPusherを。

これらを使いWebSocketを介して双方向機能を統合します。

##作る
早速、PusherのDocumentationを覗きます。
[PusherのDocumentation](https://pusher.com/tutorials/chat-app-go)

Goのインストールと基礎知識、JavaScriptの基礎知識が必要とのこと。
[Goのインストール](https://golang.org/doc/install)

####PusherのSet up
まずのアカウントを作る必要があるようなので作る。
[ここでサインアップ](https://dashboard.pusher.com/accounts/sign_up)
無事完了したらCreate a new applicationのボタンをおす。そうすることでのちに必要となるアップキーなどが使えるようになる。

もう一つApp SettingでEnable client eventsを有効にしておく必要があるらしい。これでクライアント側からイベントをトリガーしたりできるようになるらしい。

####メインのGoファイルをつくる
フォルダー名をgo-chatと名ずけてその中にエントリーポイントとなるchat.goファイルをつくる。

Pusherもインストールしておきます。

`$ go get github.com/pusher/pusher-http-go`


※windowsを使っている人はエラーにエンカウントする可能性があるらしい。

chat.goファイル内で次のコードをかく。

```chat.go
    import (
        "encoding/json"
        "fmt"
        "io/ioutil"
        "log"
        "net/http"

        pusher "github.com/pusher/pusher-http-go"
    )

    var client = pusher.Client{
        AppId:   "PUSHER_APP_ID",
        Key:     "PUSHER_APP_KEY",
        Secret:  "PUSHER_APP_SECRET",
        Cluster: "PUSHER_APP_CLUSTER",
        Secure:  true,
    }

    type user struct {
        Name  string `json:"name" xml:"name" form:"name" query:"name"`
        Email string `json:"email" xml:"email" form:"email" query:"email"`
    }

    func main() {
        http.Handle("/", http.FileServer(http.Dir("./public")))

        http.HandleFunc("/new/user", registerNewUser)
        http.HandleFunc("/pusher/auth", pusherAuth)

        log.Fatal(http.ListenAndServe(":8090", nil))
    }
```

PUSHER_APP_*の部分は自分のアカウントのダッシュボードにのっているものに変える必要がある。

**はじめに色々とインポート。**

 - encoding/jsonでjsonデータを変換したり、逆にjsonデータに変換したりする
 - io/ioutilでインプットアウトプットのユーティリティを実装する。ファイルを読んだり書き込んだりするらしい。今回ではユーザーの登録時のインプットを読み込むのに使う。
 - logでログの出力を行う。今回はlog.Fatal()でサーバを終了させてる（？）
 - net/httpでhttp処理を色々するらしい（？）のだがまだ全然掴めない。ルーティングに使用？

###Pusherクライアントとuserストラクチャ

新しいPusherクライアントを使ってキーをかきかえ。
userストラクチャを作って色々の構造にバインドするらしい（？）。良く理解できないしjson,xmlと用意する意味がうまく理解できない。

###エンドポイントをつくる

`/`
静的ファイルを返す。見た目担当。
`/new/user/`
新しいユーザーをつくる。
`/pusher/auth`
チャンネルにサブスクライブする

chat.goファイルのmainファンクションの手前にコードを付け加える。

```chat.go
    func registerNewUser(rw http.ResponseWriter, req *http.Request) {
        body, err := ioutil.ReadAll(req.Body)
        if err != nil {
            panic(err)
        }

        var newUser user

        err = json.Unmarshal(body, &newUser)
        if err != nil {
            panic(err)
        }

        client.Trigger("update", "new-user", newUser)

        json.NewEncoder(rw).Encode(newUser)
    }

    func pusherAuth(res http.ResponseWriter, req *http.Request) {
        params, _ := ioutil.ReadAll(req.Body)
        response, err := client.AuthenticatePrivateChannel(params)
        if err != nil {
            panic(err)
        }

        fmt.Fprintf(res, string(response))
    }
    
```


ここのファンクションをハンドラーにつかませる（？）。

 - 疑問
 
`(rw http.ResponseWriter, req *http.Request)`
がわからない。ただ覚えればいいのかもしれないけど（？）。
`http.ResponseWriter`は型として捉えればいいのかな？こいつの正体がわからない。
`*http.Request`のポインタはなに？

 - とりあえず中身を見てみる
 
クライアントのボディーを読み込む。ユーザーの入力した`name`を`email`読み込む。

`if err != nil {
panic(err)
}`

はエラー処理のいっつもついてくるボイラープレート的なものと思っていいのかな？

 - newUser
 
newUserをつくって

`err = json.Unmarshal(body, &newUser)`

でjsonを変換してそのアドレスにuser型としてぶっこむ。

 - イベントをトリガーする

 `client.Trigger(channel, event, data)`

に乗っ取って`update`チャンネルに`new-user`イベントを`newUser`を使ってトリガーする。

 - 疑問２

`json.NewEncoder(rw).Encode(newUser)`

はリクエストで受けとったものを`newUser`に変換してそれをjsonデータにしてリスポンスしてるのかな？
`Marshal`じゃダメなのかな？

 - 次のファンクションを見てみる
 
このファンクションでユーザーをチャンネルにauthするのかな？

`params, _ := ioutil.ReadAll(req.Body)`

アンダースコアはエラー処理を省略するため？なぜ？

`response, err := client.AuthenticatePrivateChannel(params)`

受け取った`params`を使ってauthしてるのかな?


###フロントをつくる

`public`フォルダを作り、その中に`css`と`js`フォルダをつくる。


 - HTML


`public`フォルダのルートに`index.html`を作る。

```index.html
<!DOCTYPE html>
    <html lang="en">
      <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>Chat with friends in realtime</title>
        <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta.2/css/bootstrap.min.css">
        <link rel="stylesheet" href="./css/app.css" >
      </head>
      <body>
        <header>
            <nav class="navbar navbar-expand-md navbar-dark fixed-top bg-dark">
                <a class="navbar-brand" href="#">Welcome</a>
            </nav>
        </header>
        <div class="container-fluid">
            <div class="row" id="mainrow">
                <nav class="col-sm-3 col-md-2 d-none d-sm-block bg-light sidebar">
                    <ul class="nav nav-pills flex-column" id="rooms">
                    </ul>
                </nav>
                <main role="main" class="col-sm-9 ml-sm-auto col-md-10 pt-3" id="registerScreen">
                    <h3 style="text-align: center">Type in your details to chat</h3>
                    <hr/>
                    <div class="chat" style="margin-bottom:150px">
                        <p>&nbsp;</p>
                        <form id="loginScreenForm">
                            <div class="form-group">
                              <input type="text" class="form-control" id="fullname" placeholder="Name" required>
                            </div>
                            <div class="form-group">
                              <input type="email" class="form-control" id="email" placeholder="Email Address" required>
                            </div>
                            <button type="submit" class="btn btn-block btn-primary">Submit</button>
                          </form>
                      </div>
                </main>

                <main role="main" class="col-sm-9 ml-sm-auto col-md-10 pt-3" style="display: none" id="main">
                    <h1>Chats</h1>
                    <p>👈 Select a chat to load the messages</p>
                    <p>&nbsp;</p>
                    <div class="chat" style="margin-bottom:150px">
                        <h5 id="room-title"></h5>
                        <p>&nbsp;</p>
                        <div class="response">
                            <form id="replyMessage">
                                <div class="form-group">
                                    <input type="text" placeholder="Enter Message" class="form-control" name="message" />
                                </div>
                            </form>
                        </div>
                        <div class="table-responsive">
                          <table class="table table-striped">
                            <tbody id="chat-msgs">
                            </tbody>
                        </table>
                    </div>
                </main>
            </div>
        </div>

        <script src="https://js.pusher.com/4.0/pusher.min.js"></script>
        <script src="https://code.jquery.com/jquery-3.2.1.slim.min.js"></script>
        <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.3/umd/popper.min.js"></script>
        <script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta.2/js/bootstrap.min.js"></script>
        <script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.18.0/axios.min.js"></script>
        <script type="text/javascript" src="./js/app.js"></script>
      </body>
    </html>
```

 - Css

```app.css
 body {
        padding-top: 3.5rem;
    }
    h1 {
        padding-bottom: 9px;
        margin-bottom: 20px;
        border-bottom: 1px solid #eee;
    }
    .chat {
        max-width: 80%;
        margin: 0 auto;
    }
    .sidebar {
        position: fixed;
        top: 51px;
        bottom: 0;
        left: 0;
        z-index: 1000;
        padding: 20px 0;
        overflow-x: hidden;
        overflow-y: auto;
        border-right: 1px solid #eee;
    }
    .sidebar .nav {
        margin-bottom: 20px;
    }
    .sidebar .nav-item {
        width: 100%;
    }
    .sidebar .nav-item + .nav-item {
        margin-left: 0;
    }
    .sidebar .nav-link {
        border-radius: 0;
    }
    .placeholders {
        padding-bottom: 3rem;
    }
    .placeholder img {
        padding-top: 1.5rem;
        padding-bottom: 1.5rem;
    }
    tr .sender {
        font-size: 12px;
        font-weight: 600;
    }
    tr .sender span {
        color: #676767;
    }
    .response {
        display: none;
    }
```

 - JS

```app.js
(function () {
        var pusher = new Pusher('PUSHER_APP_KEY', {
            authEndpoint: '/pusher/auth',
            cluster: 'PUSHER_APP_CLUSTER',
            encrypted: true
        });

        let chat = {
            name: undefined,
            email: undefined,
            endUserName: undefined,
            currentRoom: undefined,
            currentChannel: undefined,
            subscribedChannels: [],
            subscribedUsers: []
        }

        var publicChannel = pusher.subscribe('update');

        const chatBody = $(document)
        const chatRoomsList = $('#rooms')
        const chatReplyMessage = $('#replyMessage')

        const helpers = {
            clearChatMessages: () => {
                $('#chat-msgs').html('')
            },

            displayChatMessage: (message) => {
                if (message.email === chat.email) {
                    $('#chat-msgs').prepend(
                        `<tr>
                            <td>
                                <div class="sender">${message.sender} @ <span class="date">${message.createdAt}</span></div>
                                <div class="message">${message.text}</div>
                            </td>
                        </tr>`
                    )
                }
            },

            loadChatRoom: evt => {
                chat.currentRoom = evt.target.dataset.roomId
                chat.currentChannel = evt.target.dataset.channelId
                chat.endUserName =  evt.target.dataset.userName
                if (chat.currentRoom !== undefined) {
                    $('.response').show()
                    $('#room-title').text('Write a message to ' + evt.target.dataset.userName+ '.')
                }

                evt.preventDefault()
                helpers.clearChatMessages()
            },

            replyMessage: evt => {
                evt.preventDefault()

                let createdAt = new Date().toLocaleString()            
                let message = $('#replyMessage input').val().trim()
                let event = 'client-' + chat.currentRoom

                chat.subscribedChannels[chat.currentChannel].trigger(event, {
                    'sender': chat.name,
                    'email': chat.currentRoom,
                    'text': message, 
                    'createdAt': createdAt 
                });

                $('#chat-msgs').prepend(
                    `<tr>
                        <td>
                            <div class="sender">
                                ${chat.name} @ <span class="date">${createdAt}</span>
                            </div>
                            <div class="message">${message}</div>
                        </td>
                    </tr>`
                )

                $('#replyMessage input').val('')
            },

            LogIntoChatSession: function (evt) {
                const name  = $('#fullname').val().trim()
                const email = $('#email').val().trim().toLowerCase()

                chat.name = name;
                chat.email = email;

                chatBody.find('#loginScreenForm input, #loginScreenForm button').attr('disabled', true)

                let validName = (name !== '' && name.length >= 3)
                let validEmail = (email !== '' && email.length >= 5)

                if (validName && validEmail) {
                    axios.post('/new/user', {name, email}).then(res => {
                        chatBody.find('#registerScreen').css("display", "none");
                        chatBody.find('#main').css("display", "block");

                        chat.myChannel = pusher.subscribe('private-' + res.data.email)
                        chat.myChannel.bind('client-' + chat.email, data => {
                            helpers.displayChatMessage(data)
                        })
                    })
                } else {
                    alert('Enter a valid name and email.')
                }

                evt.preventDefault()
            }
        }


        publicChannel.bind('new-user', function(data) {
            if (data.email != chat.email){
                chat.subscribedChannels.push(pusher.subscribe('private-' + data.email));
                chat.subscribedUsers.push(data);

                $('#rooms').html("");

                chat.subscribedUsers.forEach((user, index) => {
                    $('#rooms').append(
                        `<li class="nav-item"><a data-room-id="${user.email}" data-user-name="${user.name}" data-channel-id="${index}" class="nav-link" href="#">${user.name}</a></li>`
                    )
                })
            }
        })

        chatReplyMessage.on('submit', helpers.replyMessage)
        chatRoomsList.on('click', 'li', helpers.loadChatRoom)
        chatBody.find('#loginScreenForm').on('submit', helpers.LogIntoChatSession)
    }());
```


`APP_KEY_*`の部分は変えなければいけない。

###Run

``go run chat.go``

でrunすると[http://127.0.0.1:8090](http://127.0.0.1:8090)でapplicationが完成！

#まとめ

とりあえず完成。疑問点が多いのでそれを解決しないといけないのとここからカスタムしていく予定。

フロント部分は説明はしょったのでまた付け加えるかも。