The Deliverator Slack Bot: Building on top of Urban Airship Connect

Hankering for a Connect Bot

I used to be a pretty decent engineer, but I don’t write code anymore and haven’t in years. My skills were never as good as the vast majority of Urban Airship’s engineers (they amaze me every day!), but I was pretty alright for the time.

A couple weekends ago, I got a hankering to write some code as I’ve been curious what creating a Slack bot for Urban Airship Connect might look like. Connect delivers real time individual user-level data to a destination of your choice. Slack is a popular group messaging platform for teams that we use at Urban Airship. Combining Slack with Connect and our Engage API could help surface real-time insights and execute actions in Urban Airship Engage without ever leaving Slack.

Good news for me: Slack is encouraging the development of bots & apps on top of the Slack platform. We have numerous bots that we use at Urban Airship to help us run the company (e.g., get notified of alerts, new customers, support cases, execute deploys, praise Airshipper good work and add to their karma score). I named my new  bot “Deliverator” based on some discussion with friends.

I went into this assuming I’d spend a bunch of time getting set up and then run out of time or get frustrated, but I was delighted that it was incredibly easy to get started.

This post summarizes the technical setup, code examples, a demonstration, feature ideas and next steps for the Deliverator bot. I’ll be really interested in your feedback, which we’ll gather in the form of a quick survey (embedded below). We’ll select one random winner from all survey respondents to receive an Urban Airship longboard!

Technical Setup

Java

I wrote a lot of Java years ago so I figured I’d start there. I also used to use IntelliJ and it is even better than it was years ago so I set it up really quickly. Urban Airship Connect and Slack both have great libraries available. I used the Urban Airship Connect Java library to use Connect over the Connect HTTPS Streaming API, the Urban Airship Engage Java Library to use the HTTPS Push API and the Simple Slack API library.

IntelliJ Setup

Configuration

Configuring a Bot account in Slack and setting up an Urban Airship Connect token was easy.


Code Examples

Some of this code may look circa Java 1.4 style (where I left off). I’m sharing it anyway to show how easy it is to work with Connect & Slack, even for me! It’s definitely not intended to win any style points or go anywhere near production.

Bot Starting Connect & Handling Connect Events

 

 public void startUp() throws Exception {
   log.info("starting up Deliverator connection to Connect");


   ImmutableMap im = ImmutableMap.of("connect.client.mes.stream.maxAppStreamConsumeMillis", TimeUnit.SECONDS.toMillis(600L));
   Configuration config = new MapConfiguration(im);

   AsyncHttpClient httpClient = StreamUtils.buildHttpClient(new ConnectClientConfiguration(config));

   final String appKey = "your-app-key-here";
   final String token = "your-token-here";
   Creds creds = Creds.newBuilder().setAppKey(appKey).setToken(token).build();

   StreamQueryDescriptor descriptor = StreamQueryDescriptor.newBuilder()
           .setCreds(creds)
           .build();

   FatalExceptionHandler fatalExceptionHandler = new FatalExceptionHandler() {
       public void handle(Exception e) { log.fatal(e); }
   };

   Consumer consumer = new Consumer() {

       // Process Events
       public void accept(Event event) {
           log.info("Received event:" + event.getIdentifier());

           collectCounts(event);
           watchList(event);
           countPushesSent(event);
       }
   };

   final MobileEventConsumerService service = MobileEventConsumerService.newBuilder()
           .setBaseStreamQueryDescriptor(descriptor)
           .setConfig(config)
           .setClient(httpClient)
           .setOffsetManager(new InFileOffsetManager(creds.getAppKey()))
           .setFatalExceptionHandler(fatalExceptionHandler)
           .setConsumer(consumer)
           .build();

   ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1);
   Runnable stopConsuming = new Runnable() {
       public void run() {
           service.triggerShutdown();
           log.info("shutdown");
       }
   };

   scheduledExecutorService.schedule(stopConsuming, 60*60, TimeUnit.SECONDS);
   service.run();

   log.info("Connect now running ...");
}
 

