Ramblings of a Tampa engineer


Google Hangouts has become the source of 90% of my online real time communication. I have a variety of groups ranging from work, hobby, gaming and school related hangouts. For a few of these groups, I noticed the addition of a "bot" could increase our efficiency with basic tasks.

This is where I met the open source project: hangoutsbot which utilizes hangups. This amazing project has a large collection of plugins with the ability to write your own.

For one hangout, it had become common to "roll-call" and count off for attending events. Most conversations would go something like this.

Me - Okay, who is coming tonight? I'll start 1
Another - 2

You get the point. Quickly though, this conversation is lost and those checking in on the chat may completely missed the event, especially if a few hundred messages had passed.

We needed a system that handled RSVPs, but within a hangout. Forcing a user to go to a website and sign up, defeated the purpose and ease of use that doing a roll call in a hangout provided.

So first I needed to update our website to support a calendar. We found fullcalendar.io, which had amazing support in terms of functionality. We basically load the calendar, and provide it a URL for events. The calendar than sends out an AJAX request for events of that month, which Laravel handles from there.

public function getEvents(Request $request)
{
    $events = GameEvent::whereBetween('start', [$request->get('start'), $request->get('end')])->get();

    return $events->toJson();
}

We grab all events between the start/end date, so the calendar only populates events in that time frame. Our column names match what fullcalendar.io is requiring. A simple date, title, and url column. We actually leverage Laravel's mutators to create a url column, as we don't have a column for that.

So Laravel dumps this back to front-end.

[{"id":9,"title":"Kings Fall","type":"Raid","start":"2015-09-29 04:00:00","created_at":"2015-09-27 20:05:11","updated_at":"2015-09-27 20:05:11","max_players":6,"alert_5":0,"alert_15":0,"url":"http:\/\/pandalove.club\/calendar\/event\/9","backgroundColor":"#5BBD72"}]

Which full-calendar.io loads easily

$('#calendar').fullCalendar({
    firstDay: 1,
    events: "{{ URL::action('CalendarController@getEvents') }}"
});

Then we are left with this dynamic calendar.

So now we have a working calendar, where events are populated from our own data source. Now we needed to extend this to allow people to "signup" for those events.

This is where some creativity needed to happen. We need a solid bit of information for this sign up. If a user signs up, we need to know

  1. Who they are.
  2. What character they are.
  3. They are authenticated and allowed to sign up for these events.
Step 1 - Google+ Authentication

Once again, Laravel saves the day - taking the complication out of this process. Simply using the Socialite package allowed us to make our entire login/register function as easy as this.

private function redirectToProvider()
{
    return \Socialite::with('google')->redirect();
}

private function handleProviderCallback()
{
    $user = \Socialite::with('google')->user();
    \Event::fire(new GoogleLoggedIn($user));
}

So a brand new user clicking sign in, is redirected to a familiar page.

This prompt grants us permission to view name, email, avatar, and google_id. With these fields we can create a user, uniquely identified by google_id and email.

Step 2 - Linking a Google+ Account with a Destiny Account

So now our group has authenticated with our site, but our site has zero knowledge of who owns which Destiny account. Yes, this would be relatively easy to hard code, but I did not want to take that route. Asking for the user's password to authenticate on their behalf was out of the question.

A common practice I learned for verification was to provide the client with a group of random numbers and ask them to enter them in a profile location on their Destiny account. Therefore, I can leverage the API to view the profile of the account they are verifying and check if the random digits I requested are there. You get the picture, if digits are found I can safely say they own that account.

So now we have a Google account linked with a Destiny account. So the binding for $user->destiny is working perfectly. The Destiny API doesn't require any per account API keys, so just knowing which account belongs to

Step 3 - Figuring out the characters a Destiny account has

So if a user is RSVPing to an event. We need to know which character they want to be. At this point through validation, we know if they are applying to an event that they have a Google account & linked Destiny account.

This blog is more of the overall picture, so I will skip the details on building off the API to obtain characters. After this work, I'm left with a relation on my User model of their Destiny characters.

public function characters()
{
    return $this->hasMany('Onyx\Destiny\Objects\Character');
}

So now easy as pie. I can run $user->characters and have a collection of their characters (min: 1, max: 3). Though we have hit another "worry" point. Currently anyone with a Google account and Destiny account can signup and RSVP to our events.

Step 4 - Isolating sign ups to our group only

