Caffeinated Simpleton

Meetup.com webOS Client Part 1: Services

Palm’s Mojo SDK has just been released to the public this past week. I thought I would take the opportunity to show off some of the awesome things the SDK can do. The Mojo SDK is exceptionally good at tying your life together with the web services you use every day, so my first series will be on building a simple Meetup.com client. At first, we’ll just sync down meetups using the api key that’s associated with every individual account. Another day, we’ll add OAuth authentication to make it generally useful for anybody.

This article assumes that you have some knowledge of how to create an app, new scenes, and how to debug. If you aren’t somewhat comfortable with the SDK yet, check out the Hello World tutorial.

Setup

At first, we’ll just create a big button that puts all our meetups into the calendar. Nothing fancy. To create the scene, run:
$ palm-generate -t new_scene -p “name=sync”

Then in the stage-assistant.js file, put:

this.controller.pushScene(“sync”);

Creating a Calendar

The first thing that we will do is create a new calender for Meetup.com. This calendar will appear in the calendar app right next to your Google Calendar or Exchange calendar using the Palm Synergy APIs. This is great because it allows you to deliver new data to your users without having to write yet another way of presenting it. All contacts and calendars can be plugged straight into the core webOS applications.

To create a calendar, you first need to make an account:

self.accountServiceId = “palm://com.palm.accounts/crud/“;

/* Retrieves account if it exists, otherwise creates it */ setupAccount: function(self, k) { self.controller.serviceRequest(self.accountServiceId, { method: ‘listAccounts’, parameters: {}, onSuccess: function(list) { Mojo.Log.info(“Got account list: %j”, list); if (list.list && list.list.length > 0) { self.account = list.list[0]; k(); } else { self.account = { username: “justin”, domain: “meetup.com”, displayName: “Meetup.com”, dataTypes: [“CALENDAR”], isDataReadOnly: true, icons: {largeIcon: “, smallIcon: “} }; self.controller.serviceRequest(self.accountServiceId, { method: ‘createAccount’, parameters: self.account, onSuccess: function(response) { Mojo.Log.info(“Got %j for %j”, response, self.account); self.account.accountId = response.accountId; k(); } }); } }, onFailure: function() { Mojo.Controller.errorDialog(“Failed to create account”); }, onError: function(error) { Mojo.Controller.errorDialog(“Error creating account”); } }) },

You’ll notice a few different things about this code, the odd things which are just my style. Using self instead of this is an idiom from Cobra, a JavaScript class library I wrote. k is a continuation, or the function that should be called after the account is created. k I believe is an idiom for Continuation Passing Style in Scheme.

Beyond that, this is very standard Mojo code. Services are identified as “palm://com.palm.” and all methods support onSuccess, onFailure, and onError callbacks. This code checks to see if there is already an account associated with this appId, and if there is, calls its continuation. If there is not, it creates the account and calls its continuation.

After the account is created, we can create the calendar.

self.calendarServiceId = “palm://com.palm.calendar/crud/“;

/* Retrieves calendar if it exists, otherwise creates it */ setupCalendar: function(self, k) { self.controller.serviceRequest(self.calendarServiceId, { method: ‘listCalendars’, parameters: { accountId: self.account.accountId }, onSuccess: function(calList) { Mojo.Log.info(“Got calendar list”); if (calList.calendars.length > 0) { self.calendar = calList.calendars[0]; k(); } else { self.calendar = { name: “Meetup.com” } self.controller.serviceRequest(self.calendarServiceId, { method: ‘createCalendar’, parameters: { accountId: self.account.accountId, calendar: self.calendar }, onSuccess: function(response) { self.calendar.calendarId = response.calendarId k(); }, onFailure: function(error) { Mojo.Log.error(“Creating calendar failed: %j”, error); Mojo.Controller.errorDialog(“Failed to create calendar”); }, onError: function(error) { Mojo.Log.error(“Creating calendar failed: %j”, error); Mojo.Controller.errorDialog(“Error creating calendar”); } }); } }, onFailure: function() { Mojo.Controller.errorDialog(“Failed to create calendar”); }, onError: function(error) { Mojo.Controller.errorDialog(“Error creating calendar”); } }); },

This is almost identical to the account creation code, except it’s creating a calendar.

Now calling these functions is easy:

self.setupAccount(function() {
    self.setupCalendar(function() {
        self.buttonModel.disabled = false;
        self.controller.modelChanged(self.buttonModel)
    });
});

Pulling down events

