We're hiring
Products

Channels

Beams

Chatkit

DocsTutorialsSupportCareersPusher Blog
Sign InSign Up
Products

Channels

Build scalable, realtime features into your apps

Features Pricing

Beams

Send push notifications programmatically at scale

Pricing

Chatkit

Build chat into your app in hours, not days

Pricing
Developers

Docs

Read the docs to learn how to use our products

Channels Beams Chatkit

Tutorials

Explore our tutorials to build apps with Pusher products

Support

Reach out to our support team for help and advice

Status

Check on the status of any of our products

Products

Channels

Build scalable, realtime features into your apps

Features Pricing

Beams

Send push notifications programmatically at scale

Pricing

Chatkit

Build chat into your app in hours, not days

Pricing
Developers

Docs

Read the docs to learn how to use our products

Channels Beams Chatkit

Tutorials

Explore our tutorials to build apps with Pusher products

Support

Reach out to our support team for help and advice

Status

Check on the status of any of our products

Sign InSign Up

Create a Go application with online presence

  • Neo Ighodaro
August 8th, 2018
You will need Go 0.10+ installed on your machine.

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 for the development.

Here’s a demo of the final application:

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).
  • A Pusher application. Create one here.

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.

    $ cd $GOPATH/src
    $ mkdir go-pusher-presence-app
    $ 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:

    // File: ./presence.go
    package main

    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 {
        Username  string `json:"username" xml:"username" form:"username" query:"username"`
        Email string `json:"email" xml:"email" form:"email" query:"email"`
    }

    var loggedInUser user

    func main() {
        // Define our routes
        http.Handle("/", http.FileServer(http.Dir("./static")))
        http.HandleFunc("/isLoggedIn", isUserLoggedIn)
        http.HandleFunc("/new/user", NewUser)
        http.HandleFunc("/pusher/auth", pusherAuth)

        // Start executing the application on port 8090
        log.Fatal(http.ListenAndServe(":8090", nil))
    }

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:

  1. / - loads all the static files from the static directory.
  2. /isLoggedIn - checks if a user is logged in or not and returns a fitting message.
  3. /new/user - allows a new user to connect and initializes the global user instance.
  4. /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:

    // File: ./presence.go

    // [...]

    func isUserLoggedIn(rw http.ResponseWriter, req *http.Request){
        if loggedInUser.Username != "" && loggedInUser.Email != "" {
            json.NewEncoder(rw).Encode(loggedInUser)
        } else {
            json.NewEncoder(rw).Encode("false")
        }
    }

    // [...]

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

    // File: ./presence.go

    // [...]

    func NewUser(rw http.ResponseWriter, req *http.Request) {
        body, err := ioutil.ReadAll(req.Body)
        if err != nil {
            panic(err)
        }
        err = json.Unmarshal(body, &loggedInUser)
        if err != nil {
            panic(err)
        }
        json.NewEncoder(rw).Encode(loggedInUser)
    }

    // [...]

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:

    // File: ./presence.go

    // [...]

    // -------------------------------------------------------
    // Here, we authorize users so that they can subscribe to 
    // the presence channel
    // -------------------------------------------------------

    func pusherAuth(res http.ResponseWriter, req *http.Request) {
        params, _ := ioutil.ReadAll(req.Body)

        data := pusher.MemberData{
            UserId: loggedInUser.Username,
            UserInfo: map[string]string{
                "email": loggedInUser.Email,
            },
        }

        response, err := client.AuthenticatePresenceChannel(params, data)
        if err != nil {
            panic(err)
        }

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

    // [...]

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:

    <!-- File: ./static/index.html -->
    <!DOCTYPE html>
    <html lang="en">
        <head>
            <meta charset="utf-8">
            <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
            <title>Live streamer</title>
            <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css">
            <style>
                  :root {
                    --input-padding-x: .75rem;
                    --input-padding-y: .75rem;
                  }
                  html,
                  body, body > div {
                    height: 100%;
                  }
                  body > div {
                    display: -ms-flexbox;
                    display: flex;
                    -ms-flex-align: center;
                    align-items: center;
                    padding-top: 40px;
                    padding-bottom: 40px;
                    background-color: #f5f5f5;
                  }
                  .form-signin {
                    width: 100%;
                    max-width: 420px;
                    padding: 15px;
                    margin: auto;
                  }
                  .form-label-group {
                    position: relative;
                    margin-bottom: 1rem;
                  }
                  .form-label-group > input,
                  .form-label-group > label {
                    padding: var(--input-padding-y) var(--input-padding-x);
                  }
                  .form-label-group > label {
                    position: absolute;
                    top: 0;
                    left: 0;
                    display: block;
                    width: 100%;
                    margin-bottom: 0; /* Override default `<label>` margin */
                    line-height: 1.5;
                    color: #495057;
                    cursor: text; /* Match the input under the label */
                    border: 1px solid transparent;
                    border-radius: .25rem;
                    transition: all .1s ease-in-out;
                  }
                  .form-label-group input::-webkit-input-placeholder {
                    color: transparent;
                  }
                  .form-label-group input:-ms-input-placeholder {
                    color: transparent;
                  }
                  .form-label-group input::-ms-input-placeholder {
                    color: transparent;
                  }
                  .form-label-group input::-moz-placeholder {
                    color: transparent;
                  }
                  .form-label-group input::placeholder {
                    color: transparent;
                  }
                  .form-label-group input:not(:placeholder-shown) {
                    padding-top: calc(var(--input-padding-y) + var(--input-padding-y) * (2 / 3));
                    padding-bottom: calc(var(--input-padding-y) / 3);
                  }
                  .form-label-group input:not(:placeholder-shown) ~ label {
                    padding-top: calc(var(--input-padding-y) / 3);
                    padding-bottom: calc(var(--input-padding-y) / 3);
                    font-size: 12px;
                    color: #777;
                  }
            </style>
          </head>

          <body>
            <div id="app">
              <form class="form-signin">
                <div class="text-center mb-4">
                  <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">
                  <h1 class="h3 mb-3 font-weight-normal">Live streamer</h1>
                  <p>STREAM YOUR FAVOURITE VIDEOS FOR FREE</p>
                </div>
                <div class="form-label-group">
                    <input type="name" id="inputUsername" ref="username" class="form-control" placeholder="Username" required="" autofocus="">
                      <label for="inputUsername">Username</label>
                  </div>

                <div class="form-label-group">
                  <input type="email" id="inputEmail" ref="email" class="form-control" placeholder="Email address" autofocus="" required>
                    <label for="inputEmail">Email address</label>
                </div>

                <button class="btn btn-lg btn-primary btn-block" type="submit" @click.prevent="login">Connect</button>
                <p class="mt-5 mb-3 text-muted text-center">© 2017-2018</p>
              </form>
              </div>

              <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
        </body>
    </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:

    <script>
      var app = new Vue({
        el: '#app',
        methods: {
          login: function () {
            let username = this.$refs.username.value
            let email = this.$refs.email.value

            fetch('new/user', {
              method: 'POST',
              headers: {
                'Accept': 'application/json',
                'Content-Type': 'application/json'
              },
              body: JSON.stringify({username, email})
            })
            .then(res => res.json())
            .then(data => window.location.replace('/dashboard.html'))
          }
        }
      })
    </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:

    <!-- File: ./static/dashboard.html -->
    <!DOCTYPE html>
    <html lang="en">
      <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
        <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css">
        <title>Live streamer | Dashboard</title>
      </head>
      <body>
        <div id="app">
          <div class="container-fluid row shadow p-1 mb-3">
            <div class="col-3">
              <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"/>
            </div>
            <div class="col-6 ml-auto mt-3">
              <div class="input-group">
                <input type="text" class="form-control" aria-label="Text input with dropdown button">
                <div class="input-group-append">
                  <button class="btn btn-outline-secondary dropdown-toggle" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">Search</button>
                </div>
              </div>
            </div>
            <div class="col-3 float-right">
              <img src="https://www.seoclerk.com/pics/319222-1IvI0s1421931178.png"  height="72px" width="72px" class="rounded-circle border"/>
              <p class="mr-auto mt-3 d-inline"> {{ username }} </p>
            </div>
          </div>
          <div class="container-fluid">
            <div class="row">
              <div class="col-8">
                <div class="embed-responsive embed-responsive-16by9">
                  <iframe width="854" height="480" class="embed-responsive-item" src="https://www.youtube.com/embed/VYOjWnS4cMY" frameborder="0" allow="autoplay; encrypted-media" allowfullscreen></iframe>
                </div>
                <div class="text-center mt-3 p-3 text-muted font-weight-bold border">
                  {{ member }} person(s) is/are currently viewing this video 
                  <hr>
                  <li class="m-auto text-success" v-for="member in connectedMembers">
                    {{ member }}
                  </li>
                </div>
              </div>
              <div class="col-4 border text-justify" style="background: #e0e0e0; height: 30em; overflow-y: scroll; position: relative;">
                <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>
                <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>
                <div class="h-75 text-center">
                  <h2 class="text-center my-3"> Lyrics </h2>
                  <p class="w-75 m-auto" style="font-size: 1.5rem">
                    We just wanna party<br>
                    Party just for you<br>
                    We just want the money<br>
                    Money just for you<br>
                    I know you wanna party<br>
                    Party just for me<br>
                    Girl, you got me dancin' (yeah, girl, you got me dancin')<br>
                    Dance and shake the frame<br>
                    We just wanna party (yeah)<br>
                    Party just for you (yeah)<br>
                    We just want the money (yeah)<br>
                    Money just for you (you)<br>
                    I know you wanna party (yeah)<br>
                    Party just for me (yeah)<br>
                    Girl, you got me dancin' (yeah, girl, you got me dancin')<br>
                    Dance and shake the frame (you)<br>
                    This is America<br>
                    Don't catch you slippin' up<br>
                    Don't catch you slippin' up<br>
                    Look what I'm whippin' up<br>
                    This is America (woo)<br>
                    Don't catch you slippin' up<br>
                    Don't catch you slippin' up<br>
                    Look what I'm whippin' up<br>
                  </p>
                </div>
              </div>
            </div>
          </div>
        </div>
        <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
        <script src="https://js.pusher.com/4.2/pusher.min.js"></script>
      </body>
    </html>

