How to Build a Slack App with Node.js & Firebase

How to Build a Slack App with Node.js & Firebase

Learn how to build a Slack App with Node.js & Firebase Cloud Functions. CyberJeff is a simple slack bot that helps automate tasks and improve productivity

Learn how to build a Slack App with Node.js & Firebase Cloud Functions. CyberJeff is a simple slack bot that helps automate tasks and improve productivity

How to Build a Slack App with Node.js and Firebase Cloud Functions?

How to Build a Slack App with Node.js and Firebase Cloud Functions?

In this Firebase Cloud Functions tutorial, you'll learn how to build a Slack App 💬 with Node.js & Firebase Cloud Functions. A guide to serverless Slack Apps with Firebase Cloud Functions & Firestore. CyberJeff is a simple slack bot that helps automate tasks and improve productivity. Slack Apps, or Bots, allow you to extend slack with interactive features that can improve your teams productivity. The following lesson is a step-by-step guide to building a Slack App using Firebase Cloud Functions as the backend server.

Slack Apps, or Bots, allow you to extend slack with interactive features that can improve your teams productivity. The following lesson is a step-by-step guide to building a Slack App using Firebase Cloud Functions as the backend server.

Our Slack App will perform the following tasks.

  • Listen to events, such as a new user joining the #general channel.
  • Retrieve the user’s slack profile.
  • Send a private personalized message.
  • Add a slash command for user-directed actions.
Create a Slack App

At this point, it is assumed you have admin access to a Slack workspace. If not, feel free to create one to follow this tutorial.

Once you have a workspace, create a new Slack App.


Create a Slack App

Verify Cloud Function Ownership

Before Slack can send events to our Cloud Function, it needs to verify that we own the server. Slack performs a handshake by sending an HTTP request with a challenge parameter to the Cloud Function, then the function must respond back with the same value.

Initialize Cloud Functions

Initialize Cloud Functions. This demo uses the TypeScript flavor.

firebase init functions

Build The Challenge Function

The function only needs to respond with the challenge one time.

export const myBot = functions.https.onRequest( (req, res) => {

  // Request from Slack
  const { challenge }  = req.body;

  // Response from You
  res.send({ challenge })

});

Deploy it

firebase deploy --only functions:myBot

Copy the Function URL from the terminal output

Enter The Deployed URL

Subscribe to an event that you want to listen to, then paste in the Function URL from the last step. Slack should automatically verify the URL and give it a green checkmark ✅.


Subscribe to the member_joined_channel event. Notice how our URL is now verified.

Build the Bot

Now it’s time to do some real work.

Install Dependencies

The Slack Node SDK is a monorepo that contains several packages. The Web API can read and modify data in the workspace. Google PubSub will be used to handle long running background tasks in te following steps.

npm install @slack/web-api
npm install @google-cloud/pubsub

OAuth Token

The OAuth token is used to authenticate your bot/server into a workspace so it can interact with your channel (like post messages).


Install the app into your workspace.

Once installed, it will take you directly to the OAuth token. It usually starts with xoxb or xoxp.


Copy the OAuth Token

Copy the OAuth Token and save it as a Firebase Functions environment variable.

firebase functions:config:set slack.token=YOUR-TOKEN

Signing Secrets

When receiving events from Slack, you should validate the signing secret, which can be found in Basic Info panel. This ensures that only requests from Slack can interact with your function by decoding the digital signature of the request.

firebase functions:config:set slack.signing_secret=YOUR-TOKEN

Add Scopes (Permissions)

OAuth scopes define what your app is allowed to do. You can fine tune permissions under the OAuth & Permissions page. Follow the principle of least privledge and only allow your bot access to resources that it actually needs to do its job. In our case, we need to read a user profile and add them to a specific slack channel.

Listen to Events

Our first goal is to listen to events that happen in the Slack workspace. Slack should notify our server anytime a user joins a channel with the member_joined_channel event.

⚡ Your server must respond be quickly, within 3000ms or less, otherwise Slack will timeout and attempt to retry.

So then, how do we build an app that performs a long-running backend process? There are many right answers, but in Firebase, the best option is to enqueue a PubSub Cloud Function. It allows the initial HTTP endpoint to simply hand off the message and respond quickly to Slack.