To get the events that are associated with the account, I am using the Meetup.com JavaScript client. The client requires jQuery, but since it only requires jQuery to do JSONP, we replace that line with a Mojo call. There is no reason that we couldn’t use jQuery, but jQuery is overkill for just doing JSONP.
jQuery.getJSON(urlprefix + call_type + url, params, function(json){callback(json)})
Becomes:
var query = $H(params).toQueryString();
url = urlprefix + call_type + url + query;
Mojo.loadScriptWithCallback(url, Mojo.doNothing);

After we have the Meetup client library, getting the events is easy, but a little indirect. It takes three API calls, one to get the member id, one to get every group associated with the member id, and one to get every event in all those groups. You can see the details on Meetup’s API page.

self.client = new MeetupApiClient(Meet.Auth.apiKey);
syncCalendar: function(self) {
    // Gets my member id
    Mojo.Log.info(“Syncing calendar”);
    self.client.get_members({
        relation: “self”
    }, self._getGroups);
},

_getGroups: function(self, members) { Mojo.Log.info(“Got members”); var memberId = members.results[0].id; self.client.get_groups({ member_id: memberId }, self._getEvents); },

_getEvents: function(self, groups) { Mojo.Log.info(“Got events”); groups = groups.results; var groupString = groups[0].id; var today = new Date(); for (var i = 1; i < groups.length; i++) { groupString += “,” + groups[i].id; } self.client.get_events({ group_id: groupString, after: today.getMonth() + today.getDay() + today.getFullYear() }, self._saveEvents); },

Now self._saveEvents will receive a list of events that are coming up after today. All we need to do is loop over the list, format them as Palm calendar events, and pass them to the calendar service.

_saveEvents: function(self, events) {
    Mojo.Log.info("Saving events");

    self.numEventsProcessed = 0;
    self.events = events;

    events.results.each(function(meetupEvent) {
        var time = new Date(meetupEvent.time).getTime();
        if (meetupEvent.myrsvp != "no") {
            self.controller.serviceRequest(self.calendarServiceId, {
                method: 'createEvent',
                parameters: {
                    calendarId: self.calendar.calendarId,
                    event: {
                        eventId: meetupEvent.id,
                        subject: meetupEvent.name,
                        startTimestamp: time,
                        endTimestamp: time + 3600000, // 1 hour in ms
                        allDay: false,
                        note: self._formatNote(meetupEvent),
                        location: meetupEvent.lat + ", " + meetupEvent.lon,
                        alarm: 'none',
                    }
                },
                onSuccess: self._createdEvent,
                onError: self._errorCreatingEvent,
                onFailure: self._failureCreatingEvent
            });
        }
    });
},

_createdEvent: function(self, response) {
    self._checkIfSyncFinished();
},

_errorCreatingEvent: function(self, response) {
    Mojo.Log.error("Could not create event: %j", response);
    self._checkIfSyncFinished();
},

_failureCreatingEvent: function(self, response) {
    Mojo.Log.error("Failed to create event: %d, %j", self._eventsReturned, response);
    self._checkIfSyncFinished();
},

_checkIfSyncFinished: function(self) {
    self.numEventsProcessed++;
    if (self.numEventsProcessed == self.events.meta.count) {

        if (self.events.meta.next) {
            Mojo.Log.info("Fetching the next page of results...");
            self.client.nextPage(self._saveEvents);
        }
        else {
            Mojo.Log.info("Fetched all the events");
            self.buttonModel.disabled = false;
            self.controller.modelChanged(self.buttonModel);
            self.controller.get("sync-button").mojo.deactivate();
        }
    }
},

And that’s basically it! With the code above, all your Meetup.com events can be inserted into your webOS calendar. All you need is an event handler for your button.

self.buttonModel = {
    buttonLabel: 'Sync',
    disabled: true
};

self.controller.setupWidget('sync-button', {
    type: Mojo.Widget.activityButton
}, self.buttonModel);

Mojo.Event.listen($('sync-button'), Mojo.Event.tap, function() {
    self.buttonModel.disabled = true;
    self.controller.modelChanged(self.buttonModel)
    self.controller.get("sync-button").mojo.activate();
    self.syncCalendar();
});

This works assuming a view containing:

Debugging Tips

Creating a file called framework_config.json allows you to change the logging level. That will permit JavaScript messages to be output into /var/log/messages on the device. This is especially valuable if you’re working on a 64-bit linux machine where the inspector is currently not supported.
{
    “logLevel”: 99 // 0 means no logging, 99 will max it out
}

Removing /var/luna/data/dbdata/PalmDatabase.db3 removes all data you may have inserted. This allows you to start over fresh, but I wouldn’t recommend doing this on your actual device. You’ll lose all your data!

Todo

This program isn’t really complete, but it’s a good start. A couple of things we still have to do are: Stay tuned for articles on some or all of these exciting new features! Until then, the code is available on github.

comments powered by Disqus