2 """ Minibook: the Facebook(TM) status updater
3 (C) 2009 Gergely Imreh <imrehg@gmail.com>
14 from facebook
import Facebook
16 print "Pyfacebook is not available, cannot run."
23 gobject
.threads_init()
24 gtk
.gdk
.threads_init()
25 gtk
.gdk
.threads_enter()
29 spelling_support
= True
31 spelling_support
= False
36 LEVELS
= {'debug': logging
.DEBUG
,
38 'warning': logging
.WARNING
,
39 'error': logging
.ERROR
,
40 'critical': logging
.CRITICAL
}
43 level_name
= sys
.argv
[1]
44 level
= LEVELS
.get(level_name
, logging
.NOTSET
)
45 logging
.basicConfig(level
=level
)
49 (STATUSID
, UID
, STATUS
, DATETIME
, REPLIES
, LIKES
) = range(6)
52 #-------------------------------------------------
53 # From http://edsiper.linuxchile.cl/blog/?p=152
54 # to mitigate TreeView + threads problems
55 #-------------------------------------------------
57 class _IdleObject(gobject
.GObject
):
59 Override gobject.GObject to always emit signals in the main thread
60 by emmitting on an idle handler
64 gobject
.GObject
.__init
__(self
)
66 def emit(self
, *args
):
67 gobject
.idle_add(gobject
.GObject
.emit
, self
, *args
)
70 #-------------------------------------------------
72 #-------------------------------------------------
74 class _WorkerThread(threading
.Thread
, _IdleObject
):
75 """A single working thread."""
79 gobject
.SIGNAL_RUN_LAST
,
81 (gobject
.TYPE_PYOBJECT
, )),
83 gobject
.SIGNAL_RUN_LAST
,
85 (gobject
.TYPE_PYOBJECT
, ))}
87 def __init__(self
, function
, *args
, **kwargs
):
88 threading
.Thread
.__init
__(self
)
89 _IdleObject
.__init
__(self
)
90 self
._function
= function
96 print('Thread %s calling %s' % (self
.name
, str(self
._function
)))
102 result
= self
._function
(*args
, **kwargs
)
103 except Exception, exc
: # Catch ALL exceptions
104 # TODO: Check if this catch all warnins too!
105 print('Exception %s' % str(exc
))
106 self
.emit("exception", exc
)
109 print('Thread %s completed' % (self
.name
))
111 self
.emit("completed", result
)
115 class _ThreadManager(object):
116 """Manages the threads."""
118 def __init__(self
, max_threads
=2):
119 """Start the thread pool. The number of threads in the pool is defined
120 by `pool_size`, defaults to 2."""
121 self
._max
_threads
= max_threads
122 self
._thread
_pool
= []
128 def _remove_thread(self
, widget
, arg
=None):
129 """Called when the thread completes. We remove it from the thread list
130 (dictionary, actually) and start the next thread (if there is one)."""
132 # not actually a widget. It's the object that emitted the signal, in
133 # this case, the _WorkerThread object.
134 thread_id
= widget
.name
136 print('Thread %s completed, %d threads in the queue' % (thread_id
,
137 len(self
._thread
_pool
)))
139 self
._running
.remove(thread_id
)
141 if self
._thread
_pool
:
142 if len(self
._running
) < self
._max
_threads
:
143 next
= self
._thread
_pool
.pop()
144 print('Dequeuing thread %s', next
.name
)
145 self
._running
.append(next
.name
)
150 def add_work(self
, complete_cb
, exception_cb
, func
, *args
, **kwargs
):
151 """Add a work to the thread list."""
153 thread
= _WorkerThread(func
, *args
, **kwargs
)
154 thread_id
= '%s' % (self
._thread
_id
)
156 thread
.connect('completed', complete_cb
)
157 thread
.connect('completed', self
._remove
_thread
)
158 thread
.connect('exception', exception_cb
)
159 thread
.setName(thread_id
)
161 if len(self
._running
) < self
._max
_threads
:
162 self
._running
.append(thread_id
)
165 running_names
= ', '.join(self
._running
)
166 print('Threads %s running, adding %s to the queue',
167 running_names
, thread_id
)
168 self
._thread
_pool
.append(thread
)
175 """The main application interface"""
178 #------------------------------
179 # Information sending functions
180 #------------------------------
181 def sendupdate(self
):
182 textfield
= self
.entry
.get_buffer()
183 start
= textfield
.get_start_iter()
184 end
= textfield
.get_end_iter()
185 entry_text
= textfield
.get_text(start
, end
)
187 print "Sent entry contents: %s\n" % entry_text
188 self
._facebook
.status
.set([entry_text
], [self
._facebook
.uid
])
190 textfield
.set_text("")
193 #------------------------------
194 # Information pulling functions
195 #------------------------------
196 def get_friends_list(self
):
197 query
= ("SELECT uid, name FROM user \
198 WHERE (uid IN (SELECT uid2 FROM friend WHERE uid1 = %d) \
199 OR uid = %d)" % (self
._facebook
.uid
, self
._facebook
.uid
))
200 friends
= self
._facebook
.fql
.query([query
])
201 self
.friendsname
= {}
202 for friend
in friends
:
203 self
.friendsname
[str(friend
['uid'])] = friend
['name']
205 def post_get_friends_list(self
, widget
, results
):
206 print("%s has altogether %d friends in the database." \
207 % (self
.friendsname
[str(self
._facebook
.uid
)],
208 len(self
.friendsname
.keys())))
212 def except_get_friends_list(self
, widget
, exception
):
213 print("Get friends exception: %s" % (str(exception
)))
215 def get_status_list(self
):
216 if self
._last
_update
> 0:
217 since
= self
._last
_update
219 now
= int(time
.time())
220 since
= now
- 5*24*60*60
222 print("---> Statuses since: %s" \
223 % (time
.strftime("%c", time
.localtime(since
))))
224 query
= ('SELECT uid, time, status_id, message FROM status \
225 WHERE (uid IN (SELECT uid2 FROM friend WHERE uid1 = %d) \
230 % (self
._facebook
.uid
, self
._facebook
.uid
, since
))
231 status
= self
._facebook
.fql
.query([query
])
233 self
.liststore
.append((up
['status_id'],
240 def post_get_status_list(self
, widget
, results
):
241 print("Status updates successfully pulled.")
244 def except_get_status_list(self
, widget
, exception
):
245 print("Get status list exception: %s" % (str(exception
)))
250 def count(self
, text
):
251 start
= text
.get_start_iter()
252 end
= text
.get_end_iter()
253 thetext
= text
.get_text(start
, end
)
254 self
.count_label
.set_text('(%d)' % (160 - len(thetext
)))
257 def set_auto_refresh(self
):
259 gobject
.source_remove(self
._refresh
_id
)
261 self
._refresh
_id
= gobject
.timeout_add(
262 self
._prefs
['auto_refresh_interval']*60*1000,
264 print("Auto-refresh enabled: %d minutes" \
265 % (self
._prefs
['auto_refresh_interval']))
268 self
._threads
.add_work(self
.post_get_status_list
,
269 self
.except_get_status_list
,
270 self
.get_status_list
)
273 def status_format(self
, column
, cell
, store
, position
):
274 uid
= store
.get_value(position
, Columns
.UID
)
276 name
= self
.friendsname
[str(uid
)]
278 print store
.get_value(position
, Columns
.STATUS
)
279 print store
.get_value(position
, Columns
.UID
)
280 print store
.get_value(position
, Columns
.DATETIME
)
282 status
= store
.get_value(position
, Columns
.STATUS
)
283 datetime
= time
.localtime(float(store
.get_value(position
, \
285 displaytime
= time
.strftime('%c', datetime
)
287 #replace characters that would choke the markup
288 status
= re
.sub(r
'<', r
'<', status
)
289 status
= re
.sub(r
'>', r
'>', status
)
290 markup
= '<b>%s</b> %s\non %s' % \
291 (name
, status
, displaytime
)
292 cell
.set_property('markup', markup
)
295 #--------------------
296 # Interface functions
297 #--------------------
298 def systray_click(self
, widget
, user_param
=None):
299 if self
.window
.get_property('visible'):
300 x
, y
= self
.window
.get_position()
301 self
._prefs
['window_pos_x'] = x
302 self
._prefs
['window_pos_y'] = y
305 x
= self
._prefs
['window_pos_x']
306 y
= self
._prefs
['window_pos_y']
307 self
.window
.move(x
, y
)
308 self
.window
.deiconify()
309 self
.window
.present()
311 def create_grid(self
):
312 self
.liststore
= gtk
.ListStore(gobject
.TYPE_STRING
,
318 self
.treeview
= gtk
.TreeView(self
.liststore
)
319 self
.treeview
.set_property('headers-visible', False)
320 self
.treeview
.set_rules_hint(True)
322 self
.status_renderer
= gtk
.CellRendererText()
323 #~ self.status_renderer.set_property('wrap-mode', gtk.WRAP_WORD)
324 self
.status_renderer
.set_property('wrap-width', 350)
325 self
.status_renderer
.set_property('width', 10)
327 self
.status_column
= gtk
.TreeViewColumn('Message', \
328 self
.status_renderer
, text
=1)
329 self
.status_column
.set_cell_data_func(self
.status_renderer
, \
331 self
.treeview
.append_column(self
.status_column
)
332 self
.treeview
.set_resize_mode(gtk
.RESIZE_IMMEDIATE
)
337 def __init__(self
, facebook
):
338 global spelling_support
340 # create a new window
341 self
.window
= gtk
.Window(gtk
.WINDOW_TOPLEVEL
)
342 self
.window
.set_size_request(400, 250)
343 self
.window
.set_title("Minibook")
344 self
.window
.connect("delete_event", lambda w
, e
: gtk
.main_quit())
346 vbox
= gtk
.VBox(False, 0)
347 self
.window
.add(vbox
)
351 self
.statuslist_window
= gtk
.ScrolledWindow()
352 self
.statuslist_window
.set_policy(gtk
.POLICY_NEVER
, gtk
.POLICY_ALWAYS
)
353 self
.statuslist_window
.add(self
.treeview
)
355 self
.statuslist_window
.show()
356 vbox
.add(self
.statuslist_window
)
358 hbox
= gtk
.HBox(False, 0)
360 label
= gtk
.Label("What's on your mind?")
361 hbox
.pack_start(label
, True, True, 0)
363 self
.count_label
= gtk
.Label("(160)")
364 hbox
.pack_start(self
.count_label
, True, True, 0)
365 self
.count_label
.show()
369 self
.entry
= gtk
.TextView()
370 text
= self
.entry
.get_buffer()
371 text
.connect('changed', self
.count
)
372 vbox
.pack_start(self
.entry
, True, True, 0)
375 hbox
= gtk
.HBox(False, 0)
379 button
= gtk
.Button(stock
=gtk
.STOCK_CLOSE
)
380 button
.connect("clicked", lambda w
: gtk
.main_quit())
381 hbox
.pack_start(button
, True, True, 0)
382 button
.set_flags(gtk
.CAN_DEFAULT
)
383 button
.grab_default()
386 button
= gtk
.Button(stock
=gtk
.STOCK_ADD
)
387 button
.connect("clicked", lambda w
: self
.sendupdate())
388 hbox
.pack_start(button
, True, True, 0)
389 button
.set_flags(gtk
.CAN_DEFAULT
)
390 button
.grab_default()
395 spelling
= gtkspell
.Spell(self
.entry
, 'en')
397 spelling_support
= False
400 self
._facebook
= facebook
402 self
._app
_icon
= 'minibook.png'
403 self
._systray
= gtk
.StatusIcon()
404 self
._systray
.set_from_file(self
._app
_icon
)
405 self
._systray
.set_tooltip('%s\n' \
406 'Left-click: toggle window hiding' % (APPNAME
))
407 self
._systray
.connect('activate', self
.systray_click
)
408 self
._systray
.set_visible(True)
410 self
.window
.set_icon_from_file(self
._app
_icon
)
412 self
._threads
= _ThreadManager()
414 self
.userinfo
= self
._facebook
.users
.getInfo([self
._facebook
.uid
], \
416 #~ self._threads.add_work(self.post_updates,
417 #~ self.except_updates,
419 self
._last
_update
= 0
420 self
._threads
.add_work(self
.post_get_friends_list
,
421 self
.except_get_friends_list
,
422 self
.get_friends_list
)
425 x
, y
= self
.window
.get_position()
426 self
._prefs
['window_pos_x'] = x
427 self
._prefs
['window_pos_y'] = y
428 self
._prefs
['auto_refresh_interval'] = 5
430 self
._refresh
_id
= None
431 self
.set_auto_refresh()
436 gtk
.gdk
.threads_leave()
439 if __name__
== "__main__":
441 config_file
= open("config", "r")
442 api_key
= config_file
.readline()[:-1]
443 secret_key
= config_file
.readline()[:-1]
445 exit('Error while loading config file: %s' % (str(e
)))
446 facebook
= Facebook(api_key
, secret_key
)
447 facebook
.auth
.createToken()
450 # Delay dialog to allow for login in browser
451 dia
= gtk
.Dialog('minibook: login',
454 gtk
.DIALOG_DESTROY_WITH_PARENT | \
455 gtk
.DIALOG_NO_SEPARATOR
,
456 ("Logged In", gtk
.RESPONSE_OK
, gtk
.STOCK_CANCEL
, gtk
.RESPONSE_CLOSE
))
457 label
= gtk
.Label("Click after logging in to Facebook in your browser:")
458 dia
.vbox
.pack_start(label
, True, True, 10)
462 if result
== gtk
.RESPONSE_CLOSE
:
467 facebook
.auth
.getSession()
468 print 'Session Key: ', facebook
.session_key
469 print 'Your UID: ', facebook
.uid