Create a Go application with online presence

Introduction

When building applications that allow multiple users to interact with one another, it is essential to display their online presence so that each user gets an idea of how many other users are online.

In this article, we will build a live streaming application that displays the online presence of the users currently streaming a video. We will use Go, JavaScript (Vue) and Pusher Channels for the development.

Here’s a demo of the final application:

go-online-presence-demo

The source code for this tutorial is available on GitHub.

Prerequisites

To follow along with this article, you will need the following:

  • A code editor like Visual Studio Code.
  • Basic knowledge of the Go programming language.
  • Go (version >= 0.10.x) installed on your computer. Installation guide.
  • Basic knowledge of JavaScript (Vue).
  • Pusher account. Create a free sandbox Pusher account or sign in.

Once you have all the above requirements, we can proceed.

Building the backend server

We will build the backend server in Go. Create a new project directory in the src directory that is located in the $GOPATH, let’s call this directory go-pusher-presence-app.

1$ cd $GOPATH/src
2    $ mkdir go-pusher-presence-app
3    $ cd go-pusher-presence-app

Next, create a new Go file and call it presence.go, this file will be where our entire backend server logic will be. Now, let’s pull in the official Go Pusher package with this command:

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

Open the presence.go file and paste the following code:

1// File: ./presence.go
2    package main
3    
4    import (
5        "encoding/json"
6        "fmt"
7        "io/ioutil"
8        "log"
9        "net/http"
10        pusher "github.com/pusher/pusher-http-go"
11    )
12    
13    var client = pusher.Client{
14        AppId:   "PUSHER_APP_ID",
15        Key:     "PUSHER_APP_KEY",
16        Secret:  "PUSHER_APP_SECRET",
17        Cluster: "PUSHER_APP_CLUSTER",
18        Secure:  true,
19    }
20    
21    type user struct {
22        Username  string `json:"username" xml:"username" form:"username" query:"username"`
23        Email string `json:"email" xml:"email" form:"email" query:"email"`
24    }
25    
26    var loggedInUser user
27    
28    func main() {
29        // Define our routes
30        http.Handle("/", http.FileServer(http.Dir("./static")))
31        http.HandleFunc("/isLoggedIn", isUserLoggedIn)
32        http.HandleFunc("/new/user", NewUser)
33        http.HandleFunc("/pusher/auth", pusherAuth)
34    
35        // Start executing the application on port 8090
36        log.Fatal(http.ListenAndServe(":8090", nil))
37    }

NOTE: Replace the PUSHER_APP_* keys with the keys on your Pusher dashboard.

Here’s a breakdown of what we’ve done in the code above:

  • We imported all the packages that are required for the application to work, including Pusher.
  • We instantiated the Pusher client that we will use to authenticate users from the client-side.
  • We defined a user struct and gave it two the properties — username and email — so that Go knows how to handle incoming payloads and correctly bind it to a user instance.
  • We created a global instance of the user struct so that we can use it to store a user’s name and email. This instance is going to somewhat serve the purpose of a session on a server, we will check that it is set before allowing a user to access the dashboard of this application.

In the main function, we registered four endpoints:

  • / - loads all the static files from the static directory.
  • /isLoggedIn - checks if a user is logged in or not and returns a fitting message.
  • /new/user - allows a new user to connect and initializes the global user instance.
  • /pusher/auth — authorizes users from the client-side.

In the same file, above the main function, add the code for the handler function of the /isLoggedIn endpoint:

1// File: ./presence.go
2    
3    // [...]
4    
5    func isUserLoggedIn(rw http.ResponseWriter, req *http.Request){
6        if loggedInUser.Username != "" && loggedInUser.Email != "" {
7            json.NewEncoder(rw).Encode(loggedInUser)
8        } else {
9            json.NewEncoder(rw).Encode("false")
10        }
11    }
12    
13    // [...]

After the function above, let’s add the handler function for the /new/user endpoint:

1// File: ./presence.go
2    
3    // [...]
4    
5    func NewUser(rw http.ResponseWriter, req *http.Request) {
6        body, err := ioutil.ReadAll(req.Body)
7        if err != nil {
8            panic(err)
9        }
10        err = json.Unmarshal(body, &loggedInUser)
11        if err != nil {
12            panic(err)
13        }
14        json.NewEncoder(rw).Encode(loggedInUser)
15    }
16    
17    // [...]

Above, we receive a new user's details in a POST request and bind it to an instance of the user struct. We further use this user instance to check if a user is logged in or not