Import the dependencies and initlizize them with the environment credentials.

import * as functions from 'firebase-functions';

import { WebClient } from '@slack/web-api';
const bot = new WebClient(functions.config().slack.token);

const { PubSub } = require('@google-cloud/pubsub');
const pubsubClient = new PubSub();

HTTP Gateway

The HTTP gateway validates the request, enqueues a PubSub message with the request, then responds with a 200 code to keep Slack happy.

Extra Snippet: verifySlackSignature

export const myBot = functions.https.onRequest( async (req, res) => {

    // Validate Signature
    verifySlackSignature(req); // See snippet above for implementation

    const data = JSON.stringify(req.body);
    const dataBuffer = Buffer.from(data);

    await pubsubClient
            .topic('slack-channel-join')
            .publisher()
            .publish(dataBuffer);

    res.sendStatus(200);

});

PubSub Function

All the heavy-lifting happens in the PubSub function because we have no time-constraints here. The message.json contains the same data you would have handled in req.body in the HTTP function.

  export const slackChannelJoin = functions.pubsub.topic('slack-channel-join')
    .onPublish(async (message, context) => {

    const { event } = message.json; 

    const { user, channel } = event;

    // TODO something cool...

});

Respond To the User as the Bot

In this section, our app makes requests to the Slack API that (1) fetch the user’s Slack profile (2) invite them to a channel, and (3) greet them with a direct message.

Note: The Slack API does not provide typings for the response object, so you’ll have to treat it as any in typescript.

export const slackChannelJoin = functions.pubsub
  .topic('slack-channel-join')
  .onPublish(async (message, context) => {

    const { event } = message.json; 

    const { user, channel } = event;

    // IDs for the channels you plan on working with
    const generalChannel = 'C12345';
    const newChannel = '#froopy-land';

    // Throw error if not on the general channel
    if (channel !== generalChannel) {
        throw Error()
    }

  // Get the full Slack profile

    const userResult = await bot.users.profile.get({ user });
    const { email, display_name } = userResult.profile as any;

    // Invite the slack user to a new channel
    const invite = await bot.channels.invite({
        channel: newChannel,
        user
    });

    // Send a Message
    const chatMessage = await bot.chat.postMessage({
        channel: newChannel,
        text: `Hey ${display_name}! So glad to have you on my Slack!`
    });

});
Additional Ideas to Try

Listen to Slash Commands

Events are great, but somethings you want to give users tools to manually kick off interactivity - that’s where slash commands come in. They work very similar to events, but are are triggered by the user entering /some-command into the workspace.


Slash commands follow the same basic flow as events

Video

Push Notifications Using Node.js and Firebase

Push Notifications Using Node.js and Firebase

In this article, I’m going to share with you how I upgraded a legacy web service in NodeJS/Express for push notification via Firebase. How I Solved A Push Notification Bug on a legacy NodeJS

Ever had that moment where you really want to point the finger at someone else for a buggy feature? Well, I had such and in fact, I had it for weeks!!

Hey guys, been a while I wrote an article and that’s because I having been writing codes 😺. In this article, I’m going to share with you how I upgraded a legacy web service in NodeJS/Express for push notification via Firebase

The Problem

Recently, I inherited a legacy web service written in NodeJS/Express and coincidentally the web push notification the service provided started failing. Hmm, just my luck right?

It so happens that the push notification service provider is Firebase Cloud Messaging and this was working smoothly until it did not and it broke(thanks to Uncle Murphy) in a beta test version.

So clients (Android devices and iOS devices) were not receiving the loved pushed notification. I need not add how upsetting this was for the stakeholders of the project right ?

Putting on my Debugging Hat

So with my detective hat on (feeling like Sherlock Holmes are we now?), I studied how the push notification feature was being implemented on the web service. The first thing I noticed was that the service was using the legacy HTTP protocol as described here.

Noticing that, I decided to use good ‘ol console.log on the response sent from the firebase Cloud Messaging service when it was called using the legacy HTTP protocol and I found this…

It was returning a 403: Forbidden…

So I checked here to see what that error code could mean specific to Firebase and it said:

(HTTP error code = 403) The authenticated sender ID is different from the sender ID for the registration token.

Doing more debugging I really couldn’t see how that was possible since everything seems to be okay(the developer was really following the configuration of the legacy HTTP method to the letter)

