Products
channels_full-logo

Build scalable realtime features

beams_full-logo

Programmatic push notifications

Developers

Docs

Read the docs to learn how to use our products

Tutorials

Explore our tutorials to build apps with Pusher products

Support

Reach out to our support team for help and advice

Sign in
Sign up

Notify Slack users of push notification status with webhooks

  • Lanre Adelowo

December 18th, 2019
You will need to have Android Studio, Ngrok and Go 1.12+ installed on your machine.

Pusher Beams makes it extremely easy to send push notifications to all of your iOS and Android users with a single request. In this tutorial, I will be introducing Pusher Beams webhooks and how you can take advantage of them to send a notification to Slack every time a user reads/opens the sent notification.

Prerequisites

To follow along in this tutorial you need the following things:

  • Android Studio.
  • A Pusher account. You can create one here.
  • Ngrok. We are going to build a server in this tutorial and we need to expose it to the internet so as to add it as a webhook for Pusher Beams. You can download Ngrok from its official website.
  • Go >=1.12.

To get started, you will need to create a new directory for this tutorial called beams-webhook-go. This will house both the code for the server and iOS application. It can be done with the following command:

    $ mkdir beams-webhook-go

Creating the Slack app

As explained at the introduction of this tutorial, we need to create a new Slack app. To do that, you need to visit the Slack apps’ page. You will then need to click on the New App button which will display the screenshot below. Once the fields have been filled correctly, push the Create App button to continue and create the app.

Once the app has been created, you will need to create a new Incoming webhook. This will require you to select a channel where events the app handles will be posted to. In the screenshot below, I have selected the random channel.

)

Please take note of the URL that was generated. It will be needed in the next section when creating the server.

Setting up Firebase

Log in to or create a Firebase account here and go to your console. If you do not already have a project created you will need to create one and name it anything you like, if you have a project select it. Within the Project Overview page select Add App and follow the instruction for creating a new Android application.

Once you have completed the setup for your Android app you will be returned to the dashboard. You will need to go to the project settings (found within the “settings cog” in the top left). Once in the project settings select the Cloud Messaging tab. Copy the Server Key you will need it for setting up your Pusher Beams instance.

Creating the Pusher Beams app

Log in or create an account to access your dashboard here. Create a new beams instance using the dashboard.

Once the Pusher Beams instance have been created, you will be presented with a quickstart screen. You will need to complete step one of the Android setup guide, by providing the FCM server key you copied earlier and select Continue.

You will need to copy your Pusher Beams instance ID and secret key as they will be needed when building the server in the next section.

Creating the backend server

We need to keep track of users, authenticate them. This is needed so we can make use of the authenticated users feature of Pusher Beams. To get started, you need to create another folder called server in the beams-webhook-go that was created earlier. To do that, you can make use of the following command:

    $ mkdir beams-webhook-go
    $ cd beams-webhook-go
    $ mkdir server
    $ cd server

The next thing to do is to create a .env file which will contain secrets and variables needed to connect to Pusher Beams. To create the file, you can run the following command:

    $ touch .env

In this newly created file, you need to paste the following in it:

    // beams-webhook-go/server/.env
    PUSHER_BEAMS_INSTANCE_ID=YOUR_PUSHER_BEAMS_INSTANCE_ID
    PUSHER_BEAMS_SECRET_KEY=YOUR_PUSHER_BEAMS_SECRET
    PUSHER_BEAMS_WEBHOOK_SECRET=YOUR_PUSHER_WEBHOOK_SECRETS
    SLACK_HOOKS_URL=SLACK_HOOKS_URL

You need to substitute the correct values in the above file. You can get the PUSHER_BEAMS_* values from the dashboard. PUSHER_BEAMS_WEBHOOK_SECRET can be anything you want as we’d use it to verify the request is coming from Pusher alone.

