2 """ Minibook: the Facebook(TM) status updater
3 (C) 2009 Gergely Imreh <imrehg@gmail.com>
14 from facebook
import Facebook
16 print "Pyfacebook is not available, cannot run."
23 gobject
.threads_init()
24 gtk
.gdk
.threads_init()
25 gtk
.gdk
.threads_enter()
29 spelling_support
= True
31 spelling_support
= False
37 LEVELS
= {'debug': logging
.DEBUG
,
39 'warning': logging
.WARNING
,
40 'error': logging
.ERROR
,
41 'critical': logging
.CRITICAL
}
44 level_name
= sys
.argv
[1]
45 level
= LEVELS
.get(level_name
, logging
.NOTSET
)
46 logging
.basicConfig(level
=level
)
48 _log
= logging
.getLogger('minibook')
52 (STATUSID
, UID
, STATUS
, DATETIME
, REPLIES
, LIKES
) = range(6)
55 #-------------------------------------------------
56 # From http://edsiper.linuxchile.cl/blog/?p=152
57 # to mitigate TreeView + threads problems
58 #-------------------------------------------------
60 class _IdleObject(gobject
.GObject
):
62 Override gobject.GObject to always emit signals in the main thread
63 by emmitting on an idle handler
67 gobject
.GObject
.__init
__(self
)
69 def emit(self
, *args
):
70 gobject
.idle_add(gobject
.GObject
.emit
, self
, *args
)
73 #-------------------------------------------------
75 #-------------------------------------------------
77 class _WorkerThread(threading
.Thread
, _IdleObject
):
78 """A single working thread."""
82 gobject
.SIGNAL_RUN_LAST
,
84 (gobject
.TYPE_PYOBJECT
, )),
86 gobject
.SIGNAL_RUN_LAST
,
88 (gobject
.TYPE_PYOBJECT
, ))}
90 def __init__(self
, function
, *args
, **kwargs
):
91 threading
.Thread
.__init
__(self
)
92 _IdleObject
.__init
__(self
)
93 self
._function
= function
99 _log
.debug('Thread %s calling %s' % (self
.name
, str(self
._function
)))
102 kwargs
= self
._kwargs
105 result
= self
._function
(*args
, **kwargs
)
106 except Exception, exc
:
107 _log
.error('Exception %s' % str(exc
))
108 self
.emit("exception", exc
)
111 _log
.debug('Thread %s completed' % (self
.name
))
113 self
.emit("completed", result
)
117 class _ThreadManager(object):
118 """Manages the threads."""
120 def __init__(self
, max_threads
=2):
121 """Start the thread pool. The number of threads in the pool is defined
122 by `pool_size`, defaults to 2."""
123 self
._max
_threads
= max_threads
124 self
._thread
_pool
= []
130 def _remove_thread(self
, widget
, arg
=None):
131 """Called when the thread completes. We remove it from the thread list
132 (dictionary, actually) and start the next thread (if there is one)."""
134 # not actually a widget. It's the object that emitted the signal, in
135 # this case, the _WorkerThread object.
136 thread_id
= widget
.name
138 _log
.debug('Thread %s completed, %d threads in the queue' % (thread_id
,
139 len(self
._thread
_pool
)))
141 self
._running
.remove(thread_id
)
143 if self
._thread
_pool
:
144 if len(self
._running
) < self
._max
_threads
:
145 next
= self
._thread
_pool
.pop()
146 _log
.debug('Dequeuing thread %s', next
.name
)
147 self
._running
.append(next
.name
)
152 def add_work(self
, complete_cb
, exception_cb
, func
, *args
, **kwargs
):
153 """Add a work to the thread list."""
155 thread
= _WorkerThread(func
, *args
, **kwargs
)
156 thread_id
= '%s' % (self
._thread
_id
)
158 thread
.connect('completed', complete_cb
)
159 thread
.connect('completed', self
._remove
_thread
)
160 thread
.connect('exception', exception_cb
)
161 thread
.setName(thread_id
)
163 if len(self
._running
) < self
._max
_threads
:
164 self
._running
.append(thread_id
)
167 running_names
= ', '.join(self
._running
)
168 _log
.debug('Threads %s running, adding %s to the queue',
169 running_names
, thread_id
)
170 self
._thread
_pool
.append(thread
)
177 """The main application interface"""
180 #------------------------------
181 # Information sending functions
182 #------------------------------
183 def sendupdate(self
):
184 textfield
= self
.entry
.get_buffer()
185 start
= textfield
.get_start_iter()
186 end
= textfield
.get_end_iter()
187 entry_text
= textfield
.get_text(start
, end
)
189 _log
.info('Sent status update: %s\n' % entry_text
)
190 self
._facebook
.status
.set([entry_text
], [self
._facebook
.uid
])
192 textfield
.set_text("")
195 #------------------------------
196 # Information pulling functions
197 #------------------------------
198 def get_friends_list(self
):
199 query
= ("SELECT uid, name FROM user \
200 WHERE (uid IN (SELECT uid2 FROM friend WHERE uid1 = %d) \
201 OR uid = %d)" % (self
._facebook
.uid
, self
._facebook
.uid
))
202 friends
= self
._facebook
.fql
.query([query
])
203 self
.friendsname
= {}
204 for friend
in friends
:
205 self
.friendsname
[str(friend
['uid'])] = friend
['name']
207 def post_get_friends_list(self
, widget
, results
):
208 _log
.info('%s has altogether %d friends in the database.' \
209 % (self
.friendsname
[str(self
._facebook
.uid
)],
210 len(self
.friendsname
.keys())))
214 def except_get_friends_list(self
, widget
, exception
):
215 _log
.error("Get friends exception: %s" % (str(exception
)))
217 def get_status_list(self
):
218 if self
._last
_update
> 0:
219 since
= self
._last
_update
221 now
= int(time
.time())
222 since
= now
- 5*24*60*60
223 till
= int(time
.time())
225 _log
.info("Fetching status updates published between %s and %s" \
226 % (time
.strftime("%c", time
.localtime(since
)),
227 time
.strftime("%c", time
.localtime(since
))))
229 query
= ('SELECT uid, time, status_id, message FROM status \
230 WHERE (uid IN (SELECT uid2 FROM friend WHERE uid1 = %d) \
232 AND time > %d AND time < %d\
235 % (self
._facebook
.uid
, self
._facebook
.uid
, since
, till
))
236 _log
.debug('Status list query: %s' % (query
))
238 status
= self
._facebook
.fql
.query([query
])
240 _log
.info('Received %d new status' % (len(status
)))
241 return [status
, till
]
243 def post_get_status_list(self
, widget
, results
):
244 _log
.debug('Status updates successfully pulled.')
247 self
.liststore
.prepend((up
['status_id'],
253 self
._last
_update
= results
[1]
256 def except_get_status_list(self
, widget
, exception
):
257 _log
.error("Get status list exception: %s" % (str(exception
)))
262 def count(self
, text
):
263 start
= text
.get_start_iter()
264 end
= text
.get_end_iter()
265 thetext
= text
.get_text(start
, end
)
266 self
.count_label
.set_text('(%d)' % (160 - len(thetext
)))
269 def set_auto_refresh(self
):
271 gobject
.source_remove(self
._refresh
_id
)
273 self
._refresh
_id
= gobject
.timeout_add(
274 self
._prefs
['auto_refresh_interval']*60*1000,
276 _log
.info("Auto-refresh enabled: %d minutes" \
277 % (self
._prefs
['auto_refresh_interval']))
280 _log
.info('Refreshing now at %s' % (time
.strftime('%H:%M:%S')))
281 self
._threads
.add_work(self
.post_get_status_list
,
282 self
.except_get_status_list
,
283 self
.get_status_list
)
286 def status_format(self
, column
, cell
, store
, position
):
287 uid
= store
.get_value(position
, Columns
.UID
)
288 name
= self
.friendsname
[str(uid
)]
289 status
= store
.get_value(position
, Columns
.STATUS
)
290 posttime
= store
.get_value(position
, Columns
.DATETIME
)
292 #replace characters that would choke the markup
293 status
= re
.sub(r
'&', r
'&', status
)
294 status
= re
.sub(r
'<', r
'<', status
)
295 status
= re
.sub(r
'>', r
'>', status
)
296 markup
= ('<b>%s</b> %s\n(%s ago)' % \
297 (name
, status
, timesince
.timesince(posttime
)))
298 _log
.debug('Marked up text: %s' % (markup
))
299 cell
.set_property('markup', markup
)
302 def open_url(self
, source
, url
):
303 """Open url as new browser tab."""
304 _log
.debug('Opening url: %s' % url
)
306 webbrowser
.open_new_tab(url
)
307 self
.window
.set_focus(self
.entry
)
309 #--------------------
310 # Interface functions
311 #--------------------
312 def systray_click(self
, widget
, user_param
=None):
313 if self
.window
.get_property('visible'):
314 _log
.debug('Hiding window')
315 x
, y
= self
.window
.get_position()
316 self
._prefs
['window_pos_x'] = x
317 self
._prefs
['window_pos_y'] = y
320 x
= self
._prefs
['window_pos_x']
321 y
= self
._prefs
['window_pos_y']
322 _log
.debug('Restoring window at (%d, %d)' % (x
, y
))
323 self
.window
.move(x
, y
)
324 self
.window
.deiconify()
325 self
.window
.present()
327 def create_grid(self
):
328 self
.liststore
= gtk
.ListStore(gobject
.TYPE_STRING
,
334 self
.liststore
.set_sort_column_id(Columns
.DATETIME
, \
336 self
.liststore
.set_sort_func(Columns
.DATETIME
, \
337 self
._order
_datetime
)
339 self
.treeview
= gtk
.TreeView(self
.liststore
)
340 self
.treeview
.set_property('headers-visible', False)
341 self
.treeview
.set_rules_hint(True)
343 self
.status_renderer
= gtk
.CellRendererText()
344 #~ self.status_renderer.set_property('wrap-mode', gtk.WRAP_WORD)
345 self
.status_renderer
.set_property('wrap-width', 350)
346 self
.status_renderer
.set_property('width', 10)
348 self
.status_column
= gtk
.TreeViewColumn('Message', \
349 self
.status_renderer
, text
=1)
350 self
.status_column
.set_cell_data_func(self
.status_renderer
, \
352 self
.treeview
.append_column(self
.status_column
)
353 self
.treeview
.set_resize_mode(gtk
.RESIZE_IMMEDIATE
)
355 self
.treeview
.connect('row-activated', self
.open_status_web
)
356 self
.treeview
.connect('button-press-event', self
.click_status
)
358 def open_status_web(self
, treeview
, path
, view_column
, user_data
=None):
359 """ Callback to open status update in web browser when received
362 model
= treeview
.get_model()
364 iter = model
.get_iter(path
)
365 uid
= model
.get_value(iter, Columns
.UID
)
366 status_id
= model
.get_value(iter, Columns
.STATUSID
)
367 status_url
= ('http://www.facebook.com/profile.php?' \
368 'id=%d&v=feed&story_fbid=%s' % (uid
, status_id
))
369 self
.open_url(path
, status_url
)
372 def click_status(self
, treeview
, event
, user_data
=None):
373 """Callback when a mouse click event occurs on one of the rows."""
374 _log
.debug('clicked on status list')
375 if event
.button
!= 3:
376 # Only right clicks are processed
378 _log
.debug('right-click received')
383 pth
= treeview
.get_path_at_pos(x
, y
)
385 # The click wasn't on a row
388 path
, col
, cell_x
, cell_y
= pth
389 treeview
.grab_focus()
390 treeview
.set_cursor(path
, col
, 0)
392 self
.show_status_popup(treeview
, event
)
395 def show_status_popup(self
, treeview
, event
, user_data
=None):
396 _log
.debug('show popup menu')
397 cursor
= treeview
.get_cursor()
402 model
= treeview
.get_model()
405 iter = model
.get_iter(path
)
407 popup_menu
= gtk
.Menu()
408 popup_menu
.set_screen(self
.window
.get_screen())
410 # An open submenu with various choices underneath
413 uid
= model
.get_value(iter, Columns
.UID
)
414 status_id
= model
.get_value(iter, Columns
.STATUSID
)
415 url
= ('http://www.facebook.com/profile.php?' \
416 'id=%d&v=feed&story_fbid=%s' % (uid
, status_id
))
417 item_name
= 'This status'
418 item
= gtk
.MenuItem(item_name
)
419 item
.connect('activate', self
.open_url
, url
)
420 open_menu_items
.append(item
)
422 url
= ('http://www.facebook.com/profile.php?' \
424 item_name
= 'User wall'
425 item
= gtk
.MenuItem(item_name
)
426 item
.connect('activate', self
.open_url
, url
)
427 open_menu_items
.append(item
)
429 url
= ('http://www.facebook.com/profile.php?' \
430 'id=%d&v=info' % (uid
))
431 item_name
= 'User info'
432 item
= gtk
.MenuItem(item_name
)
433 item
.connect('activate', self
.open_url
, url
)
434 open_menu_items
.append(item
)
436 url
= ('http://www.facebook.com/profile.php?' \
437 'id=%d&v=photos' % (uid
))
438 item_name
= 'User photos'
439 item
= gtk
.MenuItem(item_name
)
440 item
.connect('activate', self
.open_url
, url
)
441 open_menu_items
.append(item
)
443 open_menu
= gtk
.Menu()
444 for item
in open_menu_items
:
445 open_menu
.append(item
)
447 open_item
= gtk
.MenuItem("Open in browser")
448 open_item
.set_submenu(open_menu
)
450 popup_menu
.append(open_item
)
451 popup_menu
.show_all()
460 popup_menu
.popup(None, None, None, b
, t
)
464 def _order_datetime(self
, model
, iter1
, iter2
, user_data
=None):
465 """Used by the ListStore to sort the columns (in our case, "column")
467 time1
= model
.get_value(iter1
, Columns
.DATETIME
)
468 time2
= model
.get_value(iter2
, Columns
.DATETIME
)
483 def __init__(self
, facebook
):
484 global spelling_support
486 # create a new window
487 self
.window
= gtk
.Window(gtk
.WINDOW_TOPLEVEL
)
488 self
.window
.set_size_request(400, 250)
489 self
.window
.set_title("Minibook")
490 self
.window
.connect("delete_event", lambda w
, e
: gtk
.main_quit())
492 vbox
= gtk
.VBox(False, 0)
493 self
.window
.add(vbox
)
497 self
.statuslist_window
= gtk
.ScrolledWindow()
498 self
.statuslist_window
.set_policy(gtk
.POLICY_NEVER
, gtk
.POLICY_ALWAYS
)
499 self
.statuslist_window
.add(self
.treeview
)
501 self
.statuslist_window
.show()
502 vbox
.add(self
.statuslist_window
)
504 hbox
= gtk
.HBox(False, 0)
506 label
= gtk
.Label("What's on your mind?")
507 hbox
.pack_start(label
, True, True, 0)
509 self
.count_label
= gtk
.Label("(160)")
510 hbox
.pack_start(self
.count_label
, True, True, 0)
511 self
.count_label
.show()
515 self
.entry
= gtk
.TextView()
516 text
= self
.entry
.get_buffer()
517 text
.connect('changed', self
.count
)
518 vbox
.pack_start(self
.entry
, True, True, 0)
521 hbox
= gtk
.HBox(False, 0)
525 button
= gtk
.Button(stock
=gtk
.STOCK_CLOSE
)
526 button
.connect("clicked", lambda w
: gtk
.main_quit())
527 hbox
.pack_start(button
, True, True, 0)
528 button
.set_flags(gtk
.CAN_DEFAULT
)
529 button
.grab_default()
532 button
= gtk
.Button(stock
=gtk
.STOCK_ADD
)
533 button
.connect("clicked", lambda w
: self
.sendupdate())
534 hbox
.pack_start(button
, True, True, 0)
535 button
.set_flags(gtk
.CAN_DEFAULT
)
536 button
.grab_default()
541 spelling
= gtkspell
.Spell(self
.entry
, 'en')
543 spelling_support
= False
546 self
._facebook
= facebook
548 self
._app
_icon
= 'minibook.png'
549 self
._systray
= gtk
.StatusIcon()
550 self
._systray
.set_from_file(self
._app
_icon
)
551 self
._systray
.set_tooltip('%s\n' \
552 'Left-click: toggle window hiding' % (APPNAME
))
553 self
._systray
.connect('activate', self
.systray_click
)
554 self
._systray
.set_visible(True)
556 self
.window
.set_icon_from_file(self
._app
_icon
)
558 self
._threads
= _ThreadManager()
560 self
.userinfo
= self
._facebook
.users
.getInfo([self
._facebook
.uid
], \
562 self
._last
_update
= 0
563 self
._threads
.add_work(self
.post_get_friends_list
,
564 self
.except_get_friends_list
,
565 self
.get_friends_list
)
568 x
, y
= self
.window
.get_position()
569 self
._prefs
['window_pos_x'] = x
570 self
._prefs
['window_pos_y'] = y
571 self
._prefs
['auto_refresh_interval'] = 5
573 self
._refresh
_id
= None
574 self
.set_auto_refresh()
579 gtk
.gdk
.threads_leave()
580 _log
.debug('Exiting')
583 if __name__
== "__main__":
585 config_file
= open("config", "r")
586 api_key
= config_file
.readline()[:-1]
587 secret_key
= config_file
.readline()[:-1]
588 _log
.debug('Config file loaded successfully')
590 _log
.error('Error while loading config file: %s' % (str(e
)))
593 facebook
= Facebook(api_key
, secret_key
)
594 facebook
.auth
.createToken()
596 _log
.debug('Showing Facebook login page in default browser.')
598 # Delay dialog to allow for login in browser
599 dia
= gtk
.Dialog('minibook: login',
602 gtk
.DIALOG_DESTROY_WITH_PARENT | \
603 gtk
.DIALOG_NO_SEPARATOR
,
604 ("Logged In", gtk
.RESPONSE_OK
, gtk
.STOCK_CANCEL
, gtk
.RESPONSE_CLOSE
))
605 label
= gtk
.Label("Click after logging in to Facebook in your browser:")
606 dia
.vbox
.pack_start(label
, True, True, 10)
610 if result
== gtk
.RESPONSE_CLOSE
:
611 _log
.debug('Exiting before Facebook login.')
615 facebook
.auth
.getSession()
616 _log
.info('Session Key: %s' % (facebook
.session_key
))
617 _log
.info('User\'s UID: %d' % (facebook
.uid
))