Paying the Technical Debt 😰

After weeks of searching StackOverflow and every article I could find on the said matter, I decided to update the legacy API used for calling the Firebase Cloud Messaging service to the most advocated…

Firebase Admin

Using the docs, I did the following:

  1. Installed the Firebase Admin SDK on the Web Service via:
$ npm install firebase-admin --save
  1. Then I went ahead(of course) to import the module using CommonJS syntax:


Requiring the Firebase SDK in my NodeJS/Express Web Service

  1. Now according to the setup docs, you will need to generate a private key file for your service account in order to initialize the Firebase Admin SDK. You can find this by going to the firebase console, selecting your app and clicking on the cog icon to see the settings(The docs also describes this process)

  2. I placed the generated file in my root directory(trust me I .gitignore it 😃). Then I update my code to have this:

  1. I initialized the Firebase Admin SDK to get it rolling:

Are We Pushing Yet?

💦 hope that wasn’t too much to take in? So let’s use the Firebase Admin to send a test push notification to our client apps, shall we?

Hold the Phone?

Before you raise any eyebrows, In the spirit of DRY, I am using a helper function here called: sendPushNotification which takes in the message configuration object as an argument. Below is the definition of the helper function:

So you could see above how the Firebase Admin SDK is being used. Not surprisingly it returns a promise which on successful, returns the message-id of the push notification message. I am simply logging it out here, but you, of course, can do whatever you want(It’s a free world ).

Rounding up…

So there you go, that’s how I saved the day for my stakeholders and it was all smiles when push notification started working like a charm again thanks to the Firebase Admin SDK.

Firebase Flutter Tutorial - Firebase CloudStorage in Flutter

Firebase Flutter Tutorial - Firebase CloudStorage in Flutter

In this Firebase Flutter Tutorial, we wrap the Firebase Storage package into a service to easily manage and upload the firebase storage.

Hello there Flutter Dev 🙋‍♂️ In this tutorial we will be going over Cloudstorge on Firebase and how to integrate that with your mobile application.

Today we'll do a simple task that is probably a very common task in mobile app development. We will provide the user with a UI to select and upload a photo, save that photo in a post and display that to them in a collection. The photo will be stored in Firebase CloudStorage and we'll use a URL to the photo to display to users in the app. We'll start off by updating the UI to allow us to upload a photo.

Cloud Storage Setup

Before we start with the code lets setup our cloud storage. Open up the firebase console and click on the storage icon in the left toolbar. Click on create bucket, choose the location and continue. You will now have what is called a "bucket" where you can store files. You can think of this as a hard drive that you access through a url web request. Each of the files here will have an access token / url that you can access only through that url with the attached token. You can set visibility by controling the access level for that download url token. This is the url we'll use to display the image in the app.

Implementation

Let go over a quick implementation overview. We'll create a service that wraps the provided firebase storage package. This service will take in a File object and a title and upload that to the storage. When the operation is complete we will return the url which is what we'll put inside of our post as the imageUrl. The file that we're passing in will be selected using the UI presented by the image picker library. Let's get to it.

Code setup

We start off by adding the firebase_storage and the image_picker package to the pubspec.

firebase_storage: ^3.1.1
image_picker: ^0.6.3+1

Firebase storage is to interact with the Firebase Cloud Storage, the image picker is to show the user a UI that will allow them to select an image from their device.

Cloud Storage Implementation

Under the services folder create a new file called cloud_storage_service.dart. We'll give it a function called uploadImage that Takes in a required file as well as a title. You can pass in the UID, or anything you'd like to identify your images by.

import 'package:firebase_storage/firebase_storage.dart';

class CloudStorageService {
  Future<CloudStorageResult> uploadImage({
    @required File imageToUpload,
    @required String title,
  }) async {

  }
}

class CloudStorageResult {
  final String imageUrl;
  final String imageFileName;

  CloudStorageResult({this.imageUrl, this.imageFileName});
}