Bot Starting Slack & Handling Slack Bot Commands

 public void startUp() throws Exception {

   log.info("starting up Deliverator connection to Slack");
   slackSession = SlackSessionFactory.
           createWebSocketSlackSession("your-slack-api-token");
   slackSession.connect();
   SlackMessageHandle joinStat = slackSession.joinChannel(channelName);
   deliveratorChan = slackSession.findChannelByName(channelName);

   SlackMessageHandle slackMessageHandle = slackSession.sendMessage(deliveratorChan, "¡¡¡ Deliverator Reporting for duty !!!");
   slackMessageHandle.getMessageId();

   log.info("Slack is all set up ...");

   slackSession.addMessagePostedListener(new SlackMessagePostedListener()
   {
       public void onEvent(SlackMessagePosted event, SlackSession session)
       {

           String slackMessage = event.getMessageContent();

           if (botCommandManager.isCommand(slackMessage))
           {
               log.info("Deliverator command received:" + slackMessage);

               CommandResult result = botCommandManager.execute(slackMessage);

               log.info("CommandResult:" + result);

               if (result != null) {
                   session.sendMessage(deliveratorChan, result.getBotReturnText());
               } else {
                   session.sendMessage(deliveratorChan, "Unsupported Deliverator Command");
               }


           } else {
               log.info("NA / not a deliverator command");
           }

       }
   });

}
 

Stats Command

The StatsCommand pulls off various counts from the ConnectManager. The ConnectManager increments counts for each event it processes.

 public class StatsCommand implements Command {

   public CommandResult execute() {
       CommandResult result = new CommandResult();
       result.setBotReturnText(getStatsString());

       return result;
   }


   private String getStatsString() {
       ConnectManager connectManager = ConnectManager.getInstance();

       return "Deliverator Stats{" +
               "total='" + connectManager.getEventCountTotal() + '\'' +
               ", android=" + connectManager.getEventCountTotal() +
               ", ios=" + connectManager.getIosTotal() +
               ", namedUser=" + connectManager.getNamedUserTotal() +
               ", openCount=" + connectManager.getOpenCount() +
               ", closeCount=" + connectManager.getCloseCount() +
               ", customCount=" + connectManager.getCustomCount() +
               ", locationCount=" + connectManager.getLocationCount() +
               ", sendCount=" + connectManager.getSendCount() +
               ", tagChangeCount=" + connectManager.getTagChangeCount() +
               ", firstOpenCount=" + connectManager.getFirstOpenCount() +
               ", pushBodyCount=" + connectManager.getPushBodyCount() +
               ", uninstallCount=" + connectManager.getUninstallCount() +
               ", richDeliveryCount=" + connectManager.getRichDeliveryCount() +
               ", richReadCount=" + connectManager.getRichReadCount() +
               ", richDeleteCount=" + connectManager.getRichDeleteCount() +
               ", iamExpiryCount=" + connectManager.getIamExpiryCount() +
               ", iamResolutionCount=" + connectManager.getIamResolutionCount() +
               ", iamDisplayCount=" + connectManager.getIamDisplayCount() +
               '}';

   }
}
 

Send Push Notification

For testing, I just sent a blank push notification to all iOS users from Urban Airship’s beloved internal test app, Goat. After the notification is sent, the push ID is set on ConnectManager so that the user-level send events can be tracked and I could see progress as the push is sent.

 public APIPushResponse sendPush() {

   APIPushResponse pushResponse = null;

   log.info("Fixing to send a push");

   APIClient apiClient = APIClient.newBuilder()
           .setKey(appKey)
           .setSecret(appSecret)
           .build();

   PushPayload payload = PushPayload.newBuilder()
           .setAudience(Selectors.all())
           // send a blank push 
           .setNotification(Notifications.alert(""))
           .setDeviceTypes(DeviceTypeData.of(DeviceType.IOS))
           .build();

   try {
       APIClientResponse response = apiClient.push(payload);

       pushResponse = response.getApiResponse();

       String pushId = response.getApiResponse().getPushIds().get().get(0);
       log.info("Sent push:" + pushId);

       ConnectManager.getInstance().setWatchPushId(pushId);

       log.info(String.format("Response %s", response.toString()));
       log.info("response object"+ response);

   } catch (APIRequestException ex) {
       log.error(String.format("APIRequestException " + ex));
       log.error("Something wrong with the request " + ex.toString());
   } catch (IOException e) {
       log.error("IOException in API request " + e.getMessage());
   }
   return pushResponse;

}