Lastly, after the function above, let’s add the code for the /pusher/auth endpoint:

1// File: ./presence.go
2    
3    // [...]
4    
5    // -------------------------------------------------------
6    // Here, we authorize users so that they can subscribe to 
7    // the presence channel
8    // -------------------------------------------------------
9    
10    func pusherAuth(res http.ResponseWriter, req *http.Request) {
11        params, _ := ioutil.ReadAll(req.Body)
12        
13        data := pusher.MemberData{
14            UserId: loggedInUser.Username,
15            UserInfo: map[string]string{
16                "email": loggedInUser.Email,
17            },
18        }
19    
20        response, err := client.AuthenticatePresenceChannel(params, data)
21        if err != nil {
22            panic(err)
23        }
24    
25        fmt.Fprintf(res, string(response))
26    }
27    
28    // [...]

To ensure that every connected user has a unique presence, we used the properties of the global loggedInUser variable in setting the pusher.MemberData instance.

The syntax for authenticating a Pusher presence channel is:

    client.AuthenticatePresenceChannel(params, presenceData)

Building the frontend

Next, in the root of the project, create a static folder. Create two files the directory named index.html and dashboard.html. In the index.html file, we will write the HTML code that allows users to connect to the live streaming application using their name and email.

Setting up the connection page

Open the index.html file and update it with the following code:

1<!-- File: ./static/index.html -->
2    <!DOCTYPE html>
3    <html lang="en">
4        <head>
5            <meta charset="utf-8">
6            <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
7            <title>Live streamer</title>
8            <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css">
9            <style>
10                  :root {
11                    --input-padding-x: .75rem;
12                    --input-padding-y: .75rem;
13                  }
14                  html,
15                  body, body > div {
16                    height: 100%;
17                  }
18                  body > div {
19                    display: -ms-flexbox;
20                    display: flex;
21                    -ms-flex-align: center;
22                    align-items: center;
23                    padding-top: 40px;
24                    padding-bottom: 40px;
25                    background-color: #f5f5f5;
26                  }
27                  .form-signin {
28                    width: 100%;
29                    max-width: 420px;
30                    padding: 15px;
31                    margin: auto;
32                  }
33                  .form-label-group {
34                    position: relative;
35                    margin-bottom: 1rem;
36                  }
37                  .form-label-group > input,
38                  .form-label-group > label {
39                    padding: var(--input-padding-y) var(--input-padding-x);
40                  }
41                  .form-label-group > label {
42                    position: absolute;
43                    top: 0;
44                    left: 0;
45                    display: block;
46                    width: 100%;
47                    margin-bottom: 0; /* Override default `<label>` margin */
48                    line-height: 1.5;
49                    color: #495057;
50                    cursor: text; /* Match the input under the label */
51                    border: 1px solid transparent;
52                    border-radius: .25rem;
53                    transition: all .1s ease-in-out;
54                  }
55                  .form-label-group input::-webkit-input-placeholder {
56                    color: transparent;
57                  }
58                  .form-label-group input:-ms-input-placeholder {
59                    color: transparent;
60                  }
61                  .form-label-group input::-ms-input-placeholder {
62                    color: transparent;
63                  }
64                  .form-label-group input::-moz-placeholder {
65                    color: transparent;
66                  }
67                  .form-label-group input::placeholder {
68                    color: transparent;
69                  }
70                  .form-label-group input:not(:placeholder-shown) {
71                    padding-top: calc(var(--input-padding-y) + var(--input-padding-y) * (2 / 3));
72                    padding-bottom: calc(var(--input-padding-y) / 3);
73                  }
74                  .form-label-group input:not(:placeholder-shown) ~ label {
75                    padding-top: calc(var(--input-padding-y) / 3);
76                    padding-bottom: calc(var(--input-padding-y) / 3);
77                    font-size: 12px;
78                    color: #777;
79                  }
80            </style>
81          </head>
82    
83          <body>
84            <div id="app">
85              <form class="form-signin">
86                <div class="text-center mb-4">
87                  <img class="mb-4" src="https://www.onlinelogomaker.com/blog/wp-content/uploads/2017/07/Fotolia_117855281_Subscription_Monthly_M.jpg" alt="" width="72" height="72">
88                  <h1 class="h3 mb-3 font-weight-normal">Live streamer</h1>
89                  <p>STREAM YOUR FAVOURITE VIDEOS FOR FREE</p>
90                </div>
91                <div class="form-label-group">
92                    <input type="name" id="inputUsername" ref="username" class="form-control" placeholder="Username" required="" autofocus="">
93                      <label for="inputUsername">Username</label>
94                  </div>
95    
96                <div class="form-label-group">
97                  <input type="email" id="inputEmail" ref="email" class="form-control" placeholder="Email address" autofocus="" required>
98                    <label for="inputEmail">Email address</label>
99                </div>
100    
101                <button class="btn btn-lg btn-primary btn-block" type="submit" @click.prevent="login">Connect</button>
102                <p class="mt-5 mb-3 text-muted text-center">© 2017-2018</p>
103              </form>
104              </div>
105    
106              <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
107        </body>
108    </html>