The next thing is to create two files -  `main.go`  and a `slack.go` - which will contain the logic for our server. We need to create three routes:
  • /auth: since we are building personalized notifications in this tutorial, we need to identify and differentiate a user from others so we can be certain only he/she receives the push notification.
  • /push : we need to be simulate an actual Push notification request. Whatever information is sent to the route will be published to an already authenticated user.
  • /slack: this will act as the webhook URL that is going to be added in the Pusher Beams dashboard.

You can create the aforementioned files with the following command:

    $ touch main.go slack.go

Since we are using Go modules, you will need to also create a go.mod file. It can be done automatically by running go mod init. The next thing is to actually build the server. In main.go paste the following content:

    // beams-webhook-go/server/main.go

    package main

    import (
            "encoding/json"
            "flag"
            "fmt"
            "log"
            "net/http"
            "os"

            "github.com/joho/godotenv"
            pushnotifications "github.com/pusher/push-notifications-go"
    )

    func main() {
            err := godotenv.Load()
            if err != nil {
                    log.Fatal("Error loading .env file")
            }

            port := flag.Int64("http.port", 3000, "Port to run HTTP server on")

            flag.Parse()

            beamsClient, err := pushnotifications.New(os.Getenv("PUSHER_BEAMS_INSTANCE_ID"), os.Getenv("PUSHER_BEAMS_SECRET_KEY"))
            if err != nil {
                    log.Fatalf("Could not set up Push Notifications client... %v", err)
            }

            mux := http.NewServeMux()

            mux.HandleFunc("/push", createPushNotificationHandler(beamsClient))
            mux.HandleFunc("/auth", authenticateUser(beamsClient))
            mux.HandleFunc("/slack", handleWebhook)

            if err := http.ListenAndServe(fmt.Sprintf(":%d", *port), mux); err != nil {
                    log.Fatal(err)
            }
    }

    var currentUser = ""

    func authenticateUser(client pushnotifications.PushNotifications) http.HandlerFunc {
            return func(w http.ResponseWriter, r *http.Request) {

                    userIDinQueryParam := r.URL.Query().Get("user_id")

                    beamsToken, err := client.GenerateToken(userIDinQueryParam)
                    if err != nil {
                            w.WriteHeader(http.StatusInternalServerError)
                            return
                    }

                    currentUser = userIDinQueryParam

                    beamsTokenJson, err := json.Marshal(beamsToken)
                    if err != nil {
                            w.WriteHeader(http.StatusInternalServerError)
                            return
                    }

                    w.WriteHeader(http.StatusOK)
                    w.Write(beamsTokenJson)
            }
    }

    func createPushNotificationHandler(client pushnotifications.PushNotifications) http.HandlerFunc {
            return func(w http.ResponseWriter, r *http.Request) {

                    if r.Method != http.MethodPost {
                            w.WriteHeader(http.StatusMethodNotAllowed)
                            return
                    }

                    var data map[string]interface{}

                    type response struct {
                            Status  bool   `json:"status"`
                            Message string `json:"message"`
                    }

                    if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
                            w.WriteHeader(http.StatusBadRequest)
                            encode(w, response{
                                    Status:  false,
                                    Message: "Invalid bad request",
                            })
                            return
                    }

                    publishRequest := map[string]interface{}{
                            "apns": map[string]interface{}{
                                    "aps": map[string]interface{}{
                                            "alert": data,
                                    },
                            },
                            "fcm": map[string]interface{}{
                                    "notification": data,
                            },
                    }

                    _, err := client.PublishToUsers([]string{currentUser}, publishRequest)
                    if err != nil {
                            log.Println(err)
                            w.WriteHeader(http.StatusInternalServerError)
                            encode(w, response{
                                    Status:  false,
                                    Message: "Could not send push notification",
                            })
                            return
                    }

                    w.WriteHeader(http.StatusOK)
                    encode(w, response{
                            Status:  true,
                            Message: "Push notification sent successfully",
                    })
            }
    }

    var encode = func(w http.ResponseWriter, v interface{}) {
            _ = json.NewEncoder(w).Encode(v)
    }

