How to add support for new services to Friends?

With the new friends-app landing in Ubuntu, it occurred to me that adding support for Instagram in the timeline would be cool. I also thought I might take a stab at it myself, but I'm having a hard time finding any documentation.

Is there a spec describing what is needed from each protocol? How does auth work? Would support need to be added to ubuntu-online-accounts first or is there a way for friends to register the new protocol?

It's very new and has a very hard to Google name, so any pointers in the right direction would be appreciated.


Solution 1:

Friends author here.

Indeed, as you suspected, support is required in Ubuntu Online Accounts before support can be added to Friends. Friends architecture depends very heavily upon UOA in order to do all the authorization and manage all the API keys for us. My favorite example is LinkedIn, because it is so far the only protocol that was community contributed. The UOA plugin is mostly just two XML files, plus a little bit of autoconf trickery, which looks like this. (scroll down a bit for the diff, which clearly shows every single thing that needed to be added for LinkedIn to work).

Once you've done the same for your protocol, you need to propose the merge against lp:account-plugins and get Mardy to review, approve, and merge them. Once that is in place, you can begin to write the friends plugin, which will be written in Python 3.

Now, one of the major, major improvements that Friends introduces over Gwibber is the use of subclasses. In the original Gwibber code, absolutely nothing was done with subclasses, so every new protocol plugin was a huge copy&paste hackjob of various bits of low-level functionality. When implementing Friends, I took great care to extract the common functionality into a superclass, which can be easily subclassed and modified. The superclass also has quite a lot of docstrings, which you should refer to when getting started. Unfortunately we haven't set up sphinx to publish those anywhere yet, so you'll just have to read the code for now.

Some important things to keep in mind is that the name of your class has to match the "providername" used, case-insensitively. So if you defined the providername to be instagram, then you should create the file protocols/instagram.py, and name your Python class Instagram.

The two most important methods that you absolutely have to implement in order for your plugin to actually do anything, are called _whoami and receive. These are well documented in base.py (linked above), but basically the _whoami method will be called automatically, and passed in a dict that represents an already-parsed JSON blob that was given to us by the service when the authentication happened. If you're lucky, that dict will contain your Instagram username, user id, and display name, but if not you'll need to make a secondary API call in order to gather that information. Please see Facebook._whoami for an example of a protocol that didn't provide the information up front and required an additional API call from within the method, and see Twitter._whoami for an example of a protocol that gave us all the details we needed right up front.

Afterwards, the receive method is responsible for making the API call that polls the service for new messages. This one is a little bit more free-form, because every REST API is slightly different, so you should refer to the website's API docs in order to figure out what exactly needs to be done here. In http.py we provide Uploader and Downloader classes that make it easy to make REST API calls, and can even parse JSON server responses for you. It's important to use these convenience classes because they wrap libsoup, which is configured to honour the GNOME proxy settings (you may recall how terrible Gwibber's proxy support always was, well we've fixed all that now).

Once you've got the API response from the server, you need to store it in our DeeModel (where Gwibber used a JSON blob dumped into an sqlite db for storing your messages, we're using a DeeModel, which is basically just a database that shares state across DBus, making it easy for multiple clients to display the message data easily). We call the act of storing a new message "publishing", and we provide a convenience method for it at Base._publish. Basically all you have to do is fill in the blanks here, make sure as much information as possible is filled into as many columns as possible. The possible arguments to _publish are defined in the schema, and again you can refer to the existing plugins to see how they do it.

Once you've gotten this far, you should have enough to be able to test it. We provide a couple tools in the tools directory to make it easy to run your code from within the source tree, so you don't have to install it to the system every time you want to make a change. What you should do is open one terminal, cd to the root of the source tree, and run ./tools/debug_slave.py. What that does is connects to the DeeModel, and just displays everything that happens to it, so you can see messages appearing live as they come in. Then, in a second terminal, cd to the root of the source tree again, and run ./tools/debug_live.py instagram receive and that will manually trigger the Instagram.receive method and display a bunch of debugging output to tell you about what's going on as it runs (you can sprinkle your code with calls to log.debug("hi") if you want to see even more details about what happens).

Oh, and if you're still reading, the linkedin plugin hasn't landed in trunk yet, but you can still take a look at it here.

If you have any other questions, I am always in #gwibber on freenode, and also I feel quite strongly that the new codebase is much more readable and better documented than anything Gwibber ever had, so please read the code that's there and it shouldn't be too difficult to learn by example. Facebook and Twitter are the most complete.

Thanks for your interest in Friends!