On line 106, we added Vue using a CDN. Let’s add the Vue script for the page.

Before the closing body tag add the following code:

1<script>
2      var app = new Vue({
3        el: '#app',
4        methods: {
5          login: function () {
6            let username = this.$refs.username.value
7            let email = this.$refs.email.value
8    
9            fetch('new/user', {
10              method: 'POST',
11              headers: {
12                'Accept': 'application/json',
13                'Content-Type': 'application/json'
14              },
15              body: JSON.stringify({username, email})
16            })
17            .then(res => res.json())
18            .then(data => window.location.replace('/dashboard.html'))
19          }
20        }
21      })
22    </script>

This script above submits user data to the backend Go server and navigates the browser’s location to the dashboard’s URL.

Next, let’s build the dashboard.

Setting up the dashboard

Open the dashboard.html file and update it with the following code:

1<!-- File: ./static/dashboard.html -->
2    <!DOCTYPE html>
3    <html lang="en">
4      <head>
5        <meta charset="utf-8">
6        <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
7        <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css">
8        <title>Live streamer | Dashboard</title>
9      </head>
10      <body>
11        <div id="app">
12          <div class="container-fluid row shadow p-1 mb-3">
13            <div class="col-3">
14              <img class="ml-3" src="https://www.onlinelogomaker.com/blog/wp-content/uploads/2017/07/Fotolia_117855281_Subscription_Monthly_M.jpg" height="72px" width="72px"/>
15            </div>
16            <div class="col-6 ml-auto mt-3">
17              <div class="input-group">
18                <input type="text" class="form-control" aria-label="Text input with dropdown button">
19                <div class="input-group-append">
20                  <button class="btn btn-outline-secondary dropdown-toggle" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">Search</button>
21                </div>
22              </div>
23            </div>
24            <div class="col-3 float-right">
25              <img src="https://www.seoclerk.com/pics/319222-1IvI0s1421931178.png"  height="72px" width="72px" class="rounded-circle border"/>
26              <p class="mr-auto mt-3 d-inline"> {{ username }} </p>
27            </div>
28          </div>
29          <div class="container-fluid">
30            <div class="row">
31              <div class="col-8">
32                <div class="embed-responsive embed-responsive-16by9">
33                  <iframe width="854" height="480" class="embed-responsive-item" src="https://www.youtube.com/embed/VYOjWnS4cMY" frameborder="0" allow="autoplay; encrypted-media" allowfullscreen></iframe>
34                </div>
35                <div class="text-center mt-3 p-3 text-muted font-weight-bold border">
36                  {{ member }} person(s) is/are currently viewing this video 
37                  <hr>
38                  <li class="m-auto text-success" v-for="member in connectedMembers">
39                    {{ member }}
40                  </li>
41                </div>
42              </div>
43              <div class="col-4 border text-justify" style="background: #e0e0e0; height: 30em; overflow-y: scroll; position: relative;">
44                <div class="border invisible h-50 w-75 text-center" ref="added" style="font-size: 2rem; position: absolute; right: 0; background: #48cbe0">{{ addedMember }} just started watching.</div>
45                <div class="border invisible h-50 w-75 text-center" ref="removed" style="font-size: 2rem; position: absolute; right: 0; background: #ff8325">{{ removedMember }} just stopped watching.</div>
46                <div class="h-75 text-center">
47                  <h2 class="text-center my-3"> Lyrics </h2>
48                  <p class="w-75 m-auto" style="font-size: 1.5rem">
49                    We just wanna party<br>
50                    Party just for you<br>
51                    We just want the money<br>
52                    Money just for you<br>
53                    I know you wanna party<br>
54                    Party just for me<br>
55                    Girl, you got me dancin' (yeah, girl, you got me dancin')<br>
56                    Dance and shake the frame<br>
57                    We just wanna party (yeah)<br>
58                    Party just for you (yeah)<br>
59                    We just want the money (yeah)<br>
60                    Money just for you (you)<br>
61                    I know you wanna party (yeah)<br>
62                    Party just for me (yeah)<br>
63                    Girl, you got me dancin' (yeah, girl, you got me dancin')<br>
64                    Dance and shake the frame (you)<br>
65                    This is America<br>
66                    Don't catch you slippin' up<br>
67                    Don't catch you slippin' up<br>
68                    Look what I'm whippin' up<br>
69                    This is America (woo)<br>
70                    Don't catch you slippin' up<br>
71                    Don't catch you slippin' up<br>
72                    Look what I'm whippin' up<br>
73                  </p>
74                </div>
75              </div>
76            </div>
77          </div>
78        </div>
79        <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
80        <script src="https://js.pusher.com/4.2/pusher.min.js"></script>
81      </body>
82    </html>