To access the Firestore Storage instance we use the FirebaseStorage.instance static property. The storage library works similar to the firebase documents. You can get a reference to a file that doesn't exist yet and then add the data in there that you want. We'll get a reference to our future file using the title and the date epoch to keep it unique. Once we have our reference we will call putFile and pass it in the selected File. This will give us a StorageUploadTask. This object has an onComplete Future that returns a StorageTaskSnapshot (similar to firebase snapshot). We can await that future and once we have the snapshot we can use the StorageReference returned and get the downloadUrl. We'll return the url when the task is complete or null.

  Future<CloudStorageResult> uploadImage({
    @required File imageToUpload,
    @required String title,
  }) async {

    var imageFileName = title + DateTime.now().millisecondsSinceEpoch.toString();

    final StorageReference firebaseStorageRef = FirebaseStorage.instance
    .ref()
    .child(imageFileName);

    StorageUploadTask uploadTask = firebaseStorageRef.putFile(imageToUpload);

    StorageTaskSnapshot storageSnapshot = await uploadTask.onComplete;

    var downloadUrl = await storageSnapshot.ref.getDownloadURL();

    if (uploadTask.isComplete) {
      var url = downloadUrl.toString();
      return CloudStorageResult(
        imageUrl: url,
        imageFileName: imageFileName,
        );
    }

    return null;
  }

Open up the locator.dart file and register the CloudStorageService with the get_it instance.

locator.registerLazySingleton(() => CloudStorageService());

Image Selection Implementation

We'll start off by wrapping the ImagePicker library into our own class. This way our business logic is not dependent on any third party packages. It's something I like to do, if you go the additional step and add it behind an interface then you can mock it out during testing as well.

Create a new folder called utils. Inside create a new file called image_selector.dart

import 'package:image_picker/image_picker.dart';

class ImageSelector {
  Future<File> selectImage() async {
    return await ImagePicker.pickImage(source: ImageSource.gallery);
  }

}

I know it seems silly to have a class that wraps one line, but you can do much more with it than this. You can keep the file in memory until you're certain it's uploaded, you can have different sources passed in from different functions, etc. The main reason for this is to remove the dependency of ImagePicker from any of the code in the app that has to make use of the functionality.

Open up the locator.dart file and register the ImageSelector with the get_it instance.

locator.registerLazySingleton(() => ImageSelector());

Finally open up the CreatePostViewModel where we'll locate the selector and then make use of it in a function called selectAndUploadImage. We'll also import the CloudStorageService for later use. We'll use the selectImage function to set the image to upload and display that to the user in the UI.

class CreatePostViewModel extends BaseModel {
  final ImageSelector _imageSelector = locator<ImageSelector>();
  final CloudStorageService _cloudStorageService = locator<CloudStorageService>();

  File _selectedImage;
  File get selectedImage => _selectedImage;

  Future selectImage() async {
    var tempImage = await _imageSelector.selectImage();
    if(tempImage != null) {
      _selectedImage = tempImage;
      notifyListeners();
    }
  }
}

In the same viewmodel update the addPost function to upload the image if we're not editting the post. We'll then use that url as the imageUrl in the post. For error handling I would show a snack bar if the imageUrl comes back null that indicates to the user that the image upload has failed.

 Future addPost({@required String title}) async {
    setBusy(true);

    CloudStorageResult storageResult;

    if (!_editting) {
      storageResult = await _cloudStorageService.uploadImage(
          imageToUpload: _selectedImage, title: title);
    }

    var result;

     if (!_editting) {
      result = await _firestoreService.addPost(Post(
        title: title,
        userId: currentUser.id,
        imageUrl:  storageResult.imageUrl,
        imageFileName: storageResult.imageFileName
      ));
    } else {
      result = await _firestoreService.updatePost(Post(
        title: title,
        userId: _edittingPost.userId,
        documentId: _edittingPost.documentId,
        imageUrl: _edittingPost.imageUrl,
        imageFileName: _edittingPost.imageFileName,
      ));
    }

    ...
  }

Next, open the Post model and add the new imageFileName String that we'll use to later delete the post.

class Post {
  ...
  final String imageFileName;

  Post({
    ...
    this.imageFileName,
  });

  Map<String, dynamic> toMap() {
    return {
      ...
      'imageFileName': imageFileName,
    };
  }

  static Post fromMap(Map<String, dynamic> map, String documentId) {
    if (map == null) return null;

    return Post(
      ...
      imageFileName: map['imageFileName'],
      documentId: documentId,
    );
  }
}