So this turned out easier than I thought. During our API pull of their profile (Where we validated the random characters) we can see what "clans/groups" that user is a part of. So we simply look for our clan_id and throw a boolean flag so our User model knows it is a fellow clan member.

Our proof of concept (without hangouts) was working great. Users could sign up with a Google account, validate ownership of their Destiny account and load their characters in order to sign up for an event.

However, we hadn't even touched our Hangouts chat yet.

Step 5 - Bringing this functionality to chat

So now we have a bot sitting in our hangouts. First I needed to get comfortable in how the bot could obtain information via commands. So I wrote a quick /bot xur command to test the waters.

def xur(bot, event, *args):
    """get xur data"""
    r = urllib.request.urlopen("http://pandalove.club/api/v1/xur/")
    parsed = json.loads(r.read().decode('utf-8'))

    bot.send_html_to_conversation(event.conv_id, parsed['message'])

This simply opens up a GET request to our website and returns json. So after some tweaking of the returned information. We end up with this.

Which was pretty fun to make. Any time this video game character arrives on Friday 4am CST, I can issue /bot xur when I wake up and be presented with the items.

Step 6 - Applying this functionality to the real problem

So now we need /bot rsvp to function, but the problem at hand is we need to know

  1. What Event they are RSVPing to
  2. Who this person is
  3. What character they want

Figuring out problem #1 was rather easy. Just require the ID of the event to be a parameter. So /bot rsvp 5 would RSVP you to the event with id of 5. However, we are just sending text to the server, so we have no idea "who" is sending this text. Once again, hangups bot to the rescue.

I noticed the event parameter that all functions are required to have. Investigating this variable led me to event.user_id.chat_id. Which is the Google ID of the person who sent the message. If you remember from Step 1, we have this information, so the bot can use this to validate people.

The problem was now how to validate characters. There was no easy way for the command to say "let me play on ___ character". So I got a bit creative.

Running /bot rsvp 5 without the 4th parameter, would return another message explaining what your 4th parameter should be. You can see from the example below, how this played out.

This was absolutely amazing. Our system was working perfectly, so we added some functionality.

  1. /bot events - to list upcoming events
  2. /bot event id - to list that event id information
  3. /bot update - Updates your profile and destiny information
  4. /bot light - Provides light information (Destiny term) on your character.
  5. /bot grimoire - Provides grimoire information (Destiny term) on your character.

After a few weeks, there was a common feature request. "Can we get alerted before an event starts? Like a reminder."

Step 7 - Pushing messages to the bot.

After a few days of discussion. We agreed to add a 15 and 5 minute reminder prior to an event. The problem was our bot knew nothing about these events. This functionality was server side on our Laravel install.

Laravel with a simple schedule command running every 5 minutes could detect when an event was 15/5 minutes away from starting, but we needed a way to push a message to the hangout.

Off to the docs of hangupsbot, and I found the API Plugin. This clever plugin added a listener to the bot, then you could POST requests to that URL and the bot would react on that command.

Guzzle made that too easy.

$url = env('BOT_HOST') . ":" . env('BOT_PORT');
$response = $this->guzzle->request('POST', $url, [
    'json' => [
        'key' => env('BOT_APIKEY'),
        'sendto' => $sendto,
        'content' => $content
    ]
]);

This was one of our more interesting improvements. The bot would now message us privately on Google Hangouts triggering a notification to remind us of the event we signed up for.

Step 8 - End: Identifying those in an Event

Now we had an opportunity to extend again. A new feature request was "I want to know who is online before I sign on". So off to Google and I find xboxapi.com. An unofficial service for making Xbox API calls.

They had an interesting endpoint

  • /v2/{xuid}/presence - This is the current presence information for a specified XUID

So an additional library was created using this endpoint. All we needed was the XUID of the gamertag and the API would return the "online status" of that XUID.

$url = sprintf(XboxConstants::$getPresenceUrl, $account->xuid);
$requests[$account->seo] = $client->getAsync($url, [
    'headers' => ['X-AUTH' => env('XBOXAPI_KEY')]
]);

We would iterate through our accounts and get their "online status" on Xbox. The image below shows this in action.


Our website, bot and more grew rapidly throughout this process. This turned out to be a constantly evolving fun project, using multiple technologies together.

You’ve successfully subscribed to Connor Tumbleson
Welcome back! You’ve successfully signed in.
Great! You’ve successfully signed up.
Success! Your email is updated.
Your link has expired
Success! Check your email for magic link to sign-in.