IMPORTANT: Video is an embed from YouTube and may not play depending on your region.

On line 80 we imported the JavaScript Pusher library so let’s add some code to utilize it. Before the closing body tag, add the following code:

1<script>
2    var app = new Vue({
3        el: '#app',
4        data: {
5            username: '',
6            member: 0,
7            addedMember: '',
8            removedMember: '',
9            connectedMembers: []
10        },
11    
12        created() {
13            fetch('/isLoggedIn', {
14                method: 'GET',
15                headers: {
16                    'Accept': 'application/json',
17                    'Content-Type': 'application/json'
18                }
19            })
20            .then(res => res.json())
21            .then(data => {
22                if (data != 'false') {
23                    this.username = data.username
24                } else {
25                    window.location.replace('/')
26                }
27            })
28    
29            this.subscribe()
30        },
31    
32        methods: {
33            subscribe: function () {
34                const pusher = new Pusher('PUSHER_APP_KEY', {
35                    authEndpoint: '/pusher/auth',
36                    cluster: 'PUSHER_APP_CLUSTER',
37                    encrypted: true
38                });
39    
40                let channel = pusher.subscribe('presence-channel')
41    
42                channel.bind('pusher:subscription_succeeded', data => {
43                    this.member = data.count
44                    data.each(member => this.connectedMembers.push(member.id))
45                })
46    
47                // Display a notification when a member comes online
48                channel.bind('pusher:member_added', data => {
49                    this.member++
50                    this.connectedMembers.push(data.id)
51                    this.addedMember = data.id
52    
53                    this.$refs.added.classList.add('visible')
54                    this.$refs.added.classList.remove('invisible')
55    
56                    window.setTimeout(() => {
57                        this.$refs.added.classList.remove('visible');
58                        this.$refs.added.classList.add('invisible');
59                    }, 3000)
60                });
61    
62                // Display a notification when a member goes offline
63                channel.bind('pusher:member_removed', data => {
64                    this.member--
65                    let index = this.connectedMembers.indexOf(data.id)
66    
67                    if (index > -1) {
68                        this.connectedMembers.splice(index, 1)
69                    }
70    
71                    this.removedMember = data.id
72                    this.$refs.removed.classList.add('visible')
73                    this.$refs.removed.classList.remove('invisible')
74    
75                    window.setTimeout(() => {
76                        this.$refs.removed.classList.remove('visible')
77                        this.$refs.removed.classList.add('invisible')
78                    }, 3000)
79                })
80            }
81        }
82    })
83    </script>

In the snippet above, we created some Vue data variables to display reactive updates on different parts of the DOM. We also registered a created() lifecycle hook that checks if a user is connected on the backend server and eligible to view the dashboard before calling the subscribe() method.

The subscribe() method first configures a Pusher instance using the keys provided on the dashboard then subscribes to a presence channel. Next, it binds to several events that are available on the returned object of a presence channel subscription.

In the callback function of these bindings, we are able to update the state of the data variables, this is how we display the visual updates on user presence in this application.

Testing the application

We can test the application by compiling down the Go source code and running it with this command:

    $ go run presence.go

The application will be available for testing on this address http://127.0.0.1:8090, here’s a display of how the application should look:

go-online-presence-demo

Conclusion

In this tutorial, we have learned how to leverage the Pusher SDK in creating a live streaming application powered by a Go backend server.

The source code for this tutorial is available on GitHub.