Now we can go onto the UI for the functionality. First thing to do is update the CreatePostView and add a gesture detector onto the grey rectangle we're displaying. When tapped we'll call the selectImage function. We'll also add a conditional to make sure when an image is selected we show it in that grey block. Update the container in the create_post_view that has the text in it to the following.

GestureDetector(
  // When we tap we call selectImage
  onTap: () => model.selectImage(),
  child: Container(
    height: 250,
    decoration: BoxDecoration(
        color: Colors.grey[200],
        borderRadius: BorderRadius.circular(10)),
    alignment: Alignment.center,
    // If the selected image is null we show "Tap to add post image"
    child: model.selectedImage == null
        ? Text(
            'Tap to add post image',
            style: TextStyle(color: Colors.grey[400]),
          )
          // If we have a selected image we want to show it
        : Image.file(model.selectedImage),
  ),
)

If you run the app now, tap on the FAB, enter a title and tap on the image block you'll see the image picker pop up. Select an image and it should be showing in the grey block in place of the text :) Add the post by pressing the FAB and it'll send it up to the cloud and return you a url.

If you open up the cloud storage now you'll see a file with the title you enetered and a number after it. That's the image you uploaded. Next up is displaying the image.

Image display implementation

To display the images from the cloud storage we will use the cached_network_image package. Add it to your pubspec.

cached_network_image: ^2.0.0

Open up the post_item and we'll update the UI. First thing is to make sure when we have an image we don't want to give the list a fixed size. We'll check if there's an image. If there's an image we set the heigh to null (meaning wrap content), otherwise we set it to 60.

class PostItem extends StatelessWidget {
  final Post post;
  ...
  @override
  Widget build(BuildContext context) {
    return Container(
      // Check if we have an image and set it to null or 60
      height: post.imageUrl != null ? null : 60,
      margin: const EdgeInsets.only(top: 20),
      alignment: Alignment.center,
      child: Row(
        children: <Widget>[
          Expanded(
              child: Padding(
            padding: const EdgeInsets.only(left: 15.0),
            child: Column(
              mainAxisSize: MainAxisSize.min,
              children: <Widget>[
                // If the image is not null load the imageURL
                post.imageUrl != null
                    ? SizedBox(
                        height: 250,
                        child: CachedNetworkImage(
                          imageUrl: post.imageUrl,
                          placeholder: (context, url) =>
                              CircularProgressIndicator(),
                          errorWidget: (context, url, error) =>
                              Icon(Icons.error),
                        ),
                      )
                // If the image is null show nothing
                    : Container(),
                Text(post.title),
              ],
            ),
          )),
         ...
        ],
      ),
      ...
    );
  }
}

That's it. You can now, post an image to the cloud storage. Then see it load as an item in the list of posts :)

Delete on Cloud Storage

Last thing to do is to delete the image again when a post is removed. This can be done by simple getting the ref and calling delete on it. Open up the CloudStorageService and we'll add a delete function.

class CloudStorageService {
   Future deleteImage(String imageFileName) async {
    final StorageReference firebaseStorageRef =
        FirebaseStorage.instance.ref().child(imageFileName);

    try {
      await firebaseStorageRef.delete();
      return true;
    } catch (e) {
      return e.toString();
    }
  }
}

Open up the HomeViewModel, locate the CloudStorageService and then after deleting the post from the firestore db call the delete function on the CloudStorageService as well.

class HomeViewModel extends BaseModel {
  final CloudStorageService _cloudStorageService = locator<CloudStorageService>();

  Future deletePost(int index) async {
    var dialogResponse = await _dialogService.showConfirmationDialog(
      title: 'Are you sure?',
      description: 'Do you really want to delete the post?',
      confirmationTitle: 'Yes',
      cancelTitle: 'No',
    );

    if (dialogResponse.confirmed) {
      var postToDelete = _posts[index];
      setBusy(true);
      await _firestoreService.deletePost(postToDelete.documentId);
      // Delete the image after the post is deleted
      await _cloudStorageService.deleteImage(postToDelete.imageFileName);
      setBusy(false);
    }
  }

}

And That's it. Basic Cloud storage functionality wrapped into a service for easy use. Make sure to follow me on Youtube for the rest of the series. Until next week :)