Your cart is currently empty!
Witter – a basic python twitter client for Maemo
So I wrote last week about developing a basic twitter client. And this week I got the main stuff done, and wanted to share the code example here.
In my looking for help developing apps for Maemo from a start of basically no GTK knowledge or python knowledge I found the examples either too trivial, or way over engineered. So I wrote this intending it to be useful (to me), contain only enough capability to basically read my timeline and tweet. I’ve intentionally not added bells and whistles (yet) and it’s a single ‘monolithic’ app. By which I mean it’s all in a single python file, there is no separation of gui and logic, no nice engineered constructs etc etc.
I hope that it does show an intermediate level example of writing an application for Maemo using Python. This is what it looks like in action
This was taken full screen. The app supports switching in and our of full screen. And it adjusts the width of the displayed text to fit. As you can see the shot was taken not long after completing the application.
It also sorts the tweets using the ListStore ability to just tell it which column to sort on. This is very useful as it means I don’t have to mess around myself. Originally I had it sorting on created_at, but since I was loading that as a String it would order Thursday below Wednesday. Rather than cast the string into a meaningful date object of some form, I just used ID instead, which is a Long. Still comparing as a string, but the number always increments so newer tweets always appear t the top. Obviously if you prefer newer tweets at the bottom, just flip the sort order to Ascending.
So here is the code, it’s a little under 300 lines, but I’ve commented it pretty well (I think) to explain what it’s all doing.
# ============================================================================
# Name : witter.py
# Author : Daniel Would
# Version : 0.1
# Description : Witter
# ============================================================================
#This is the bunch of things I wound up importing
#I think I need them all..
import gtk
import pygtk
import hildon
import urllib2
import urllib
import base64
import urlparse
import simplejson
import socket
#Initially I found I’d hang the whole interface if I was having network probs
#because by default there is an unlimited wait on connect so I set
#the timeout to 10 seconds afterwhich you get back a timeout error
# timeout in seconds
timeout = 10
socket.setdefaulttimeout(timeout)
#the main witter application
class Witter(hildon.Program):
#first an init method to set everything up
def __init__(self):
hildon.Program.__init__(self)
#being lazy this just uses basic auth and I am not doing anything
#yet to store uid/pwd so for the moment just put info here
self.username = “YOUR_USERNAME”
self.password = “YOUR_PASSWORD”
#This being a hildon app we start with a hildon.Window
self.window = hildon.Window()
#connect the delete event for closing the window
self.window.connect(“delete_event”, self.quit)
#add window to self
self.add_window(self.window)
#For this app I wanted a scrollable area for the tweets to show up
#so I create a gtk ScrolledWindow
self.scrolled_window = gtk.ScrolledWindow(hadjustment=None, vadjustment=None)
# as well as somewhere to show the tweets we need somewhere to write a tweet
# this being twitter we cap the input at 140 chars
self.tweetInput = gtk.Entry(max=140)
# we also want a couple of control buttons to load up tweets and submit a tweet
self.buttonloadTweets = gtk.Button(label=”Load Tweets”,stock=None, use_underline=None );
# we connect out load tweets button to the getTweets method
self.buttonloadTweets.connect(“clicked”, self.getTweets)
self.buttonnewTweet = gtk.Button(label=”Tweet”,stock=None, use_underline=None );
#we connect the Tweet button to the newTweet method
self.buttonnewTweet.connect(“clicked”, self.newTweet, self.tweetInput)
# a vertical box to set the scrollable window and the button box
# in the display
self.box1 = gtk.VBox(False, 0)
#a horizontal box to put our tweet input box and two control buttons in
self.buttonBox = gtk.HBox()
# add the Vbox to the window
self.window.add(self.box1)
# create a menu object by calling a method to deine it
menu = self.create_menu(self.scrolled_window)
# add the menu to the window
self.window.set_menu(menu)
# define a liststore we use this to store our tweets and some associated data
# the fields are : Name,nameColour,Tweet+timestamp,TweetColour,Id
self.liststore = gtk.ListStore(str, str, str, str, str)
# create the TreeView using treestore this is the object which displays the
# info stored in the liststore
self.treeview = gtk.TreeView(self.liststore)
# create the TreeViewColumn to display the data, I decided on two colums
# one for name and the other for the tweet
self.tvcname = gtk.TreeViewColumn(‘Name’)
self.tvctweet = gtk.TreeViewColumn(‘Tweet’)
# add the two tree view columns to the treeview
self.treeview.append_column(self.tvcname)
self.treeview.append_column(self.tvctweet)
# we need a CellRendererText to render the data
self.cell = gtk.CellRendererText()
# add the cell renderer to the columns
self.tvcname.pack_start(self.cell, True)
self.tvctweet.pack_start(self.cell,True)
# set the cell “text” attribute to column 0 – retrieve text
# from that column in liststore and treat it as the text to render
# in this case it’s the name of a tweeter
self.tvcname.add_attribute(self.cell, ‘text’, 0)
# we then use the second field of our liststore to hold the colour for
# the ‘name’ text
self.tvcname.add_attribute(self.cell, ‘foreground’, 1)
# next we add a mapping to the tweet column, again the third field
# in our list store is the tweet text
self.tvctweet.add_attribute(self.cell, ‘text’,2)
# and the fourth is the colour of the tweet text
self.tvctweet.add_attribute(self.cell, ‘foreground’, 3)
# we start up non-fullscreen, and we want the tweets to appear without
# scrolling left-right (well I wanted that) so I set a wrap width for
# the text being rendered
self.cell.set_property(‘wrap-width’, 500)
# make it searchable (I found this in an example and thought I might use it
# but currently I make no use of this setting
self.treeview.set_search_column(0)
# Allow sorting on the column. This is cool because no matter what order
# we load tweets in, we always get a view which is sorted by the tweet id which
# always increments, so we get them in order
self.liststore.set_sort_column_id(4,gtk.SORT_DESCENDING)
# I don’t want to accidentally be dragging and dropping rows out of order
self.treeview.set_reorderable(False)
#with all that done I add the treeview to the scrolled window
self.scrolled_window.add(self.treeview)
# Then just ‘pack# the scrolled window and a Hbox into the
# V box
self.box1.pack_start(self.scrolled_window, True, True, 0)
self.box1.pack_start(self.buttonBox, False, True,0)
#and pack the hbox with input field and buttons
self.buttonBox.pack_start(self.tweetInput, True,True,0)
self.buttonBox.pack_start(self.buttonnewTweet, False, False,0)
self.buttonBox.pack_start(self.buttonloadTweets, False, False,0)
#setup some urllib things to use to fetch twitter feeds
self.last_id=None
def quit(self, *args):
#this is our end method called when window is closed
print “Stop Wittering”
gtk.main_quit()
def create_menu(self, widget):
#a fairly standard menu create
#I put in the same options as I have buttons
# and linked to the same methods
menu = gtk.Menu()
menuItemGetTweets = gtk.MenuItem(“Get Tweets”)
menuItemGetTweets.connect(“activate”, self.getTweets )
menuItemTweet = gtk.MenuItem(“Tweet”)
menuItemTweet.connect(“activate”,self.newTweet)
menuItemSeparator = gtk.SeparatorMenuItem()
menuItemExit = gtk.MenuItem(“Exit”)
menuItemExit.connect(“activate”, self.quit);
menu.append(menuItemGetTweets)
menu.append(menuItemTweet)
menu.append(menuItemSeparator)
menu.append(menuItemExit)
menuItemFile = gtk.MenuItem(“File”)
menuItemFile.set_submenu(menu)
return menu
def run(self):
#this is the main execution method
# we set things visible, connect a couple of event hooks to methods
# specifically to handle switching in and our of fullscreen
self.window.show_all()
self.window.connect(“key-press-event”, self.on_key_press)
self.window.connect(“window-state-event”, self.on_window_state_change)
#this starts everything up
gtk.main()
def getTweets(self, *args):
#Now for the main logic…fetching tweets
#at the moment I’m just using basic auth.
#urllib2 provides all the HTTP handling stuff
auth_handler = urllib2.HTTPBasicAuthHandler()
#realm here is important. or at least it seemed to be
#this info is on the login box if you go to the url in a browser
auth_handler.add_password(realm=’Twitter API’,
uri=’http://twitter.com/statuses/friends_timeline.json’,
user=self.username,
passwd=self.password)
#we create an ‘opener’ object with our auth_handler
opener = urllib2.build_opener(auth_handler)
# …and install it globally so it can be used with urlopen.
urllib2.install_opener(opener)
#switch on whether this is an refresh or a first download
if self.last_id == None:
json = urllib2.urlopen(‘http://twitter.com/statuses/friends_timeline.json’)
else:
#basically the twitter API will respond with just tweets newer than the ID we send
json = urllib2.urlopen(‘http://twitter.com/statuses/friends_timeline.json?since_id=’+str(self.last_id)+’L’)
#JSON is awesome stuff. we get given a long string of json encoded information
#which contains all the tweets, with lots of info, we decode to a json object
data = simplejson.loads(json.read())
#then this line does all the hard work. Basicaly for evey top level object in the JSON
#structure we call out getStatus method with the contents of the USER structure
#and the values of top level values text/id/created_at
[self.getStatus(x[‘user’],x[‘text’], x[‘id’], x[‘created_at’]) for x in data]
def getStatus(self, user,data, id, created_at):
#at this point user is another JSON structure of lots more values of which we are currently
#only interested in screen_name
#append to our list store the values from the JSON data we’ve been passed for a tweet
# the funny #NXNXNX type values are colours I chose a slightly blue for the name
# and black for the tweet. At some point I intend to do some alternating colours for
# cell backgrounds to make the display clearer
self.liststore.append([ user[‘screen_name’],”#2E00B8″,data+”nposted on: “+created_at,”#000000”, id])
#now we process the id, this is so we can do a refresh with just the posts since the latest one we have
#if we haven’t stored the most recent id then store this one
if self.last_id == None:
self.last_id=id
else:
#if we have an id stored, check if this one is ‘newer’ if so then store it
if long(self.last_id) < long(id):
self.last_id=id
def newTweet(self, widget, text_widget,*args):
#The other main need of a twitter client
#the ability to post an update
#get the tweet text from the input box
tweet = text_widget.get_text()
#see if we have just an empty string (eg eroneous button press)
if (tweet == “”):
return
#we get the text in the input box then we construct the outbound tweet
#first we need to encode for utf-8
tweet = unicode(tweet).encode(‘utf-8’)
#then we need to urlencode so that we can use twitter chars like @ without
#causing problems
post = urllib.urlencode({ ‘status’ : tweet })
#build the request with the url and our post data
req = urllib2.Request(‘http://twitter.com/statuses/update.json’, post)
#setup the auth stuff
auth_handler = urllib2.HTTPBasicAuthHandler()
auth_handler.add_password(realm=’Twitter API’,
uri=’http://twitter.com/statuses/update.json’,
user=self.username,
passwd=self.password)
opener = urllib2.build_opener(auth_handler)
# …and install it globally so it can be used with urlopen.
urllib2.install_opener(opener)
json = urllib2.urlopen(req)
data = simplejson.loads(json.read())
#message sent, I’m assuming a failure to send would not continue
#in this method? so it’s safe to remove the tweet line
# what I don’t want is to lose the tweet I typed if we didn’t
# sucessfully send it to twitter. that would be annoying (I’m looking
# at you Mauku)
text_widget.set_text(“”);
def on_window_state_change(self, widget, event, *args):
#this just sets a flag to keep track of what state we’re in
if event.new_window_state & gtk.gdk.WINDOW_STATE_FULLSCREEN:
self.window_in_fullscreen = True
else:
self.window_in_fullscreen = False
def on_key_press(self, widget, event, *args):
#this picks up the press of the full screen key and toggles
#from one mode to the other
if event.keyval == gtk.keysyms.F6:
# The “Full screen” hardware key has been pressed
if self.window_in_fullscreen:
self.window.unfullscreen ()
#when we toggle off fullscreen set the cell render wrap
#to 500
self.cell.set_property(‘wrap-width’, 500)
else:
self.window.fullscreen ()
#when we toggle into fullscreen set the cell render wrap
#wider
self.cell.set_property(‘wrap-width’, 630)
if __name__ == “__main__”:
#this is just what initialises the app and calls run
app = Witter()
app.run()
And that’s it. I used esbox to develop and just had it using SCP/SSH to copy accross to my n810 and execute directly there, which was a pretty easy way to develop.
There are lots of things I will now go on to add to this client. Things like checking for replies/DMs. Being able to make easy reference to an URLs in tweets, and reply to people etc etc. But I wanted to show the code of the bare bones working in case it helped anyone else get started with developing apps for Maemo.
6 responses to “Witter – a basic python twitter client for Maemo”
I tried to run the app on my n900 as an example but I got an error saying “ImportError: No module named simplejson”
How do I add the simplejson module for use with python?
if you install witter from the application manager then it will install the pre-reqs. if you just want to play with python then you’ll need to look for the python modules and use apt-get to install them manually.if you are developing your own app it’s worth using the apt-get method to keep track of the dependancies.
apt-get needs you to be root. but then something like:
apt-get install python-simplejson
thanks for sharing. I have edited the self.username & self.password with my login credentials and tried to run it from my maemo5-N900. No errors and app launched, but when I clicked “load tweets”, the following error:
Traceback (most recent call last):
File “witter.py”, line 175, in getTweets
json = urllib2.urlopen(‘http://twitter.com/statuses/friends_timeline.json’)
File “/usr/lib/python2.5/urllib2.py”, line 124, in urlopen
return _opener.open(url, data)
File “/usr/lib/python2.5/urllib2.py”, line 387, in open
response = meth(req, response)
File “/usr/lib/python2.5/urllib2.py”, line 498, in http_response
‘http’, request, response, code, msg, hdrs)
File “/usr/lib/python2.5/urllib2.py”, line 419, in error
result = self._call_chain(*args)
File “/usr/lib/python2.5/urllib2.py”, line 360, in _call_chain
result = func(*args)
File “/usr/lib/python2.5/urllib2.py”, line 823, in http_error_401
url, req, headers)
File “/usr/lib/python2.5/urllib2.py”, line 801, in http_error_auth_reqed
return self.retry_http_basic_auth(host, req, realm)
File “/usr/lib/python2.5/urllib2.py”, line 811, in retry_http_basic_auth
return self.parent.open(req)
File “/usr/lib/python2.5/urllib2.py”, line 387, in open
response = meth(req, response)
File “/usr/lib/python2.5/urllib2.py”, line 498, in http_response
‘http’, request, response, code, msg, hdrs)
File “/usr/lib/python2.5/urllib2.py”, line 425, in error
return self._call_chain(*args)
File “/usr/lib/python2.5/urllib2.py”, line 360, in _call_chain
result = func(*args)
File “/usr/lib/python2.5/urllib2.py”, line 506, in http_error_default
raise HTTPError(req.get_full_url(), code, msg, hdrs, fp)
urllib2.HTTPError: HTTP Error 401: Unauthorized
I tried manually from my Desktop-XP machine the following uri:http://twitter.com/statuses/friends_timeline.json
it prompted for 2 times to enter the login credentials & i entered the same for twice. But it gave the following error:
This webpage is not available.
The webpage at http://twitter.com/statuses/friends_timeline.json might be temporarily down or it may have moved permanently to a new web address.
this code won’t work anymore because twitter has disabled basic auth support. it is now required to use oauth.if you install witter from the n900 application manager you will get a version that supports oauth.
I have installed witter app on my N900 and works fine. Now i want to make above code to make it work with oauth support. Can I do that?? I want to do because above code is a bit easy to understand when compared the currently installed witter code. I am trying to understand the basic functionality of Witter client.
the code from this version does all it’s own url handling stuff tto talk to twitter. in order to work with oauth i switched to using the python-twitter library and the oauth library that is availiable to sit on top of it. basically i gave up on the idea of implementing that myself, and just used what was available.