2 """ Minibook: the Facebook(TM) status updater
3 (C) 2009 Gergely Imreh <imrehg@gmail.com>
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
29 MAX_MESSAGE_LENGTH
= 255
36 from facebook
import Facebook
38 print "Pyfacebook is not available, cannot run."
45 gobject
.threads_init()
46 gtk
.gdk
.threads_init()
47 gtk
.gdk
.threads_enter()
51 spelling_support
= True
53 spelling_support
= False
60 LEVELS
= {'debug': logging
.DEBUG
,
62 'warning': logging
.WARNING
,
63 'error': logging
.ERROR
,
64 'critical': logging
.CRITICAL
}
67 level_name
= sys
.argv
[1]
68 level
= LEVELS
.get(level_name
, logging
.CRITICAL
)
69 logging
.basicConfig(level
=level
)
71 logging
.basicConfig(level
=logging
.CRITICAL
)
73 _log
= logging
.getLogger('minibook')
77 (STATUSID
, UID
, STATUS
, DATETIME
, COMMENTS
, LIKES
) = range(6)
80 #-------------------------------------------------
81 # Threading related objects.
82 # Info http://edsiper.linuxchile.cl/blog/?p=152
83 # to mitigate TreeView + threads problems
84 # These classes are based on the code available at http://gist.github.com/51686
85 # (c) 2008, John Stowers <john.stowers@gmail.com>
86 #-------------------------------------------------
88 class _IdleObject(gobject
.GObject
):
90 Override gobject.GObject to always emit signals in the main thread
91 by emmitting on an idle handler
95 gobject
.GObject
.__init
__(self
)
97 def emit(self
, *args
):
98 gobject
.idle_add(gobject
.GObject
.emit
, self
, *args
)
101 #-------------------------------------------------
103 #-------------------------------------------------
105 class _WorkerThread(threading
.Thread
, _IdleObject
):
107 A single working thread.
112 gobject
.SIGNAL_RUN_LAST
,
114 (gobject
.TYPE_PYOBJECT
, )),
116 gobject
.SIGNAL_RUN_LAST
,
118 (gobject
.TYPE_PYOBJECT
, ))}
120 def __init__(self
, function
, *args
, **kwargs
):
121 threading
.Thread
.__init
__(self
)
122 _IdleObject
.__init
__(self
)
123 self
._function
= function
125 self
._kwargs
= kwargs
129 _log
.debug('Thread %s calling %s' % (self
.name
, str(self
._function
)))
132 kwargs
= self
._kwargs
135 result
= self
._function
(*args
, **kwargs
)
136 except Exception, exc
:
137 self
.emit("exception", exc
)
140 _log
.debug('Thread %s completed' % (self
.name
))
142 self
.emit("completed", result
)
146 class _ThreadManager(object):
148 Manager to add new threads and remove finished ones from queue
151 def __init__(self
, max_threads
=4):
153 Start the thread pool. The number of threads in the pool is defined
154 by `pool_size`, defaults to 4
157 self
._max
_threads
= max_threads
158 self
._thread
_pool
= []
164 def _remove_thread(self
, widget
, arg
=None):
166 Called when the thread completes. Remove it from the thread list
167 and start the next thread (if there is any)
170 # not actually a widget. It's the object that emitted the signal, in
171 # this case, the _WorkerThread object.
172 thread_id
= widget
.name
174 _log
.debug('Thread %s completed, %d threads in the queue' % (thread_id
,
175 len(self
._thread
_pool
)))
177 self
._running
.remove(thread_id
)
179 if self
._thread
_pool
:
180 if len(self
._running
) < self
._max
_threads
:
181 next
= self
._thread
_pool
.pop()
182 _log
.debug('Dequeuing thread %s', next
.name
)
183 self
._running
.append(next
.name
)
188 def add_work(self
, complete_cb
, exception_cb
, func
, *args
, **kwargs
):
190 Add a work to the thread list
191 complete_cb: function to call when 'func' finishes
192 exception_cb: function to call when 'func' rises an exception
193 func: function to do the main part of the work
194 *args, **kwargs: arguments for func
197 thread
= _WorkerThread(func
, *args
, **kwargs
)
198 thread_id
= '%s' % (self
._thread
_id
)
200 thread
.connect('completed', complete_cb
)
201 thread
.connect('completed', self
._remove
_thread
)
202 thread
.connect('exception', exception_cb
)
203 thread
.setName(thread_id
)
205 if len(self
._running
) < self
._max
_threads
:
206 self
._running
.append(thread_id
)
209 running_names
= ', '.join(self
._running
)
210 _log
.debug('Threads %s running, adding %s to the queue',
211 running_names
, thread_id
)
212 self
._thread
_pool
.append(thread
)
220 The main application interface, GUI and Facebook interfacing functions
224 #------------------------------
225 # Information sending functions
226 #------------------------------
227 def sendupdate(self
):
229 Sending status update to FB, if the user entered any
232 textfield
= self
.entry
.get_buffer()
233 start
= textfield
.get_start_iter()
234 end
= textfield
.get_end_iter()
235 entry_text
= textfield
.get_text(start
, end
)
237 # Warn user if status message is too long. If insist, send text
238 if len(entry_text
) > MAX_MESSAGE_LENGTH
:
239 warning_message
= ("Your message is longer than %d " \
240 "characters and if submitted it is likely to be " \
241 "truncated by Facebook as:\n\"%s...\"\nInstead of " \
242 "sending this update, do you want to return to editing?" \
243 % (MAX_MESSAGE_LENGTH
, entry_text
[0:251]))
244 warning_dialog
= gtk
.MessageDialog(parent
=self
.window
,
245 type=gtk
.MESSAGE_WARNING
,
246 flags
=gtk
.DIALOG_MODAL | gtk
.DIALOG_DESTROY_WITH_PARENT
,
247 message_format
="Your status update is too long.",
248 buttons
=gtk
.BUTTONS_YES_NO
)
249 warning_dialog
.format_secondary_text(warning_message
)
250 response
= warning_dialog
.run()
251 warning_dialog
.destroy()
252 # If user said yes, don't send just return to editing
253 if response
== gtk
.RESPONSE_YES
:
256 _log
.info('Sending status update: %s\n' % entry_text
)
257 self
.statusbar
.pop(self
.statusbar_context
)
258 self
.statusbar
.push(self
.statusbar_context
, \
259 "Sending your status update")
260 self
._facebook
.status
.set([entry_text
], [self
._facebook
.uid
])
262 # Empty entry field and status bar
263 textfield
.set_text("")
264 self
.statusbar
.pop(self
.statusbar_context
)
266 # wait a little before getting new updates, so FB can catch up
270 #------------------------------
271 # Information pulling functions
272 #------------------------------
273 def get_friends_list(self
):
275 Fetching list of friends' names (and the current user's) to
276 store and use when status updates are displayed
277 Threading callbacks: post_get_friends_list, except_get_friends_list
280 self
.statusbar
.pop(self
.statusbar_context
)
281 self
.statusbar
.push(self
.statusbar_context
, \
282 "Fetching list of friends")
283 query
= ("SELECT uid, name, pic_square FROM user "\
284 "WHERE (uid IN (SELECT uid2 FROM friend WHERE uid1 = %d) "\
285 "OR uid = %d)" % (self
._facebook
.uid
, self
._facebook
.uid
))
286 _log
.debug("Friend list query: %s" % (query
))
287 friends
= self
._facebook
.fql
.query([query
])
290 def post_get_friends_list(self
, widget
, results
):
292 Callback function when friends list is successfully pulled
293 Makes dictionary of uid->friendsname, and pulls new statuses
297 for friend
in friends
:
298 # In "friend" table UID is integer
299 self
.friendsname
[str(friend
['uid'])] = friend
['name']
300 self
.friendsprofilepic
[str(friend
['uid'])] = \
303 # Not all friends can be pulled, depends on their privacy settings
304 _log
.info('%s has altogether %d friends in the database.' \
305 % (self
.friendsname
[str(self
._facebook
.uid
)],
306 len(self
.friendsname
.keys())))
307 self
.statusbar
.pop(self
.statusbar_context
)
312 def except_get_friends_list(self
, widget
, exception
):
314 Callback if there's an exception raised while fetching friends' names
317 _log
.error("Get friends exception: %s" % (str(exception
)))
318 self
.statusbar
.pop(self
.statusbar_context
)
319 self
.statusbar
.push(self
.statusbar_context
, \
320 "Error while fetching friends' list")
322 def get_status_list(self
):
324 Fetching new statuses using FQL query for user's friends (and
325 their own) between last fetch and now
326 Threading callbacks: post_get_status_list, except_get_status_list
329 # Halt point, only one status update may proceed at a time
330 # .release() is called at all 3 possible update finish:
331 # except_get_status_list, _post_get_cl_list, _except_get_cl_list
332 self
.update_sema
.acquire()
334 self
.statusbar
.pop(self
.statusbar_context
)
335 self
.statusbar
.push(self
.statusbar_context
, \
336 "Fetching status updates")
338 # If not first update then get new statuses since then
339 # otherwise get them since 5 days ago (i.e. long time ago)
340 if self
._last
_update
> 0:
341 since
= self
._last
_update
343 now
= int(time
.time())
344 since
= now
- 5*24*60*60
345 till
= int(time
.time())
347 _log
.info("Fetching status updates published between %s and %s" \
348 % (time
.strftime("%c", time
.localtime(since
)),
349 time
.strftime("%c", time
.localtime(till
))))
351 # User "stream" table to get status updates because the older "status"
352 # has too many bugs and limitations
353 # Status update is a post that has no attachment nor target
354 query
= ("SELECT source_id, created_time, post_id, message " \
356 "WHERE ((source_id IN (SELECT uid2 FROM friend WHERE uid1 = %d) "\
357 "OR source_id = %d) "\
358 "AND created_time > %d AND created_time < %d "\
359 "AND attachment = '' AND target_id = '') "\
360 "ORDER BY created_time DESC "\
362 % (self
._facebook
.uid
, self
._facebook
.uid
, since
, till
))
363 _log
.debug('Status list query: %s' % (query
))
365 status
= self
._facebook
.fql
.query([query
])
367 _log
.info('Received %d new status' % (len(status
)))
368 return [status
, till
]
370 def post_get_status_list(self
, widget
, results
):
372 Callback function when new status updates are successfully pulled
373 Adds statuses to listview and initiates pulling comments & likes
374 restults: [status_updates_array, till_time]
377 _log
.debug('Status updates successfully pulled.')
381 # There are new updates
385 # source_id is the UID, and in "stream" it is string, not int
386 self
.liststore
.prepend((up
['post_id'],
392 # Scroll to newest status in view
393 model
= self
.treeview
.get_model()
394 first_iter
= model
.get_iter_first()
395 first_path
= model
.get_path(first_iter
)
396 self
.treeview
.scroll_to_cell(first_path
)
398 self
.statusbar
.pop(self
.statusbar_context
)
400 # pull comments and likes too
401 self
._threads
.add_work(self
._post
_get
_cl
_list
,
402 self
._except
_get
_cl
_list
,
407 def except_get_status_list(self
, widget
, exception
):
409 Callback if there's an exception raised while fetching status list
412 _log
.error("Get status list exception: %s" % (str(exception
)))
413 self
.statusbar
.pop(self
.statusbar_context
)
414 self
.statusbar
.push(self
.statusbar_context
, \
415 "Error while fetching status updates")
416 # Finish, give semaphore back in case anyone's waiting
417 self
.update_sema
.release()
419 ### image download function
420 def _dl_profile_pic(self
, uid
, url
):
422 Download user profile pictures
423 Threading callbacks: _post_dl_profile_pic, _exception_dl_profile_pic
427 request
= urllib2
.Request(url
=url
)
428 _log
.debug('Starting request of %s' % (url
))
429 response
= urllib2
.urlopen(request
)
430 data
= response
.read()
431 _log
.debug('Request completed')
435 ### Results from the picture request
436 def _post_dl_profile_pic(self
, widget
, data
):
438 Callback when profile picture is successfully downloaded
439 Replaces default picture with the users profile pic in status list
444 loader
= gtk
.gdk
.PixbufLoader()
448 user_pic
= loader
.get_pixbuf()
449 # Replace default picture
450 self
._profilepics
[uid
] = user_pic
452 # Redraw to get new picture
453 self
.treeview
.queue_draw()
456 def _exception_dl_profile_pic(self
, widget
, exception
):
458 Callback when there's an excetion during downloading profile picture
461 _log
.debug('Exception trying to get a profile picture.')
462 _log
.debug(str(exception
))
465 ### get comments and likes
466 def _get_cl_list(self
, till
):
468 Fetch comments & likes for the listed statuses
469 Threading callbacks: _post_get_cl_list, _except_get_cl_list
470 till: time between self.last_update and till
473 _log
.info('Pulling comments & likes for listed status updates')
474 self
.statusbar
.pop(self
.statusbar_context
)
475 self
.statusbar
.push(self
.statusbar_context
, \
476 "Fetching comments & likes")
478 # Preparing list of status update post_id for FQL query
480 for row
in self
.liststore
:
481 post_id
.append('post_id = "%s"' % (row
[Columns
.STATUSID
]))
482 all_id
= ' OR '.join(post_id
)
484 query
= ('SELECT post_id, comments, likes FROM stream WHERE ((%s) ' \
485 'AND updated_time > %d AND updated_time < %d)' % \
486 (all_id
, self
._last
_update
, till
))
487 _log
.debug('Comments & Likes query: %s' % (query
))
489 cl_list
= self
._facebook
.fql
.query([query
])
491 return (cl_list
, till
)
493 ### Results from the picture request
494 def _post_get_cl_list(self
, widget
, data
):
496 Callback when successfully fetched new comments and likes
497 Ends up here if complete 'refresh' is successfull
507 status_id
= item
['post_id']
508 likes_list
[status_id
] = str(item
['likes']['count'])
509 comments_list
[status_id
] = str(item
['comments']['count'])
511 for row
in self
.liststore
:
512 rowstatus
= row
[Columns
.STATUSID
]
513 # have to check if post really exists, deleted post still
514 # show up in "status" table sometimes, not sure in "stream"
515 if rowstatus
in likes_list
.keys():
516 row
[Columns
.LIKES
] = likes_list
[rowstatus
]
517 row
[Columns
.COMMENTS
] = comments_list
[rowstatus
]
519 _log
.debug("Possible deleted status update: " \
520 "uid: %s, status_id: %s, user: %s, text: %s, time: %s" \
521 % (row
[Columns
.UID
], rowstatus
, \
522 self
.friendsname
[str(row
[Columns
.UID
])], \
523 row
[Columns
.STATUS
], row
[Columns
.DATETIME
]))
525 # Update time of last update since this finished just fine
526 self
._last
_update
= till
527 _log
.info('Finished updating status messages, comments and likes.')
528 self
.statusbar
.pop(self
.statusbar_context
)
530 # Last update time in human readable format
531 update_time
= time
.strftime("%H:%M", time
.localtime(till
))
532 self
.statusbar
.push(self
.statusbar_context
, \
533 "Last update at %s" % (update_time
))
535 # Finish, give semaphore back in case anyone's waiting
536 self
.update_sema
.release()
539 def _except_get_cl_list(self
, widget
, exception
):
541 Callback if there' an exception during comments and likes fetch
544 _log
.error('Exception while getting comments and likes')
545 _log
.error(str(exception
))
546 self
.statusbar
.pop(self
.statusbar_context
)
547 self
.statusbar
.push(self
.statusbar_context
, \
548 "Error while fetching comments & likes")
549 # Finish, give semaphore back in case anyone's waiting
550 self
.update_sema
.release()
556 def count(self
, text
):
558 Count remaining characters in status update text
561 start
= text
.get_start_iter()
562 end
= text
.get_end_iter()
563 thetext
= text
.get_text(start
, end
)
564 self
.count_label
.set_text('(%d)' \
565 % (MAX_MESSAGE_LENGTH
- len(thetext
)))
568 def set_auto_refresh(self
):
570 Enable auto refresh statuses in pre-defined intervals
574 gobject
.source_remove(self
._refresh
_id
)
576 self
._refresh
_id
= gobject
.timeout_add(
577 self
._prefs
['auto_refresh_interval']*60*1000,
579 _log
.info("Auto-refresh enabled: %d minutes" \
580 % (self
._prefs
['auto_refresh_interval']))
582 def refresh(self
, widget
=None):
584 Queueing refresh in thread pool, subject to semaphores
587 _log
.info('Queueing refresh now at %s' % (time
.strftime('%H:%M:%S')))
588 self
._threads
.add_work(self
.post_get_status_list
,
589 self
.except_get_status_list
,
590 self
.get_status_list
)
593 def status_format(self
, column
, cell
, store
, position
):
595 Format how status update should look in list
598 uid
= store
.get_value(position
, Columns
.UID
)
599 name
= self
.friendsname
[uid
]
600 status
= store
.get_value(position
, Columns
.STATUS
)
601 posttime
= store
.get_value(position
, Columns
.DATETIME
)
603 #replace characters that would choke the markup
604 status
= re
.sub(r
'&', r
'&', status
)
605 status
= re
.sub(r
'<', r
'<', status
)
606 status
= re
.sub(r
'>', r
'>', status
)
607 markup
= ('<b>%s</b> %s\n(%s ago)' % \
608 (name
, status
, timesince
.timesince(posttime
)))
609 _log
.debug('Marked up text: %s' % (markup
))
610 cell
.set_property('markup', markup
)
613 def open_url(self
, source
, url
):
615 Open url as new browser tab
618 _log
.debug('Opening url: %s' % url
)
620 webbrowser
.open_new_tab(url
)
621 self
.window
.set_focus(self
.entry
)
623 def copy_status_to_clipboard(self
, source
, text
):
625 Copy current status (together with poster name but without time)
629 clipboard
= gtk
.Clipboard()
630 _log
.debug('Copying to clipboard: %s' % (text
))
631 clipboard
.set_text(text
)
633 #--------------------
634 # Interface functions
635 #--------------------
636 def quit(self
, widget
):
642 def systray_click(self
, widget
, user_param
=None):
644 Callback when systray icon receives left-click
647 # Toggle visibility of main window
648 if self
.window
.get_property('visible'):
649 _log
.debug('Hiding window')
650 x
, y
= self
.window
.get_position()
651 self
._prefs
['window_pos_x'] = x
652 self
._prefs
['window_pos_y'] = y
655 x
= self
._prefs
['window_pos_x']
656 y
= self
._prefs
['window_pos_y']
657 _log
.debug('Restoring window at (%d, %d)' % (x
, y
))
658 self
.window
.move(x
, y
)
659 self
.window
.deiconify()
660 self
.window
.present()
662 def create_grid(self
):
664 Create list where each line consist of:
665 profile pic, status update, comments and likes count
668 # List for storing all relevant info
669 self
.liststore
= gtk
.ListStore(gobject
.TYPE_STRING
,
676 # Short items by time, newest first
677 self
.sorter
= gtk
.TreeModelSort(self
.liststore
)
678 self
.sorter
.set_sort_column_id(Columns
.DATETIME
, gtk
.SORT_DESCENDING
)
679 self
.treeview
= gtk
.TreeView(self
.sorter
)
682 self
.treeview
.set_property('headers-visible', False)
683 # Alternating background colours for lines
684 self
.treeview
.set_rules_hint(True)
686 # Column showing profile picture
687 profilepic_renderer
= gtk
.CellRendererPixbuf()
688 profilepic_column
= gtk
.TreeViewColumn('Profilepic', \
690 profilepic_column
.set_fixed_width(55)
691 profilepic_column
.set_sizing(gtk
.TREE_VIEW_COLUMN_FIXED
)
692 profilepic_column
.set_cell_data_func(profilepic_renderer
,
693 self
._cell
_renderer
_profilepic
)
694 self
.treeview
.append_column(profilepic_column
)
696 # Column showing status text
697 self
.status_renderer
= gtk
.CellRendererText()
698 # wrapping: pango.WRAP_WORD == 0, don't need to import pango for that
699 self
.status_renderer
.set_property('wrap-mode', 0)
700 self
.status_renderer
.set_property('wrap-width', 320)
701 self
.status_renderer
.set_property('width', 320)
702 self
.status_column
= gtk
.TreeViewColumn('Message', \
703 self
.status_renderer
, text
=1)
704 self
.status_column
.set_cell_data_func(self
.status_renderer
, \
706 self
.treeview
.append_column(self
.status_column
)
708 # Showing the number of comments
709 comments_renderer
= gtk
.CellRendererText()
710 comments_column
= gtk
.TreeViewColumn('Comments', \
711 comments_renderer
, text
=1)
712 comments_column
.set_cell_data_func(comments_renderer
, \
713 self
._cell
_renderer
_comments
)
714 self
.treeview
.append_column(comments_column
)
716 # Showing the comments icon
717 commentspic_renderer
= gtk
.CellRendererPixbuf()
718 commentspic_column
= gtk
.TreeViewColumn('CommentsPic', \
719 commentspic_renderer
)
720 commentspic_column
.set_cell_data_func(commentspic_renderer
, \
721 self
._cell
_renderer
_commentspic
)
722 commentspic_column
.set_fixed_width(28)
723 commentspic_column
.set_sizing(gtk
.TREE_VIEW_COLUMN_FIXED
)
724 self
.treeview
.append_column(commentspic_column
)
726 # Showing the number of likes
727 likes_renderer
= gtk
.CellRendererText()
728 likes_column
= gtk
.TreeViewColumn('Likes', \
729 likes_renderer
, text
=1)
730 likes_column
.set_cell_data_func(likes_renderer
, \
731 self
._cell
_renderer
_likes
)
732 self
.treeview
.append_column(likes_column
)
734 # Showing the likes icon
735 likespic_renderer
= gtk
.CellRendererPixbuf()
736 likespic_column
= gtk
.TreeViewColumn('Likespic', \
738 likespic_column
.set_cell_data_func(likespic_renderer
, \
739 self
._cell
_renderer
_likespic
)
740 likespic_column
.set_fixed_width(28)
741 likespic_column
.set_sizing(gtk
.TREE_VIEW_COLUMN_FIXED
)
742 self
.treeview
.append_column(likespic_column
)
744 self
.treeview
.set_resize_mode(gtk
.RESIZE_IMMEDIATE
)
746 self
.treeview
.connect('row-activated', self
.open_status_web
)
747 self
.treeview
.connect('button-press-event', self
.click_status
)
749 def create_menubar(self
):
751 Showing the app's (very basic) menubar
754 refresh_action
= gtk
.Action('Refresh', '_Refresh',
755 'Get new status updates', gtk
.STOCK_REFRESH
)
756 refresh_action
.connect('activate', self
.refresh
)
758 quit_action
= gtk
.Action('Quit', '_Quit',
759 'Exit %s' % (APPNAME
), gtk
.STOCK_QUIT
)
760 quit_action
.connect('activate', self
.quit
)
762 about_action
= gtk
.Action('About', '_About', 'About %s' % (APPNAME
),
764 about_action
.connect('activate', self
.show_about
)
766 self
.action_group
= gtk
.ActionGroup('MainMenu')
767 self
.action_group
.add_action_with_accel(refresh_action
, 'F5')
768 # accel = None to use the default acceletator
769 self
.action_group
.add_action_with_accel(quit_action
, None)
770 self
.action_group
.add_action(about_action
)
772 uimanager
= gtk
.UIManager()
773 uimanager
.insert_action_group(self
.action_group
, 0)
776 <menubar name="MainMenu">
777 <menuitem action="Quit" />
779 <menuitem action="Refresh" />
781 <menuitem action="About" />
785 uimanager
.add_ui_from_string(ui
)
786 self
.main_menu
= uimanager
.get_widget('/MainMenu')
789 def show_about(self
, widget
):
791 Show the about dialog
794 about_window
= gtk
.AboutDialog()
795 about_window
.set_program_name(APPNAME
)
796 about_window
.set_version(VERSION
)
797 about_window
.set_copyright('2009 Gergely Imreh <imrehg@gmail.com>')
798 about_window
.set_license(MIT
)
799 about_window
.set_website('http://imrehg.github.com/minibook/')
800 about_window
.set_website_label('%s on GitHub' % (APPNAME
))
801 about_window
.connect('close', self
.close_dialog
)
805 def close_dialog(self
, user_data
=None):
807 Hide the dialog window
812 def open_status_web(self
, treeview
, path
, view_column
, user_data
=None):
814 Callback to open status update in web browser when received left click
817 model
= treeview
.get_model()
821 iter = model
.get_iter(path
)
822 uid
= model
.get_value(iter, Columns
.UID
)
823 status_id
= model
.get_value(iter, Columns
.STATUSID
).split("_")[1]
824 status_url
= ('http://www.facebook.com/profile.php?' \
825 'id=%s&v=feed&story_fbid=%s' % (uid
, status_id
))
826 self
.open_url(path
, status_url
)
829 def click_status(self
, treeview
, event
, user_data
=None):
831 Callback when a mouse click event occurs on one of the rows
834 _log
.debug('Clicked on status list')
835 if event
.button
!= 3:
836 # Only right clicks are processed
838 _log
.debug('right-click received')
843 pth
= treeview
.get_path_at_pos(x
, y
)
847 path
, col
, cell_x
, cell_y
= pth
848 treeview
.grab_focus()
849 treeview
.set_cursor(path
, col
, 0)
851 self
.show_status_popup(treeview
, event
)
854 def show_status_popup(self
, treeview
, event
, user_data
=None):
856 Show popup menu relevant to the clicked status update
859 _log
.debug('Show popup menu')
860 cursor
= treeview
.get_cursor()
863 model
= treeview
.get_model()
868 iter = model
.get_iter(path
)
870 popup_menu
= gtk
.Menu()
871 popup_menu
.set_screen(self
.window
.get_screen())
875 # Open this status update in browser
876 uid
= model
.get_value(iter, Columns
.UID
)
877 status_id
= model
.get_value(iter, Columns
.STATUSID
).split("_")[1]
878 url
= ('http://www.facebook.com/profile.php?' \
879 'id=%s&v=feed&story_fbid=%s' % (uid
, status_id
))
880 item_name
= 'This status'
881 item
= gtk
.MenuItem(item_name
)
882 item
.connect('activate', self
.open_url
, url
)
883 open_menu_items
.append(item
)
885 # Open user's wall in browser
886 url
= ('http://www.facebook.com/profile.php?' \
888 item_name
= 'User wall'
889 item
= gtk
.MenuItem(item_name
)
890 item
.connect('activate', self
.open_url
, url
)
891 open_menu_items
.append(item
)
893 # Open user's info in browser
894 url
= ('http://www.facebook.com/profile.php?' \
895 'id=%s&v=info' % (uid
))
896 item_name
= 'User info'
897 item
= gtk
.MenuItem(item_name
)
898 item
.connect('activate', self
.open_url
, url
)
899 open_menu_items
.append(item
)
901 # Open user's photos in browser
902 url
= ('http://www.facebook.com/profile.php?' \
903 'id=%s&v=photos' % (uid
))
904 item_name
= 'User photos'
905 item
= gtk
.MenuItem(item_name
)
906 item
.connect('activate', self
.open_url
, url
)
907 open_menu_items
.append(item
)
909 open_menu
= gtk
.Menu()
910 for item
in open_menu_items
:
911 open_menu
.append(item
)
913 # Menu item for "open in browser" menu
914 open_item
= gtk
.ImageMenuItem('Open in browser')
915 open_item
.get_image().set_from_stock(gtk
.STOCK_GO_FORWARD
, \
917 open_item
.set_submenu(open_menu
)
918 popup_menu
.append(open_item
)
920 # Menu item to copy status message to clipboard
921 message
= model
.get_value(iter, Columns
.STATUS
)
922 name
= self
.friendsname
[str(uid
)]
923 text
= ("%s %s" % (name
, message
))
924 copy_item
= gtk
.ImageMenuItem('Copy status')
925 copy_item
.get_image().set_from_stock(gtk
.STOCK_COPY
, \
927 copy_item
.connect('activate', self
.copy_status_to_clipboard
, text
)
928 popup_menu
.append(copy_item
)
930 popup_menu
.show_all()
939 popup_menu
.popup(None, None, None, b
, t
)
943 def _cell_renderer_profilepic(self
, column
, cell
, store
, position
):
945 Showing profile picture in status update list
946 Use default picture if we don't (can't) have the user's
947 If first time trying to display it try to download and display default
950 uid
= str(store
.get_value(position
, Columns
.UID
))
951 if not uid
in self
._profilepics
:
952 profilepicurl
= self
.friendsprofilepic
[uid
]
954 _log
.debug('%s does not have profile picture stored, ' \
955 'queuing fetch from %s' % (uid
, profilepicurl
))
956 self
._threads
.add_work(self
._post
_dl
_profile
_pic
,
957 self
._exception
_dl
_profile
_pic
,
958 self
._dl
_profile
_pic
,
962 _log
.debug('%s does not have profile picture set, ' % (uid
))
964 self
._profilepics
[uid
] = self
._default
_profilepic
966 cell
.set_property('pixbuf', self
._profilepics
[uid
])
970 def _cell_renderer_comments(self
, column
, cell
, store
, position
):
972 Cell renderer for the number of comments
975 comments
= int(store
.get_value(position
, Columns
.COMMENTS
))
977 cell
.set_property('text', str(comments
))
979 cell
.set_property('text', '')
981 def _cell_renderer_commentspic(self
, column
, cell
, store
, position
):
983 Cell renderer for comments picture if there are any comments
986 comments
= int(store
.get_value(position
, Columns
.COMMENTS
))
988 cell
.set_property('pixbuf', self
.commentspic
)
990 cell
.set_property('pixbuf', None)
992 def _cell_renderer_likes(self
, column
, cell
, store
, position
):
994 Cell renderer for number of likes
997 likes
= int(store
.get_value(position
, Columns
.LIKES
))
999 cell
.set_property('text', str(likes
))
1001 cell
.set_property('text', '')
1003 def _cell_renderer_likespic(self
, column
, cell
, store
, position
):
1005 Cell renderer for likess picture if there are any likes
1008 likes
= int(store
.get_value(position
, Columns
.LIKES
))
1010 cell
.set_property('pixbuf', self
.likespic
)
1012 cell
.set_property('pixbuf', None)
1017 def __init__(self
, facebook
):
1019 Creating main window and setting up relevant variables
1022 global spelling_support
1024 # Connect to facebook object
1025 self
._facebook
= facebook
1027 # Picture shown if cannot get a user's own profile picture
1028 unknown_user
= 'pixmaps/unknown_user.png'
1030 self
._default
_profilepic
= gtk
.gdk
.pixbuf_new_from_file(
1033 self
._default
_profilepic
= gtk
.gdk
.Pixbuf(gtk
.gdk
.COLORSPACE_RGB
,
1034 has_alpha
=False, bits_per_sample
=8, width
=50, height
=50)
1036 # Icons for "comments" and "likes"
1037 self
.commentspic
= gtk
.gdk
.pixbuf_new_from_file('pixmaps/comments.png')
1038 self
.likespic
= gtk
.gdk
.pixbuf_new_from_file('pixmaps/likes.png')
1040 self
.friendsname
= {}
1041 self
.friendsprofilepic
= {}
1042 self
._profilepics
= {}
1043 # Semaphore to let only one status update proceed at a time
1044 self
.update_sema
= threading
.BoundedSemaphore(value
=1)
1046 # create a new window
1047 self
.window
= gtk
.Window(gtk
.WINDOW_TOPLEVEL
)
1048 self
.window
.set_size_request(480, 250)
1049 self
.window
.set_title("Minibook")
1050 self
.window
.connect("delete_event", lambda w
, e
: gtk
.main_quit())
1052 vbox
= gtk
.VBox(False, 0)
1053 self
.window
.add(vbox
)
1056 self
.create_menubar()
1057 vbox
.pack_start(self
.main_menu
, False, True, 0)
1059 # Status update display window
1061 self
.statuslist_window
= gtk
.ScrolledWindow()
1062 self
.statuslist_window
.set_policy(gtk
.POLICY_NEVER
, gtk
.POLICY_ALWAYS
)
1063 self
.statuslist_window
.add(self
.treeview
)
1064 vbox
.pack_start(self
.statuslist_window
, True, True, 0)
1066 # Area around the status update entry box with labels and button
1067 label_box
= gtk
.HBox(False, 0)
1068 label
= gtk
.Label("What's on your mind?")
1069 self
.count_label
= gtk
.Label("(%d)" % (MAX_MESSAGE_LENGTH
))
1070 label_box
.pack_start(label
)
1071 label_box
.pack_start(self
.count_label
)
1073 self
.entry
= gtk
.TextView()
1074 text
= self
.entry
.get_buffer()
1075 text
.connect('changed', self
.count
)
1076 text_box
= gtk
.VBox(True, 0)
1077 text_box
.pack_start(label_box
)
1078 text_box
.pack_start(self
.entry
, True, True, 4)
1080 update_button
= gtk
.Button(stock
=gtk
.STOCK_ADD
)
1081 update_button
.connect("clicked", lambda w
: self
.sendupdate())
1083 update_box
= gtk
.HBox(False, 0)
1084 update_box
.pack_start(text_box
, expand
=True, fill
=True,
1086 update_box
.pack_start(update_button
, expand
=False, fill
=False,
1089 vbox
.pack_start(update_box
, False, True, 0)
1092 self
.statusbar
= gtk
.Statusbar()
1093 vbox
.pack_start(self
.statusbar
, False, False, 0)
1094 self
.statusbar_context
= self
.statusbar
.get_context_id(
1095 '%s is here.' % (APPNAME
))
1097 # Set up spell checking if it is available
1098 if spelling_support
:
1100 spelling
= gtkspell
.Spell(self
.entry
, 'en')
1102 spelling_support
= False
1105 self
.window
.show_all()
1107 # Set up systray icon
1108 self
._app
_icon
= 'pixmaps/minibook.png'
1109 self
._systray
= gtk
.StatusIcon()
1110 self
._systray
.set_from_file(self
._app
_icon
)
1111 self
._systray
.set_tooltip('%s\n' \
1112 'Left-click: toggle window hiding' % (APPNAME
))
1113 self
._systray
.connect('activate', self
.systray_click
)
1114 self
._systray
.set_visible(True)
1116 self
.window
.set_icon_from_file(self
._app
_icon
)
1118 # Enable thread manager
1119 self
._threads
= _ThreadManager()
1121 # Start to set up preferences
1123 x
, y
= self
.window
.get_position()
1124 self
._prefs
['window_pos_x'] = x
1125 self
._prefs
['window_pos_y'] = y
1126 self
._prefs
['auto_refresh_interval'] = 5
1128 # Last update: never, start first one
1129 self
._last
_update
= 0
1130 self
._threads
.add_work(self
.post_get_friends_list
,
1131 self
.except_get_friends_list
,
1132 self
.get_friends_list
)
1134 # Enable auto-refresh
1135 self
._refresh
_id
= None
1136 self
.set_auto_refresh()
1145 gtk
.gdk
.threads_leave()
1146 _log
.debug('Exiting')
1149 if __name__
== "__main__":
1151 Set up facebook object, login and start main window
1154 # Currently cannot include the registered app's
1155 # api_key and secret_key, thus have to save them separately
1156 # Here those keys are loaded
1158 config_file
= open("config", "r")
1159 api_key
= config_file
.readline()[:-1]
1160 secret_key
= config_file
.readline()[:-1]
1161 _log
.debug('Config file loaded successfully')
1162 except Exception, e
:
1163 _log
.critical('Error while loading config file: %s' % (str(e
)))
1166 facebook
= Facebook(api_key
, secret_key
)
1168 facebook
.auth
.createToken()
1170 # Like catch errors like
1171 # http://bugs.developers.facebook.com/show_bug.cgi?id=5474
1172 # and http://bugs.developers.facebook.com/show_bug.cgi?id=5472
1173 _log
.critical("Error on Facebook's side, " \
1174 "try starting application later")
1177 _log
.debug('Showing Facebook login page in default browser.')
1180 # Delay dialog to allow for login in browser
1182 while not got_session
:
1183 dia
= gtk
.Dialog('minibook: login',
1185 gtk
.DIALOG_MODAL | \
1186 gtk
.DIALOG_DESTROY_WITH_PARENT | \
1187 gtk
.DIALOG_NO_SEPARATOR
,
1188 ("Logged in", gtk
.RESPONSE_OK
, \
1189 gtk
.STOCK_CANCEL
, gtk
.RESPONSE_CANCEL
))
1190 label
= gtk
.Label("%s is opening your web browser to " \
1191 "log in Facebook.\nWhen finished, click 'Logged in', " \
1192 "or you can cancel now." % (APPNAME
))
1193 dia
.vbox
.pack_start(label
, True, True, 10)
1197 # Cancel login and close app
1198 if result
== gtk
.RESPONSE_CANCEL
:
1199 _log
.debug('Exiting before Facebook login.')
1203 facebook
.auth
.getSession()
1206 # Likely clicked "logged in" but not logged in yet, start over
1209 _log
.info('Session Key: %s' % (facebook
.session_key
))
1210 _log
.info('User\'s UID: %d' % (facebook
.uid
))
1212 # Start main window and app
1213 MainWindow(facebook
)