⚠️ 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:

    <script>
    var app = new Vue({
        el: '#app',
        data: {
            username: '',
            member: 0,
            addedMember: '',
            removedMember: '',
            connectedMembers: []
        },

        created() {
            fetch('/isLoggedIn', {
                method: 'GET',
                headers: {
                    'Accept': 'application/json',
                    'Content-Type': 'application/json'
                }
            })
            .then(res => res.json())
            .then(data => {
                if (data != 'false') {
                    this.username = data.username
                } else {
                    window.location.replace('/')
                }
            })

            this.subscribe()
        },

        methods: {
            subscribe: function () {
                const pusher = new Pusher('PUSHER_APP_KEY', {
                    authEndpoint: '/pusher/auth',
                    cluster: 'PUSHER_APP_CLUSTER',
                    encrypted: true
                });

                let channel = pusher.subscribe('presence-channel')

                channel.bind('pusher:subscription_succeeded', data => {
                    this.member = data.count
                    data.each(member => this.connectedMembers.push(member.id))
                })

                // Display a notification when a member comes online
                channel.bind('pusher:member_added', data => {
                    this.member++
                    this.connectedMembers.push(data.id)
                    this.addedMember = data.id

                    this.$refs.added.classList.add('visible')
                    this.$refs.added.classList.remove('invisible')

                    window.setTimeout(() => {
                        this.$refs.added.classList.remove('visible');
                        this.$refs.added.classList.add('invisible');
                    }, 3000)
                });

                // Display a notification when a member goes offline
                channel.bind('pusher:member_removed', data => {
                    this.member--
                    let index = this.connectedMembers.indexOf(data.id)

                    if (index > -1) {
                        this.connectedMembers.splice(index, 1)
                    }

                    this.removedMember = data.id
                    this.$refs.removed.classList.add('visible')
                    this.$refs.removed.classList.remove('invisible')

                    window.setTimeout(() => {
                        this.$refs.removed.classList.remove('visible')
                        this.$refs.removed.classList.add('invisible')
                    }, 3000)
                })
            }
        }
    })
    </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:

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.

Clone the project repository
  • Go
  • JavaScript
  • Online Presence
  • Vue.js
  • Channels

Products

  • Channels
  • Beams
  • Chatkit

© 2019 Pusher Ltd. All rights reserved.

Pusher Limited is a company registered in England and Wales (No. 07489873) whose registered office is at 160 Old Street, London, EC1V 9BW.