In the above, we load the values we saved in the .env, connect to Pusher Beams and also create an HTTP server. The needed routes have also been created. But a missing part is handleWebhook which doesn’t exist yet. We will be implementing that in the slack.go file. You will need to open the slack.go file and paste the following contents:

    // beams-webhook-go/server/slack.go

    package main

    import (
            "bytes"
            "crypto/hmac"
            "crypto/sha1"
            "encoding/hex"
            "encoding/json"
            "io"
            "net/http"
            "os"
    )

    func handleWebhook(w http.ResponseWriter, r *http.Request) {

            hasher := hmac.New(sha1.New, []byte(os.Getenv("PUSHER_BEAMS_WEBHOOK_SECRET")))

            type response struct {
                    Message string `json:"message"`
                    Status  bool   `json:"status"`
            }

            if r.Header.Get("Webhook-Event-Type") != "v1.UserNotificationOpen" {
                    w.WriteHeader(http.StatusOK)
                    encode(w, response{
                            Message: "Ok",
                            Status:  true,
                    })
                    return
            }

            if _, err := io.Copy(hasher, r.Body); err != nil {
                    w.WriteHeader(http.StatusBadRequest)
                    encode(w, response{
                            Message: "Could not create crypto hash",
                            Status:  false,
                    })
                    return
            }

            expectedHash := hex.EncodeToString(hasher.Sum(nil))

            if expectedHash != r.Header.Get("webhook-signature") {
                    w.WriteHeader(http.StatusBadRequest)
                    encode(w, response{
                            Message: "Invalid webhook signature",
                            Status:  false,
                    })
                    return
            }

            var request struct {
                    Message string `json:"text"`
            }

            request.Message = "User opened a notification just now"

            var buf = new(bytes.Buffer)

            _ = json.NewEncoder(buf).Encode(request)

            req, err := http.NewRequest(http.MethodPost, os.Getenv("SLACK_HOOKS_URL"), buf)
            if err != nil {
                    w.WriteHeader(http.StatusInternalServerError)
                    encode(w, response{
                            Message: "Could not send notification to Slack",
                            Status:  false,
                    })
                    return
            }

            resp, err := http.DefaultClient.Do(req)
            if err != nil {
                    w.WriteHeader(http.StatusInternalServerError)
                    encode(w, response{
                            Message: "Error while pinging Slack",
                            Status:  false,
                    })
                    return
            }

            if resp.StatusCode > http.StatusAccepted {
                    w.WriteHeader(http.StatusInternalServerError)
                    encode(w, response{
                            Message: "Unexpected response from Slack",
                            Status:  false,
                    })
                    return
            }

            w.WriteHeader(http.StatusOK)
            encode(w, response{
                    Message: "Message sent to Slack successfully",
                    Status:  true,
            })
    }

In the above, we only care about webhooks that are of the type, v1.UserNotificationOpen. There are also other types of webhook events but this is the one of utmost concern in the tutorial. After which we verify that Pusher actually made the request, that is done by generating a hash of the request body with the key we added to the PUSHER_BEAMS_WEBHOOK_SECRET in the .env file earlier, then match it with what was sent in the request headers. If they both match, we can be certain it is a valid request.

The next thing to do is to run the server. This can be done by running the following commands:

    $ go build
    $ go mod tidy ## This step can be skipped
    $ ./server

At this point, the server will be running at port 3000. You will then need to start ngrok as the server needs to be exposed to the internet so it can be added as a webhook in the Pusher Beams dashboard. To do that, run the following command:

    $ ngrok http 3000

If the above command succeeds, you will be presented with a URL, copy it as you will be needing it in a bit.

The next thing to do is to visit your Pusher Beams dashboard, select your app and visit the Settings tab.

Paste the URL the ngrok command outputted, then append /slack to it. Also, the secret that was defined in the .env file as PUSHER_BEAMS_WEBHOOK_SECRET will need to be added here.

Creating the Android app