Demo

Here’s what Deliverator  looks like in action inside Slack.

Stats Command

First, let’s start up the bot and see what the current stats are. For now, stats are just counts of a couple things.


Push Command

Second, let’s send a push, monitor the send count as it goes out and then check the stats again.


 

Watch

Last, I’ll watch my Named User for any events on any of my devices that have the Goat app installed. This could be really useful for testing or troubleshooting a user issue. To demonstrate it in action, I used our message composer to create a push notification with a social sharing button. It also sets a tag named “deliverator” if you tap the button. I included a screenshot of the composer interface and the on-device experience below.

Set Watch

Tell Deliverator to start watching my events.

Watch Results

You can see the SEND event for when I sent the message and then you also see the OPEN event where I received the push and opened the app from it. You also see the CUSTOM event that fired when I tapped the share button, a LOCATION event and the TAG_CHANGE event where the “deliverator” tag is set. Pretty cool!

Deliverator Bot Feature Ideas

Creating these basic commands sparked my imagination for other features we could create for a Slack bot:

  • Messaging: Build in a bunch of options to the push command, supporting In-App Messaging and Message Center

  • Wallet: Send a welcome offer wallet item via push notification to new users of your app.

  • Tail: Log last N events (most recent, by type, by channel, by named user) and a -f option to unleash the entire Urban Airship Connect stream for the app on the Slack channel. (This is a joke for my friends at Slack; I promise we won’t do this.)

  • List: Get a link to the latest Lifecycle Lists. Log a list to the channel.

  • Alias: Associate a channel or a named user with a shorthand for use in bot commands.

  • Top: Show top named users/channels by event type, by total events or most active users.

  • Alert: Set certain events to post to the channel immediately when they occur (e.g., first app open, app delete, inactivity, push bodies).

  • Whois: Based on a channel or named user, log the user profile to the channel and include a link to the profile page in the Urban Airship UI.

  • Errors: Log any errors Urban Airship encounters to the channel.

  • Search: Index Connect events and make them searchable from Slack.

  • App Usage Hourly/Daily Summary: Update stats on your app every hour in channel (new users, opens, app deletes, custom events, profile changes, most active users, conversion rates, etc.). This could also include graphs!

  • Watch User Behavior: Watch a user that has reported a bug in hopes of finding the problem. Watch a random active user in the app for a day so everyone in the channel can learn about user behavior and hopefully improve the app.

  • Predict: Generate alerts on users who are showing signs of churn.

  • Anomalies: Draw attention to outlier behavior in the app (could be your best users, fraud, whatever).

  • Message Send Approval: Anyone can send a message, but it must get +1 from an approver bot. Slack messages the configured approver for +1; if approved, the notification is sent.

  • Links: Make everything linked (channels, named users, reports, etc.) so that Slack users can easily click through into the Urban Airship UI to see more details.

  • Location Heat Map: A heat map of location events from past hour.

  • Secret: Mind-bendingly complex event processing, machine learning, deep learning and AI features!

Seems like I’m just scratching the surface, but I suspect that this would be useful for mobile teams everywhere that use Slack & Urban Airship.

Next Steps

I am really curious if joint Urban Airship and Slack customers would like to use a bot like this. I mentioned it to a developer from one of our customers at Mobile World Congress in Barcelona and he was excited about it, which inspired me to write this post.

What features would be most useful? Any other bot feature ideas? Any other “must have” platforms we should plug Deliverator into? Where else could you use a bot-like interface into Urban Airship?

We’ve prepared a tiny survey (below) to get your feedback on the overall potential of Deliverator Bot, and we’ll be giving an Urban Airship longboard randomly to one of the respondents, so check it out!

Also, doesn’t building on top of Urban Airship Connect look easy & powerful? Just think of all the real-time event-driven systems you can create!
Let us know if you want to give building on top of it a try!

 

 

 

Resources:
http://docs.urbanairship.com/dev-resources.html

http://docs.urbanairship.com/connect/index.html