fix: show "logged in" dialog again, if user clicked it too soon, Closes #24
[minibook.git] / minibook.py
blob2308f830111ffea315d8cbd2ffe8a68a6d423f8d
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'
8 MIT = """
9 Copyright (c) 2009 Gergely Imreh <imrehg@gmail.com>
11 Permission is hereby granted, free of charge, to any person obtaining a copy
12 of this software and associated documentation files (the "Software"), to deal
13 in the Software without restriction, including without limitation the rights
14 to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
15 copies of the Software, and to permit persons to whom the Software is
16 furnished to do so, subject to the following conditions:
18 The above copyright notice and this permission notice shall be included in
19 all copies or substantial portions of the Software.
21 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
22 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
23 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
24 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
25 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
26 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
27 THE SOFTWARE.
28 """
30 import pygtk
31 pygtk.require('2.0')
32 import gtk
33 import gobject
34 try:
35 from facebook import Facebook
36 except:
37 print "Pyfacebook is not available, cannot run."
38 exit(1)
40 import time
41 import re
42 import threading
44 gobject.threads_init()
45 gtk.gdk.threads_init()
46 gtk.gdk.threads_enter()
48 try:
49 import gtkspell
50 spelling_support = True
51 except:
52 spelling_support = False
54 import logging
55 import sys
56 import timesince
57 import urllib2
59 LEVELS = {'debug': logging.DEBUG,
60 'info': logging.INFO,
61 'warning': logging.WARNING,
62 'error': logging.ERROR,
63 'critical': logging.CRITICAL}
65 if len(sys.argv) > 1:
66 level_name = sys.argv[1]
67 level = LEVELS.get(level_name, logging.CRITICAL)
68 logging.basicConfig(level=level)
69 else:
70 logging.basicConfig(level=logging.CRITICAL)
72 _log = logging.getLogger('minibook')
75 class Columns:
76 (STATUSID, UID, STATUS, DATETIME, COMMENTS, LIKES) = range(6)
79 #-------------------------------------------------
80 # Threading related objects.
81 # Info http://edsiper.linuxchile.cl/blog/?p=152
82 # to mitigate TreeView + threads problems
83 # These classes are based on the code available at http://gist.github.com/51686
84 # (c) 2008, John Stowers <john.stowers@gmail.com>
85 #-------------------------------------------------
87 class _IdleObject(gobject.GObject):
88 """
89 Override gobject.GObject to always emit signals in the main thread
90 by emmitting on an idle handler
91 """
93 def __init__(self):
94 gobject.GObject.__init__(self)
96 def emit(self, *args):
97 gobject.idle_add(gobject.GObject.emit, self, *args)
100 #-------------------------------------------------
101 # Thread support
102 #-------------------------------------------------
104 class _WorkerThread(threading.Thread, _IdleObject):
105 """A single working thread."""
107 __gsignals__ = {
108 "completed": (
109 gobject.SIGNAL_RUN_LAST,
110 gobject.TYPE_NONE,
111 (gobject.TYPE_PYOBJECT, )),
112 "exception": (
113 gobject.SIGNAL_RUN_LAST,
114 gobject.TYPE_NONE,
115 (gobject.TYPE_PYOBJECT, ))}
117 def __init__(self, function, *args, **kwargs):
118 threading.Thread.__init__(self)
119 _IdleObject.__init__(self)
120 self._function = function
121 self._args = args
122 self._kwargs = kwargs
124 def run(self):
125 # call the function
126 _log.debug('Thread %s calling %s' % (self.name, str(self._function)))
128 args = self._args
129 kwargs = self._kwargs
131 try:
132 result = self._function(*args, **kwargs)
133 except Exception, exc:
134 self.emit("exception", exc)
135 return
137 _log.debug('Thread %s completed' % (self.name))
139 self.emit("completed", result)
140 return
143 class _ThreadManager(object):
144 """Manages the threads."""
146 def __init__(self, max_threads=4):
147 """Start the thread pool. The number of threads in the pool is defined
148 by `pool_size`, defaults to 4."""
149 self._max_threads = max_threads
150 self._thread_pool = []
151 self._running = []
152 self._thread_id = 0
154 return
156 def _remove_thread(self, widget, arg=None):
157 """Called when the thread completes. We remove it from the thread list
158 (dictionary, actually) and start the next thread (if there is one)."""
160 # not actually a widget. It's the object that emitted the signal, in
161 # this case, the _WorkerThread object.
162 thread_id = widget.name
164 _log.debug('Thread %s completed, %d threads in the queue' % (thread_id,
165 len(self._thread_pool)))
167 self._running.remove(thread_id)
169 if self._thread_pool:
170 if len(self._running) < self._max_threads:
171 next = self._thread_pool.pop()
172 _log.debug('Dequeuing thread %s', next.name)
173 self._running.append(next.name)
174 next.start()
176 return
178 def add_work(self, complete_cb, exception_cb, func, *args, **kwargs):
179 """Add a work to the thread list."""
181 thread = _WorkerThread(func, *args, **kwargs)
182 thread_id = '%s' % (self._thread_id)
184 thread.connect('completed', complete_cb)
185 thread.connect('completed', self._remove_thread)
186 thread.connect('exception', exception_cb)
187 thread.setName(thread_id)
189 if len(self._running) < self._max_threads:
190 self._running.append(thread_id)
191 thread.start()
192 else:
193 running_names = ', '.join(self._running)
194 _log.debug('Threads %s running, adding %s to the queue',
195 running_names, thread_id)
196 self._thread_pool.append(thread)
198 self._thread_id += 1
199 return
202 class MainWindow:
203 """The main application interface"""
206 #------------------------------
207 # Information sending functions
208 #------------------------------
209 def sendupdate(self):
210 textfield = self.entry.get_buffer()
211 start = textfield.get_start_iter()
212 end = textfield.get_end_iter()
213 entry_text = textfield.get_text(start, end)
214 if entry_text != "":
215 _log.info('Sending status update: %s\n' % entry_text)
216 self.statusbar.pop(self.statusbar_context)
217 self.statusbar.push(self.statusbar_context, \
218 "Sending your status update")
219 self._facebook.status.set([entry_text], [self._facebook.uid])
221 textfield.set_text("")
222 self.statusbar.pop(self.statusbar_context)
224 # wait a little before getting new updates, so FB can catch up
225 time.sleep(2)
226 self.refresh()
228 #------------------------------
229 # Information pulling functions
230 #------------------------------
231 def get_friends_list(self):
232 self.statusbar.pop(self.statusbar_context)
233 self.statusbar.push(self.statusbar_context, \
234 "Fetching list of friends")
235 query = ("SELECT uid, name, pic_square FROM user "\
236 "WHERE (uid IN (SELECT uid2 FROM friend WHERE uid1 = %d) "\
237 "OR uid = %d)" % (self._facebook.uid, self._facebook.uid))
238 friends = self._facebook.fql.query([query])
239 return friends
241 def post_get_friends_list(self, widget, results):
242 friends = results
243 for friend in friends:
244 self.friendsname[str(friend['uid'])] = friend['name']
245 self.friendsprofilepic[str(friend['uid'])] = \
246 friend['pic_square']
248 _log.info('%s has altogether %d friends in the database.' \
249 % (self.friendsname[str(self._facebook.uid)],
250 len(self.friendsname.keys())))
251 self.statusbar.pop(self.statusbar_context)
253 self.refresh()
254 return
256 def except_get_friends_list(self, widget, exception):
257 _log.error("Get friends exception: %s" % (str(exception)))
258 self.statusbar.pop(self.statusbar_context)
259 self.statusbar.push(self.statusbar_context, \
260 "Error while fetching friends' list")
262 def get_status_list(self):
264 # Halt point, only one status update may proceed at a time
265 # .release() is called at all 3 possible update finish:
266 # except_get_status_list, _post_get_cl_list, _except_get_cl_list
267 self.update_sema.acquire()
269 self.statusbar.pop(self.statusbar_context)
270 self.statusbar.push(self.statusbar_context, \
271 "Fetching status updates")
272 if self._last_update > 0:
273 since = self._last_update
274 else:
275 now = int(time.time())
276 since = now - 5*24*60*60
277 till = int(time.time())
279 _log.info("Fetching status updates published between %s and %s" \
280 % (time.strftime("%c", time.localtime(since)),
281 time.strftime("%c", time.localtime(till))))
283 query = ("SELECT source_id, created_time, post_id, message " \
284 "FROM stream "\
285 "WHERE ((source_id IN (SELECT uid2 FROM friend WHERE uid1 = %d) "\
286 "OR source_id = %d) "\
287 "AND created_time > %d AND created_time < %d "\
288 "AND attachment = '' AND target_id = '') "\
289 "ORDER BY created_time DESC "\
290 "LIMIT 100"
291 % (self._facebook.uid, self._facebook.uid, since, till))
292 _log.debug('Status list query: %s' % (query))
294 status = self._facebook.fql.query([query])
296 _log.info('Received %d new status' % (len(status)))
297 return [status, till]
299 def post_get_status_list(self, widget, results):
300 _log.debug('Status updates successfully pulled.')
301 updates = results[0]
302 till = results[1]
304 # There are new updates
305 if len(updates) > 0:
306 updates.reverse()
307 for up in updates:
308 self.liststore.prepend((up['post_id'],
309 up['source_id'],
310 up['message'],
311 up['created_time'],
312 '0',
313 '0'))
314 # Scroll to latest status in view
315 model = self.treeview.get_model()
316 first_iter = model.get_iter_first()
317 first_path = model.get_path(first_iter)
318 self.treeview.scroll_to_cell(first_path)
320 self.statusbar.pop(self.statusbar_context)
322 # pull comments and likes too
323 self._threads.add_work(self._post_get_cl_list,
324 self._except_get_cl_list,
325 self._get_cl_list,
326 till)
327 return
329 def except_get_status_list(self, widget, exception):
330 _log.error("Get status list exception: %s" % (str(exception)))
331 self.statusbar.pop(self.statusbar_context)
332 self.statusbar.push(self.statusbar_context, \
333 "Error while fetching status updates")
334 # Finish, give semaphore back in case anyone's waiting
335 self.update_sema.release()
337 ### image download function
338 def _dl_profile_pic(self, uid, url):
339 request = urllib2.Request(url=url)
340 _log.debug('Starting request of %s' % (url))
341 response = urllib2.urlopen(request)
342 data = response.read()
343 _log.debug('Request completed')
345 return (uid, data)
347 ### Results from the picture request
348 def _post_dl_profile_pic(self, widget, data):
349 (uid, data) = data
351 loader = gtk.gdk.PixbufLoader()
352 loader.write(data)
353 loader.close()
355 user_pic = loader.get_pixbuf()
356 self._profilepics[uid] = user_pic
358 self.treeview.queue_draw()
359 return
361 def _exception_dl_profile_pic(self, widget, exception):
362 _log.debug('Exception trying to get a profile picture.')
363 _log.debug(str(exception))
364 return
366 ### get comments and likes
367 def _get_cl_list(self, till):
368 _log.info('Pulling comments & likes for listed status updates')
369 self.statusbar.pop(self.statusbar_context)
370 self.statusbar.push(self.statusbar_context, \
371 "Fetching comments & likes")
373 post_id = []
374 for row in self.liststore:
375 post_id.append('post_id = "%s"' % (row[Columns.STATUSID]))
376 all_id = ' OR '.join(post_id)
378 query = ('SELECT post_id, comments, likes FROM stream WHERE ((%s) ' \
379 'AND updated_time > %d AND updated_time < %d)' % \
380 (all_id, self._last_update, till))
381 _log.debug('Comments & Likes query: %s' % (query))
383 cl_list = self._facebook.fql.query([query])
385 return (cl_list, till)
387 ### Results from the picture request
388 def _post_get_cl_list(self, widget, data):
389 list = data[0]
390 till = data[1]
392 likes_list = {}
393 comments_list = {}
395 for item in list:
396 status_id = item['post_id']
397 likes_list[status_id] = str(item['likes']['count'])
398 comments_list[status_id] = str(item['comments']['count'])
400 for row in self.liststore:
401 rowstatus = row[Columns.STATUSID]
402 # have to check if post really exists, deleted post still
403 # show up in "status" table sometimes, but not in "stream"
404 if rowstatus in likes_list.keys():
405 row[Columns.LIKES] = likes_list[rowstatus]
406 row[Columns.COMMENTS] = comments_list[rowstatus]
407 else:
408 _log.debug("Possible deleted status update: " \
409 "uid: %s, status_id: %s, user: %s, text: %s, time: %s" \
410 % (row[Columns.UID], rowstatus, \
411 self.friendsname[str(row[Columns.UID])], \
412 row[Columns.STATUS], row[Columns.DATETIME]))
414 self._last_update = till
415 _log.info('Finished updating status messages, comments and likes.')
416 self.statusbar.pop(self.statusbar_context)
418 # Last update time in human readable format
419 update_time = time.strftime("%H:%M", time.localtime(till))
420 self.statusbar.push(self.statusbar_context, \
421 "Last update at %s" % (update_time))
423 # Finish, give semaphore back in case anyone's waiting
424 self.update_sema.release()
425 return
427 def _except_get_cl_list(self, widget, exception):
428 _log.error('Exception while getting comments and likes')
429 _log.error(str(exception))
430 self.statusbar.pop(self.statusbar_context)
431 self.statusbar.push(self.statusbar_context, \
432 "Error while fetching comments & likes")
433 # Finish, give semaphore back in case anyone's waiting
434 self.update_sema.release()
435 return
437 #-----------------
438 # Helper functions
439 #-----------------
440 def count(self, text):
441 start = text.get_start_iter()
442 end = text.get_end_iter()
443 thetext = text.get_text(start, end)
444 self.count_label.set_text('(%d)' % (160 - len(thetext)))
445 return True
447 def set_auto_refresh(self):
448 if self._refresh_id:
449 gobject.source_remove(self._refresh_id)
451 self._refresh_id = gobject.timeout_add(
452 self._prefs['auto_refresh_interval']*60*1000,
453 self.refresh)
454 _log.info("Auto-refresh enabled: %d minutes" \
455 % (self._prefs['auto_refresh_interval']))
457 def refresh(self, widget=None):
458 _log.info('Queueing refresh now at %s' % (time.strftime('%H:%M:%S')))
459 self._threads.add_work(self.post_get_status_list,
460 self.except_get_status_list,
461 self.get_status_list)
462 return True
464 def status_format(self, column, cell, store, position):
465 uid = store.get_value(position, Columns.UID)
466 name = self.friendsname[str(uid)]
467 status = store.get_value(position, Columns.STATUS)
468 posttime = store.get_value(position, Columns.DATETIME)
470 #replace characters that would choke the markup
471 status = re.sub(r'&', r'&amp;', status)
472 status = re.sub(r'<', r'&lt;', status)
473 status = re.sub(r'>', r'&gt;', status)
474 markup = ('<b>%s</b> %s\n(%s ago)' % \
475 (name, status, timesince.timesince(posttime)))
476 _log.debug('Marked up text: %s' % (markup))
477 cell.set_property('markup', markup)
478 return
480 def open_url(self, source, url):
481 """Open url as new browser tab."""
482 _log.debug('Opening url: %s' % url)
483 import webbrowser
484 webbrowser.open_new_tab(url)
485 self.window.set_focus(self.entry)
487 def copy_status_to_clipboard(self, source, text):
488 clipboard = gtk.Clipboard()
489 _log.debug('Copying to clipboard: %s' % (text))
490 clipboard.set_text(text)
492 #--------------------
493 # Interface functions
494 #--------------------
495 def quit(self, widget):
496 gtk.main_quit()
498 def systray_click(self, widget, user_param=None):
499 if self.window.get_property('visible'):
500 _log.debug('Hiding window')
501 x, y = self.window.get_position()
502 self._prefs['window_pos_x'] = x
503 self._prefs['window_pos_y'] = y
504 self.window.hide()
505 else:
506 x = self._prefs['window_pos_x']
507 y = self._prefs['window_pos_y']
508 _log.debug('Restoring window at (%d, %d)' % (x, y))
509 self.window.move(x, y)
510 self.window.deiconify()
511 self.window.present()
513 def create_grid(self):
514 self.liststore = gtk.ListStore(gobject.TYPE_STRING,
515 gobject.TYPE_STRING,
516 gobject.TYPE_STRING,
517 gobject.TYPE_INT,
518 gobject.TYPE_STRING,
519 gobject.TYPE_STRING)
521 self.sorter = gtk.TreeModelSort(self.liststore)
522 self.sorter.set_sort_column_id(Columns.DATETIME, gtk.SORT_DESCENDING)
523 self.treeview = gtk.TreeView(self.sorter)
525 self.treeview.set_property('headers-visible', False)
526 self.treeview.set_rules_hint(True)
528 # Column showing profile picture
529 profilepic_renderer = gtk.CellRendererPixbuf()
530 profilepic_column = gtk.TreeViewColumn('Profilepic', \
531 profilepic_renderer)
532 profilepic_column.set_fixed_width(55)
533 profilepic_column.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
534 profilepic_column.set_cell_data_func(profilepic_renderer,
535 self._cell_renderer_profilepic)
536 self.treeview.append_column(profilepic_column)
538 # Column showing status text
539 self.status_renderer = gtk.CellRendererText()
540 # wrapping: pango.WRAP_WORD = 0, don't need to import pango for that
541 self.status_renderer.set_property('wrap-mode', 0)
542 self.status_renderer.set_property('wrap-width', 320)
543 self.status_renderer.set_property('width', 320)
544 self.status_column = gtk.TreeViewColumn('Message', \
545 self.status_renderer, text=1)
546 self.status_column.set_cell_data_func(self.status_renderer, \
547 self.status_format)
548 self.treeview.append_column(self.status_column)
550 # Showing the number of comments
551 comments_renderer = gtk.CellRendererText()
552 comments_column = gtk.TreeViewColumn('Comments', \
553 comments_renderer, text=1)
554 comments_column.set_cell_data_func(comments_renderer, \
555 self._cell_renderer_comments)
556 self.treeview.append_column(comments_column)
558 # Showing the comments icon
559 commentspic_renderer = gtk.CellRendererPixbuf()
560 commentspic_column = gtk.TreeViewColumn('CommentsPic', \
561 commentspic_renderer)
562 commentspic_column.set_cell_data_func(commentspic_renderer, \
563 self._cell_renderer_commentspic)
564 commentspic_column.set_fixed_width(28)
565 commentspic_column.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
566 self.treeview.append_column(commentspic_column)
568 # Showing the number of likes
569 likes_renderer = gtk.CellRendererText()
570 likes_column = gtk.TreeViewColumn('Likes', \
571 likes_renderer, text=1)
572 likes_column.set_cell_data_func(likes_renderer, \
573 self._cell_renderer_likes)
574 self.treeview.append_column(likes_column)
576 # Showing the likes icon
577 likespic_renderer = gtk.CellRendererPixbuf()
578 likespic_column = gtk.TreeViewColumn('Likespic', \
579 likespic_renderer)
580 likespic_column.set_cell_data_func(likespic_renderer, \
581 self._cell_renderer_likespic)
582 likespic_column.set_fixed_width(28)
583 likespic_column.set_sizing(gtk.TREE_VIEW_COLUMN_FIXED)
584 self.treeview.append_column(likespic_column)
586 self.treeview.set_resize_mode(gtk.RESIZE_IMMEDIATE)
588 self.treeview.connect('row-activated', self.open_status_web)
589 self.treeview.connect('button-press-event', self.click_status)
591 def create_menubar(self):
592 refresh_action = gtk.Action('Refresh', '_Refresh',
593 'Get new status updates', gtk.STOCK_REFRESH)
594 refresh_action.connect('activate', self.refresh)
596 quit_action = gtk.Action('Quit', '_Quit',
597 'Exit %s' % (APPNAME), gtk.STOCK_QUIT)
598 quit_action.connect('activate', self.quit)
600 about_action = gtk.Action('About', '_About', 'About %s' % (APPNAME),
601 gtk.STOCK_ABOUT)
602 about_action.connect('activate', self.show_about)
604 self.action_group = gtk.ActionGroup('MainMenu')
605 self.action_group.add_action_with_accel(refresh_action, 'F5')
606 # None = use the default acceletator
607 self.action_group.add_action_with_accel(quit_action, None)
608 self.action_group.add_action(about_action)
610 uimanager = gtk.UIManager()
611 uimanager.insert_action_group(self.action_group, 0)
612 ui = '''
613 <ui>
614 <menubar name="MainMenu">
615 <menuitem action="Quit" />
616 <separator />
617 <menuitem action="Refresh" />
618 <separator />
619 <menuitem action="About" />
620 </menubar>
621 </ui>
623 uimanager.add_ui_from_string(ui)
624 self.main_menu = uimanager.get_widget('/MainMenu')
625 return
627 def show_about(self, widget):
628 """Show the about dialog."""
629 about_window = gtk.AboutDialog()
630 about_window.set_name(APPNAME)
631 about_window.set_version(VERSION)
632 about_window.set_copyright('2009 Gergely Imreh')
633 about_window.set_license(MIT)
634 about_window.set_website('http://imrehg.github.com/minibook/')
635 about_window.set_website_label('%s on GitHub' % (APPNAME))
636 about_window.set_authors(['Gergely Imreh'])
637 about_window.connect('close', self.close_dialog)
638 about_window.run()
639 about_window.hide()
641 def close_dialog(self, user_data=None):
642 """Hide the dialog window."""
643 return True
645 def open_status_web(self, treeview, path, view_column, user_data=None):
646 """ Callback to open status update in web browser when received
647 left click.
649 model = treeview.get_model()
650 if not model:
651 return
653 iter = model.get_iter(path)
654 uid = model.get_value(iter, Columns.UID)
655 status_id = model.get_value(iter, Columns.STATUSID).split("_")[1]
656 status_url = ('http://www.facebook.com/profile.php?' \
657 'id=%s&v=feed&story_fbid=%s' % (uid, status_id))
658 self.open_url(path, status_url)
659 return
661 def click_status(self, treeview, event, user_data=None):
662 """Callback when a mouse click event occurs on one of the rows."""
663 _log.debug('clicked on status list')
664 if event.button != 3:
665 # Only right clicks are processed
666 return False
667 _log.debug('right-click received')
669 x = int(event.x)
670 y = int(event.y)
672 pth = treeview.get_path_at_pos(x, y)
673 if not pth:
674 return False
676 path, col, cell_x, cell_y = pth
677 treeview.grab_focus()
678 treeview.set_cursor(path, col, 0)
680 self.show_status_popup(treeview, event)
681 return True
683 def show_status_popup(self, treeview, event, user_data=None):
684 _log.debug('show popup menu')
685 cursor = treeview.get_cursor()
686 if not cursor:
687 return
688 model = treeview.get_model()
689 if not model:
690 return
692 path = cursor[0]
693 iter = model.get_iter(path)
695 popup_menu = gtk.Menu()
696 popup_menu.set_screen(self.window.get_screen())
698 open_menu_items = []
700 uid = model.get_value(iter, Columns.UID)
701 status_id = model.get_value(iter, Columns.STATUSID).split("_")[1]
702 url = ('http://www.facebook.com/profile.php?' \
703 'id=%s&v=feed&story_fbid=%s' % (uid, status_id))
704 item_name = 'This status'
705 item = gtk.MenuItem(item_name)
706 item.connect('activate', self.open_url, url)
707 open_menu_items.append(item)
709 url = ('http://www.facebook.com/profile.php?' \
710 'id=%s' % (uid))
711 item_name = 'User wall'
712 item = gtk.MenuItem(item_name)
713 item.connect('activate', self.open_url, url)
714 open_menu_items.append(item)
716 url = ('http://www.facebook.com/profile.php?' \
717 'id=%s&v=info' % (uid))
718 item_name = 'User info'
719 item = gtk.MenuItem(item_name)
720 item.connect('activate', self.open_url, url)
721 open_menu_items.append(item)
723 url = ('http://www.facebook.com/profile.php?' \
724 'id=%s&v=photos' % (uid))
725 item_name = 'User photos'
726 item = gtk.MenuItem(item_name)
727 item.connect('activate', self.open_url, url)
728 open_menu_items.append(item)
730 open_menu = gtk.Menu()
731 for item in open_menu_items:
732 open_menu.append(item)
734 # Menu item to open different pages connected to status in browser
735 open_item = gtk.ImageMenuItem('Open in browser')
736 open_item.get_image().set_from_stock(gtk.STOCK_GO_FORWARD, \
737 gtk.ICON_SIZE_MENU)
738 open_item.set_submenu(open_menu)
739 popup_menu.append(open_item)
741 # Menu item to copy status message to clipboard
742 message = model.get_value(iter, Columns.STATUS)
743 name = self.friendsname[str(uid)]
744 text = ("%s %s" % (name, message))
745 copy_item = gtk.ImageMenuItem('Copy status')
746 copy_item.get_image().set_from_stock(gtk.STOCK_COPY, \
747 gtk.ICON_SIZE_MENU)
748 copy_item.connect('activate', self.copy_status_to_clipboard, text)
749 popup_menu.append(copy_item)
751 popup_menu.show_all()
753 if event:
754 b = event.button
755 t = event.time
756 else:
757 b = 1
758 t = 0
760 popup_menu.popup(None, None, None, b, t)
762 return True
764 def _cell_renderer_profilepic(self, column, cell, store, position):
765 uid = str(store.get_value(position, Columns.UID))
766 if not uid in self._profilepics:
767 profilepicurl = self.friendsprofilepic[uid]
768 if profilepicurl:
769 _log.debug('%s does not have profile picture stored, ' \
770 'queuing fetch from %s' % (uid, profilepicurl))
771 self._threads.add_work(self._post_dl_profile_pic,
772 self._exception_dl_profile_pic,
773 self._dl_profile_pic,
774 uid,
775 profilepicurl)
776 else:
777 _log.debug('%s does not have profile picture set, ' % (uid))
779 self._profilepics[uid] = self._default_profilepic
781 cell.set_property('pixbuf', self._profilepics[uid])
783 return
785 def _cell_renderer_comments(self, column, cell, store, position):
786 comments = int(store.get_value(position, Columns.COMMENTS))
787 if comments > 0:
788 cell.set_property('text', str(comments))
789 else:
790 cell.set_property('text', '')
792 def _cell_renderer_commentspic(self, column, cell, store, position):
793 comments = int(store.get_value(position, Columns.COMMENTS))
794 if comments > 0:
795 cell.set_property('pixbuf', self.commentspic)
796 else:
797 cell.set_property('pixbuf', None)
799 def _cell_renderer_likes(self, column, cell, store, position):
800 likes = int(store.get_value(position, Columns.LIKES))
801 if likes > 0:
802 cell.set_property('text', str(likes))
803 else:
804 cell.set_property('text', '')
806 def _cell_renderer_likespic(self, column, cell, store, position):
807 likes = int(store.get_value(position, Columns.LIKES))
808 if likes > 0:
809 cell.set_property('pixbuf', self.likespic)
810 else:
811 cell.set_property('pixbuf', None)
813 #------------------
814 # Main Window start
815 #------------------
816 def __init__(self, facebook):
817 global spelling_support
819 unknown_user = 'pixmaps/unknown_user.png'
820 if unknown_user:
821 self._default_profilepic = gtk.gdk.pixbuf_new_from_file(
822 unknown_user)
823 else:
824 self._default_profilepic = gtk.gdk.Pixbuf(gtk.gdk.COLORSPACE_RGB,
825 has_alpha=False, bits_per_sample=8, width=50, height=50)
827 self.commentspic = gtk.gdk.pixbuf_new_from_file('pixmaps/comments.png')
828 self.likespic = gtk.gdk.pixbuf_new_from_file('pixmaps/likes.png')
830 # create a new window
831 self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
832 self.window.set_size_request(480, 250)
833 self.window.set_title("Minibook")
834 self.window.connect("delete_event", lambda w, e: gtk.main_quit())
836 vbox = gtk.VBox(False, 0)
837 self.window.add(vbox)
839 self.friendsname = {}
840 self.friendsprofilepic = {}
841 self._profilepics = {}
842 # Semaphore to let only one status update proceed at a time
843 self.update_sema = threading.BoundedSemaphore(value=1)
845 self.create_menubar()
846 vbox.pack_start(self.main_menu, False, True, 0)
848 self.create_grid()
849 self.statuslist_window = gtk.ScrolledWindow()
850 self.statuslist_window.set_policy(gtk.POLICY_NEVER, gtk.POLICY_ALWAYS)
851 self.statuslist_window.add(self.treeview)
852 vbox.pack_start(self.statuslist_window, True, True, 0)
854 label_box = gtk.HBox(False, 0)
855 label = gtk.Label("What's on your mind?")
856 self.count_label = gtk.Label("(160)")
857 label_box.pack_start(label)
858 label_box.pack_start(self.count_label)
860 self.entry = gtk.TextView()
861 text = self.entry.get_buffer()
862 text.connect('changed', self.count)
863 text_box = gtk.VBox(True, 0)
864 text_box.pack_start(label_box)
865 text_box.pack_start(self.entry, True, True, 2)
867 update_button = gtk.Button(stock=gtk.STOCK_ADD)
868 update_button.connect("clicked", lambda w: self.sendupdate())
870 update_box = gtk.HBox(False, 0)
871 update_box.pack_start(text_box, expand=True, fill=True,
872 padding=0)
873 update_box.pack_start(update_button, expand=False, fill=False,
874 padding=0)
876 vbox.pack_start(update_box, False, True, 0)
878 self.statusbar = gtk.Statusbar()
879 vbox.pack_start(self.statusbar, False, False, 0)
880 self.statusbar_context = self.statusbar.get_context_id(
881 '%s is here.' % (APPNAME))
883 if spelling_support:
884 try:
885 spelling = gtkspell.Spell(self.entry, 'en')
886 except:
887 spelling_support = False
889 self.window.show_all()
890 self._facebook = facebook
892 self._app_icon = 'pixmaps/minibook.png'
893 self._systray = gtk.StatusIcon()
894 self._systray.set_from_file(self._app_icon)
895 self._systray.set_tooltip('%s\n' \
896 'Left-click: toggle window hiding' % (APPNAME))
897 self._systray.connect('activate', self.systray_click)
898 self._systray.set_visible(True)
900 self.window.set_icon_from_file(self._app_icon)
902 self._threads = _ThreadManager()
904 self.userinfo = self._facebook.users.getInfo([self._facebook.uid], \
905 ['name'])[0]
906 self._last_update = 0
907 self._threads.add_work(self.post_get_friends_list,
908 self.except_get_friends_list,
909 self.get_friends_list)
911 self._prefs = {}
912 x, y = self.window.get_position()
913 self._prefs['window_pos_x'] = x
914 self._prefs['window_pos_y'] = y
915 self._prefs['auto_refresh_interval'] = 5
917 self._refresh_id = None
918 self.set_auto_refresh()
921 def main(facebook):
922 gtk.main()
923 gtk.gdk.threads_leave()
924 _log.debug('Exiting')
925 return 0
927 if __name__ == "__main__":
928 try:
929 config_file = open("config", "r")
930 api_key = config_file.readline()[:-1]
931 secret_key = config_file.readline()[:-1]
932 _log.debug('Config file loaded successfully')
933 except Exception, e:
934 _log.critical('Error while loading config file: %s' % (str(e)))
935 exit(1)
937 facebook = Facebook(api_key, secret_key)
938 try:
939 facebook.auth.createToken()
940 except:
941 # Like catch errors like
942 # http://bugs.developers.facebook.com/show_bug.cgi?id=5474
943 # and http://bugs.developers.facebook.com/show_bug.cgi?id=5472
944 _log.critical("Error on Facebook's side, " \
945 "try starting application later")
946 exit(1)
948 facebook.login()
949 _log.debug('Showing Facebook login page in default browser.')
951 # Delay dialog to allow for login in browser
952 got_session = False
953 while not got_session:
954 dia = gtk.Dialog('minibook: login',
955 None,
956 gtk.DIALOG_MODAL | \
957 gtk.DIALOG_DESTROY_WITH_PARENT | \
958 gtk.DIALOG_NO_SEPARATOR,
959 ("Logged in", gtk.RESPONSE_OK, \
960 gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL))
961 label = gtk.Label("%s is opening your web browser to " \
962 "log in Facebook.\nWhen finished, click 'Logged in', " \
963 "or you can cancel now." % (APPNAME))
964 dia.vbox.pack_start(label, True, True, 10)
965 label.show()
966 dia.show()
967 result = dia.run()
968 if result == gtk.RESPONSE_CANCEL:
969 _log.debug('Exiting before Facebook login.')
970 exit(0)
971 dia.destroy()
972 try:
973 facebook.auth.getSession()
974 got_session = True
975 except:
976 pass
978 _log.info('Session Key: %s' % (facebook.session_key))
979 _log.info('User\'s UID: %d' % (facebook.uid))
981 MainWindow(facebook)
982 main(facebook)