In this section, we are going to create a very basic Android app that actually doesn’t show anything to the user except for the Push notification. To do this, you will need to create a new Empty Activity project using Android Studio. You can name it PusherBeamsSlackWebhook. Provide a Package name, you need to make sure the package name matches what was provided when setting up Firebase earlier in the tutorial.

Please note that this project should be created in the beams-webhook-go folder so it lives side by side with the server directory

The next step is to add the dependencies needed, you need to update the app/build.gradle with:

    // beams-webhook-go/PusherBeamsSlackWebhook/app/build.gradle

    dependencies {
       ...
        implementation 'com.google.firebase:firebase-core:16.0.9'
        implementation 'com.google.firebase:firebase-messaging:18.0.0'
        implementation 'com.pusher:push-notifications-android:1.4.6'
       ... 
    }

    apply plugin: 'com.google.gms.google-services'

The next step is to visit the Firebase dashboard to download the google-services.json file then add it to the project. After which you will need to synchronize Gradle by pressing the Sync Now button.

Once the above succeeds, you will then need to actually implement the Pusher Beams SDK. That can be done by opening the MainActivity.kt file and replacing its entire content with the following:

    // beams-webhook-go/PusherBeamsSlackWebhook/app/src/main/java/com/example/pusherbeamsslackwebhook/MainActivity.kt

    package com.example.pusherbeamsslackwebhook

    import android.content.SharedPreferences
    import androidx.appcompat.app.AppCompatActivity
    import android.os.Bundle
    import android.util.Log
    import com.pusher.pushnotifications.*
    import com.pusher.pushnotifications.auth.AuthData
    import com.pusher.pushnotifications.auth.AuthDataGetter
    import com.pusher.pushnotifications.auth.BeamsTokenProvider
    import java.util.*

    class MainActivity : AppCompatActivity() {

        private val PREF_NAME = "uuid-generated"

        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_main)


            PushNotifications.start(applicationContext, "PUSHER_INSTANCE_ID")
            PushNotifications.addDeviceInterest("webhook-slack")

            val sharedPref: SharedPreferences = getSharedPreferences(PREF_NAME, 0)

            if (!sharedPref.getBoolean(PREF_NAME, false)) {

                var uuid = UUID.randomUUID().toString()

                val serverUrl = "https://NGROK.ngrok.io/auth?user_id=${uuid}"
                val tokenProvider = BeamsTokenProvider(serverUrl,
                    object : AuthDataGetter {
                        override fun getAuthData(): AuthData {
                            return AuthData(
                                headers = hashMapOf()
                            )
                        }
                    })


                PushNotifications.setUserId(
                    uuid,
                    tokenProvider,
                    object : BeamsCallback<Void, PusherCallbackError> {
                        override fun onFailure(error: PusherCallbackError) {
                            Log.e(
                                "BeamsAuth",
                                "Could not login to Beams: ${error.message}"
                            )
                        }

                        override fun onSuccess(vararg values: Void) {
                            Log.i("BeamsAuth", "Beams login success")
                        }
                    }
                )
                val editor = sharedPref.edit()
                editor.putBoolean(PREF_NAME, true)
                editor.apply()
            }
        }
    }

Please remember to replace PUSHER_INSTANCE_ID with the actual value gotten from the Pusher Beams dashboard.

Finally, you can run the application now.

Testing the implementation

Remember we created a push route in our server earlier, you will make use of it to create a push notification that will be sent to the application. That can be done with the following command:

    $ curl localhost:3000/push -X POST -d '{"title" : "Here is a new push notification"}'

Once the above command succeeds, a push notification will be sent to the device. You will need to open it - the push notification. After which you can take a look at the Slack workspace and channel which you configured earlier. The channel should have messages similar to the below screenshot.

Conclusion

In this tutorial we have learnt how to integrate Pusher Beams webhooks and Slack. As always, the entire code can be found on GitHub.

Clone the project repository
  • Android
  • Beams
  • Go
  • Kotlin
  • Webhooks
  • Beams

Products

  • Channels
  • Beams

© 2020 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.