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
35 from facebook
import Facebook
37 print "Pyfacebook is not available, cannot run."
44 gobject
.threads_init()
45 gtk
.gdk
.threads_init()
46 gtk
.gdk
.threads_enter()
50 spelling_support
= True
52 spelling_support
= False
59 LEVELS
= {'debug': logging
.DEBUG
,
61 'warning': logging
.WARNING
,
62 'error': logging
.ERROR
,
63 'critical': logging
.CRITICAL
}
66 level_name
= sys
.argv
[1]
67 level
= LEVELS
.get(level_name
, logging
.CRITICAL
)
68 logging
.basicConfig(level
=level
)
70 logging
.basicConfig(level
=logging
.CRITICAL
)
72 _log
= logging
.getLogger('minibook')
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
):
89 Override gobject.GObject to always emit signals in the main thread
90 by emmitting on an idle handler
94 gobject
.GObject
.__init
__(self
)
96 def emit(self
, *args
):
97 gobject
.idle_add(gobject
.GObject
.emit
, self
, *args
)
100 #-------------------------------------------------
102 #-------------------------------------------------
104 class _WorkerThread(threading
.Thread
, _IdleObject
):
105 """A single working thread."""
109 gobject
.SIGNAL_RUN_LAST
,
111 (gobject
.TYPE_PYOBJECT
, )),
113 gobject
.SIGNAL_RUN_LAST
,
115 (gobject
.TYPE_PYOBJECT
, ))}
117 def __init__(self
, function
, *args
, **kwargs
):
118 threading
.Thread
.__init
__(self
)
119 _IdleObject
.__init
__(self
)
120 self
._function
= function
122 self
._kwargs
= kwargs
126 _log
.debug('Thread %s calling %s' % (self
.name
, str(self
._function
)))
129 kwargs
= self
._kwargs
132 result
= self
._function
(*args
, **kwargs
)
133 except Exception, exc
:
134 self
.emit("exception", exc
)
137 _log
.debug('Thread %s completed' % (self
.name
))
139 self
.emit("completed", result
)
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
= []
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
)
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
)
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
)
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
)
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
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
])
241 def post_get_friends_list(self
, widget
, results
):
243 for friend
in friends
:
244 self
.friendsname
[str(friend
['uid'])] = friend
['name']
245 self
.friendsprofilepic
[str(friend
['uid'])] = \
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
)
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
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 " \
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 "\
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.')
304 # There are new updates
308 self
.liststore
.prepend((up
['post_id'],
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
,
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')
347 ### Results from the picture request
348 def _post_dl_profile_pic(self
, widget
, data
):
351 loader
= gtk
.gdk
.PixbufLoader()
355 user_pic
= loader
.get_pixbuf()
356 self
._profilepics
[uid
] = user_pic
358 self
.treeview
.queue_draw()
361 def _exception_dl_profile_pic(self
, widget
, exception
):
362 _log
.debug('Exception trying to get a profile picture.')
363 _log
.debug(str(exception
))
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")
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
):
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
]
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()
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()
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
)))
447 def set_auto_refresh(self
):
449 gobject
.source_remove(self
._refresh
_id
)
451 self
._refresh
_id
= gobject
.timeout_add(
452 self
._prefs
['auto_refresh_interval']*60*1000,
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
)
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
'&', status
)
472 status
= re
.sub(r
'<', r
'<', status
)
473 status
= re
.sub(r
'>', r
'>', 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
)
480 def open_url(self
, source
, url
):
481 """Open url as new browser tab."""
482 _log
.debug('Opening url: %s' % url
)
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
):
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
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
,
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', \
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
, \
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', \
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
),
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)
614 <menubar name="MainMenu">
615 <menuitem action="Quit" />
617 <menuitem action="Refresh" />
619 <menuitem action="About" />
623 uimanager
.add_ui_from_string(ui
)
624 self
.main_menu
= uimanager
.get_widget('/MainMenu')
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
)
641 def close_dialog(self
, user_data
=None):
642 """Hide the dialog window."""
645 def open_status_web(self
, treeview
, path
, view_column
, user_data
=None):
646 """ Callback to open status update in web browser when received
649 model
= treeview
.get_model()
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
)
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
667 _log
.debug('right-click received')
672 pth
= treeview
.get_path_at_pos(x
, y
)
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
)
683 def show_status_popup(self
, treeview
, event
, user_data
=None):
684 _log
.debug('show popup menu')
685 cursor
= treeview
.get_cursor()
688 model
= treeview
.get_model()
693 iter = model
.get_iter(path
)
695 popup_menu
= gtk
.Menu()
696 popup_menu
.set_screen(self
.window
.get_screen())
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?' \
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
, \
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
, \
748 copy_item
.connect('activate', self
.copy_status_to_clipboard
, text
)
749 popup_menu
.append(copy_item
)
751 popup_menu
.show_all()
760 popup_menu
.popup(None, None, None, b
, t
)
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
]
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
,
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
])
785 def _cell_renderer_comments(self
, column
, cell
, store
, position
):
786 comments
= int(store
.get_value(position
, Columns
.COMMENTS
))
788 cell
.set_property('text', str(comments
))
790 cell
.set_property('text', '')
792 def _cell_renderer_commentspic(self
, column
, cell
, store
, position
):
793 comments
= int(store
.get_value(position
, Columns
.COMMENTS
))
795 cell
.set_property('pixbuf', self
.commentspic
)
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
))
802 cell
.set_property('text', str(likes
))
804 cell
.set_property('text', '')
806 def _cell_renderer_likespic(self
, column
, cell
, store
, position
):
807 likes
= int(store
.get_value(position
, Columns
.LIKES
))
809 cell
.set_property('pixbuf', self
.likespic
)
811 cell
.set_property('pixbuf', None)
816 def __init__(self
, facebook
):
817 global spelling_support
819 unknown_user
= 'pixmaps/unknown_user.png'
821 self
._default
_profilepic
= gtk
.gdk
.pixbuf_new_from_file(
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)
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,
873 update_box
.pack_start(update_button
, expand
=False, fill
=False,
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
))
885 spelling
= gtkspell
.Spell(self
.entry
, 'en')
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
], \
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
)
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()
923 gtk
.gdk
.threads_leave()
924 _log
.debug('Exiting')
927 if __name__
== "__main__":
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')
934 _log
.critical('Error while loading config file: %s' % (str(e
)))
937 facebook
= Facebook(api_key
, secret_key
)
939 facebook
.auth
.createToken()
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")
949 _log
.debug('Showing Facebook login page in default browser.')
951 # Delay dialog to allow for login in browser
953 while not got_session
:
954 dia
= gtk
.Dialog('minibook: login',
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)
968 if result
== gtk
.RESPONSE_CANCEL
:
969 _log
.debug('Exiting before Facebook login.')
973 facebook
.auth
.getSession()
978 _log
.info('Session Key: %s' % (facebook
.session_key
))
979 _log
.info('User\'s UID: %d' % (facebook
.uid
))