Notify Slack users of push notification status with webhooks
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 asPUSHER_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 theserver
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.
18 December 2019
by Lanre Adelowo