add logging support
[minibook.git] / minibook.py
blob03166d7b28f465ff24e0694f246ff5ca496f0741
1 #!/usr/bin/env python
2 """ Minibook: the Facebook(TM) status updater
3 (C) 2009 Gergely Imreh <imrehg@gmail.com>
4 """
6 VERSION = '0.1.0'
7 APPNAME = 'minibook'
9 import pygtk
10 pygtk.require('2.0')
11 import gtk
12 import gobject
13 try:
14 from facebook import Facebook
15 except:
16 print "Pyfacebook is not available, cannot run."
17 exit(1)
19 import time
20 import re
21 import threading
23 gobject.threads_init()
24 gtk.gdk.threads_init()
25 gtk.gdk.threads_enter()
27 try:
28 import gtkspell
29 spelling_support = True
30 except:
31 spelling_support = False
33 import logging
34 import sys
36 LEVELS = {'debug': logging.DEBUG,
37 'info': logging.INFO,
38 'warning': logging.WARNING,
39 'error': logging.ERROR,
40 'critical': logging.CRITICAL}
42 if len(sys.argv) > 1:
43 level_name = sys.argv[1]
44 level = LEVELS.get(level_name, logging.NOTSET)
45 logging.basicConfig(level=level)
48 class Columns:
49 (STATUSID, UID, STATUS, DATETIME, REPLIES, LIKES) = range(6)
52 #-------------------------------------------------
53 # From http://edsiper.linuxchile.cl/blog/?p=152
54 # to mitigate TreeView + threads problems
55 #-------------------------------------------------
57 class _IdleObject(gobject.GObject):
58 """
59 Override gobject.GObject to always emit signals in the main thread
60 by emmitting on an idle handler
61 """
63 def __init__(self):
64 gobject.GObject.__init__(self)
66 def emit(self, *args):
67 gobject.idle_add(gobject.GObject.emit, self, *args)
70 #-------------------------------------------------
71 # Thread support
72 #-------------------------------------------------
74 class _WorkerThread(threading.Thread, _IdleObject):
75 """A single working thread."""
77 __gsignals__ = {
78 "completed": (
79 gobject.SIGNAL_RUN_LAST,
80 gobject.TYPE_NONE,
81 (gobject.TYPE_PYOBJECT, )),
82 "exception": (
83 gobject.SIGNAL_RUN_LAST,
84 gobject.TYPE_NONE,
85 (gobject.TYPE_PYOBJECT, ))}
87 def __init__(self, function, *args, **kwargs):
88 threading.Thread.__init__(self)
89 _IdleObject.__init__(self)
90 self._function = function
91 self._args = args
92 self._kwargs = kwargs
94 def run(self):
95 # call the function
96 print('Thread %s calling %s' % (self.name, str(self._function)))
98 args = self._args
99 kwargs = self._kwargs
101 try:
102 result = self._function(*args, **kwargs)
103 except Exception, exc: # Catch ALL exceptions
104 # TODO: Check if this catch all warnins too!
105 print('Exception %s' % str(exc))
106 self.emit("exception", exc)
107 return
109 print('Thread %s completed' % (self.name))
111 self.emit("completed", result)
112 return
115 class _ThreadManager(object):
116 """Manages the threads."""
118 def __init__(self, max_threads=2):
119 """Start the thread pool. The number of threads in the pool is defined
120 by `pool_size`, defaults to 2."""
121 self._max_threads = max_threads
122 self._thread_pool = []
123 self._running = []
124 self._thread_id = 0
126 return
128 def _remove_thread(self, widget, arg=None):
129 """Called when the thread completes. We remove it from the thread list
130 (dictionary, actually) and start the next thread (if there is one)."""
132 # not actually a widget. It's the object that emitted the signal, in
133 # this case, the _WorkerThread object.
134 thread_id = widget.name
136 print('Thread %s completed, %d threads in the queue' % (thread_id,
137 len(self._thread_pool)))
139 self._running.remove(thread_id)
141 if self._thread_pool:
142 if len(self._running) < self._max_threads:
143 next = self._thread_pool.pop()
144 print('Dequeuing thread %s', next.name)
145 self._running.append(next.name)
146 next.start()
148 return
150 def add_work(self, complete_cb, exception_cb, func, *args, **kwargs):
151 """Add a work to the thread list."""
153 thread = _WorkerThread(func, *args, **kwargs)
154 thread_id = '%s' % (self._thread_id)
156 thread.connect('completed', complete_cb)
157 thread.connect('completed', self._remove_thread)
158 thread.connect('exception', exception_cb)
159 thread.setName(thread_id)
161 if len(self._running) < self._max_threads:
162 self._running.append(thread_id)
163 thread.start()
164 else:
165 running_names = ', '.join(self._running)
166 print('Threads %s running, adding %s to the queue',
167 running_names, thread_id)
168 self._thread_pool.append(thread)
170 self._thread_id += 1
171 return
174 class MainWindow:
175 """The main application interface"""
178 #------------------------------
179 # Information sending functions
180 #------------------------------
181 def sendupdate(self):
182 textfield = self.entry.get_buffer()
183 start = textfield.get_start_iter()
184 end = textfield.get_end_iter()
185 entry_text = textfield.get_text(start, end)
186 if entry_text != "":
187 print "Sent entry contents: %s\n" % entry_text
188 self._facebook.status.set([entry_text], [self._facebook.uid])
190 textfield.set_text("")
191 self.refresh()
193 #------------------------------
194 # Information pulling functions
195 #------------------------------
196 def get_friends_list(self):
197 query = ("SELECT uid, name FROM user \
198 WHERE (uid IN (SELECT uid2 FROM friend WHERE uid1 = %d) \
199 OR uid = %d)" % (self._facebook.uid, self._facebook.uid))
200 friends = self._facebook.fql.query([query])
201 self.friendsname = {}
202 for friend in friends:
203 self.friendsname[str(friend['uid'])] = friend['name']
205 def post_get_friends_list(self, widget, results):
206 print("%s has altogether %d friends in the database." \
207 % (self.friendsname[str(self._facebook.uid)],
208 len(self.friendsname.keys())))
209 self.refresh()
210 return
212 def except_get_friends_list(self, widget, exception):
213 print("Get friends exception: %s" % (str(exception)))
215 def get_status_list(self):
216 if self._last_update > 0:
217 since = self._last_update
218 else:
219 now = int(time.time())
220 since = now - 5*24*60*60
222 print("---> Statuses since: %s" \
223 % (time.strftime("%c", time.localtime(since))))
224 query = ('SELECT uid, time, status_id, message FROM status \
225 WHERE (uid IN (SELECT uid2 FROM friend WHERE uid1 = %d) \
226 OR uid = %d) \
227 AND time > %d \
228 ORDER BY time DESC\
229 LIMIT 60' \
230 % (self._facebook.uid, self._facebook.uid, since))
231 status = self._facebook.fql.query([query])
232 for up in status:
233 self.liststore.append((up['status_id'],
234 up['uid'],
235 up['message'],
236 up['time'],
237 '0',
238 '0'))
240 def post_get_status_list(self, widget, results):
241 print("Status updates successfully pulled.")
242 return
244 def except_get_status_list(self, widget, exception):
245 print("Get status list exception: %s" % (str(exception)))
247 #-----------------
248 # Helper functions
249 #-----------------
250 def count(self, text):
251 start = text.get_start_iter()
252 end = text.get_end_iter()
253 thetext = text.get_text(start, end)
254 self.count_label.set_text('(%d)' % (160 - len(thetext)))
255 return True
257 def set_auto_refresh(self):
258 if self._refresh_id:
259 gobject.source_remove(self._refresh_id)
261 self._refresh_id = gobject.timeout_add(
262 self._prefs['auto_refresh_interval']*60*1000,
263 self.refresh)
264 print("Auto-refresh enabled: %d minutes" \
265 % (self._prefs['auto_refresh_interval']))
267 def refresh(self):
268 self._threads.add_work(self.post_get_status_list,
269 self.except_get_status_list,
270 self.get_status_list)
271 return True
273 def status_format(self, column, cell, store, position):
274 uid = store.get_value(position, Columns.UID)
275 try:
276 name = self.friendsname[str(uid)]
277 except:
278 print store.get_value(position, Columns.STATUS)
279 print store.get_value(position, Columns.UID)
280 print store.get_value(position, Columns.DATETIME)
282 status = store.get_value(position, Columns.STATUS)
283 datetime = time.localtime(float(store.get_value(position, \
284 Columns.DATETIME)))
285 displaytime = time.strftime('%c', datetime)
287 #replace characters that would choke the markup
288 status = re.sub(r'<', r'&lt;', status)
289 status = re.sub(r'>', r'&gt;', status)
290 markup = '<b>%s</b> %s\non %s' % \
291 (name, status, displaytime)
292 cell.set_property('markup', markup)
293 return
295 #--------------------
296 # Interface functions
297 #--------------------
298 def systray_click(self, widget, user_param=None):
299 if self.window.get_property('visible'):
300 x, y = self.window.get_position()
301 self._prefs['window_pos_x'] = x
302 self._prefs['window_pos_y'] = y
303 self.window.hide()
304 else:
305 x = self._prefs['window_pos_x']
306 y = self._prefs['window_pos_y']
307 self.window.move(x, y)
308 self.window.deiconify()
309 self.window.present()
311 def create_grid(self):
312 self.liststore = gtk.ListStore(gobject.TYPE_STRING,
313 gobject.TYPE_INT,
314 gobject.TYPE_STRING,
315 gobject.TYPE_STRING,
316 gobject.TYPE_STRING,
317 gobject.TYPE_STRING)
318 self.treeview = gtk.TreeView(self.liststore)
319 self.treeview.set_property('headers-visible', False)
320 self.treeview.set_rules_hint(True)
322 self.status_renderer = gtk.CellRendererText()
323 #~ self.status_renderer.set_property('wrap-mode', gtk.WRAP_WORD)
324 self.status_renderer.set_property('wrap-width', 350)
325 self.status_renderer.set_property('width', 10)
327 self.status_column = gtk.TreeViewColumn('Message', \
328 self.status_renderer, text=1)
329 self.status_column.set_cell_data_func(self.status_renderer, \
330 self.status_format)
331 self.treeview.append_column(self.status_column)
332 self.treeview.set_resize_mode(gtk.RESIZE_IMMEDIATE)
334 #------------------
335 # Main Window start
336 #------------------
337 def __init__(self, facebook):
338 global spelling_support
340 # create a new window
341 self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
342 self.window.set_size_request(400, 250)
343 self.window.set_title("Minibook")
344 self.window.connect("delete_event", lambda w, e: gtk.main_quit())
346 vbox = gtk.VBox(False, 0)
347 self.window.add(vbox)
348 vbox.show()
350 self.create_grid()
351 self.statuslist_window = gtk.ScrolledWindow()
352 self.statuslist_window.set_policy(gtk.POLICY_NEVER, gtk.POLICY_ALWAYS)
353 self.statuslist_window.add(self.treeview)
354 self.treeview.show()
355 self.statuslist_window.show()
356 vbox.add(self.statuslist_window)
358 hbox = gtk.HBox(False, 0)
360 label = gtk.Label("What's on your mind?")
361 hbox.pack_start(label, True, True, 0)
362 label.show()
363 self.count_label = gtk.Label("(160)")
364 hbox.pack_start(self.count_label, True, True, 0)
365 self.count_label.show()
366 vbox.add(hbox)
367 hbox.show()
369 self.entry = gtk.TextView()
370 text = self.entry.get_buffer()
371 text.connect('changed', self.count)
372 vbox.pack_start(self.entry, True, True, 0)
373 self.entry.show()
375 hbox = gtk.HBox(False, 0)
376 vbox.add(hbox)
377 hbox.show()
379 button = gtk.Button(stock=gtk.STOCK_CLOSE)
380 button.connect("clicked", lambda w: gtk.main_quit())
381 hbox.pack_start(button, True, True, 0)
382 button.set_flags(gtk.CAN_DEFAULT)
383 button.grab_default()
384 button.show()
386 button = gtk.Button(stock=gtk.STOCK_ADD)
387 button.connect("clicked", lambda w: self.sendupdate())
388 hbox.pack_start(button, True, True, 0)
389 button.set_flags(gtk.CAN_DEFAULT)
390 button.grab_default()
391 button.show()
393 if spelling_support:
394 try:
395 spelling = gtkspell.Spell(self.entry, 'en')
396 except:
397 spelling_support = False
399 self.window.show()
400 self._facebook = facebook
402 self._app_icon = 'minibook.png'
403 self._systray = gtk.StatusIcon()
404 self._systray.set_from_file(self._app_icon)
405 self._systray.set_tooltip('%s\n' \
406 'Left-click: toggle window hiding' % (APPNAME))
407 self._systray.connect('activate', self.systray_click)
408 self._systray.set_visible(True)
410 self.window.set_icon_from_file(self._app_icon)
412 self._threads = _ThreadManager()
414 self.userinfo = self._facebook.users.getInfo([self._facebook.uid], \
415 ['name'])[0]
416 #~ self._threads.add_work(self.post_updates,
417 #~ self.except_updates,
418 #~ self.getupdates)
419 self._last_update = 0
420 self._threads.add_work(self.post_get_friends_list,
421 self.except_get_friends_list,
422 self.get_friends_list)
424 self._prefs = {}
425 x, y = self.window.get_position()
426 self._prefs['window_pos_x'] = x
427 self._prefs['window_pos_y'] = y
428 self._prefs['auto_refresh_interval'] = 5
430 self._refresh_id = None
431 self.set_auto_refresh()
434 def main(facebook):
435 gtk.main()
436 gtk.gdk.threads_leave()
437 return 0
439 if __name__ == "__main__":
440 try:
441 config_file = open("config", "r")
442 api_key = config_file.readline()[:-1]
443 secret_key = config_file.readline()[:-1]
444 except Exception, e:
445 exit('Error while loading config file: %s' % (str(e)))
446 facebook = Facebook(api_key, secret_key)
447 facebook.auth.createToken()
448 facebook.login()
450 # Delay dialog to allow for login in browser
451 dia = gtk.Dialog('minibook: login',
452 None,
453 gtk.DIALOG_MODAL | \
454 gtk.DIALOG_DESTROY_WITH_PARENT | \
455 gtk.DIALOG_NO_SEPARATOR,
456 ("Logged In", gtk.RESPONSE_OK, gtk.STOCK_CANCEL, gtk.RESPONSE_CLOSE))
457 label = gtk.Label("Click after logging in to Facebook in your browser:")
458 dia.vbox.pack_start(label, True, True, 10)
459 label.show()
460 dia.show()
461 result = dia.run()
462 if result == gtk.RESPONSE_CLOSE:
463 print "Bye"
464 exit(0)
465 dia.destroy()
467 facebook.auth.getSession()
468 print 'Session Key: ', facebook.session_key
469 print 'Your UID: ', facebook.uid
471 MainWindow(facebook)
472 main(facebook)