~erigami/onehundredscopes/unity-lens-pidgin

« back to all changes in this revision

Viewing changes to src/unity-lens-pidgin

  • Committer: Erigami
  • Date: 2012-10-28 23:54:10 UTC
  • Revision ID: erigami@piepalace.ca-20121028235410-igje8kr6pi94j4ho
Improve performance, add filtering on protocol, add (primitive) status display, show protocol/status icons on portraits, split contacts into online/offline.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
1
#! /usr/bin/python
2
2
 
3
 
#    Copyright (c) 2011 David Calle <davidc@framli.eu>
 
3
#    Copyright (c) 2011 David Calle <davidc@framli.eu>, 2012 Evan Hughes.
4
4
 
5
5
#    This program is free software: you can redistribute it and/or modify
6
6
#    it under the terms of the GNU General Public License as published by
15
15
#    You should have received a copy of the GNU General Public License
16
16
#    along with this program.  If not, see <http://www.gnu.org/licenses/>.
17
17
 
 
18
 
18
19
import sys
19
 
import os
 
20
import os, errno
20
21
import glob
21
22
from gi.repository import GLib, GObject, Gio
 
23
from gi.repository import GdkPixbuf, Gdk
22
24
from gi.repository import Dee
23
25
from gi.repository import Unity
24
26
import dbus
 
27
from dbus.mainloop.glib import DBusGMainLoop
25
28
import urllib2
26
29
import simplejson
27
30
import locale
28
 
import datetime
 
31
import datetime, time 
29
32
import urlparse
30
33
 
 
34
import xdg.BaseDirectory
 
35
 
31
36
from operator import attrgetter
32
37
 
33
38
import pprint
 
39
import collections
34
40
import xml.etree.ElementTree
35
41
 
36
42
BUS_NAME = "net.launchpad.lens.pidgin"
37
43
 
 
44
CACHE = "%s/unity-lens-pidgin" % xdg.BaseDirectory.xdg_cache_home
 
45
try:
 
46
    os.makedirs(CACHE)
 
47
except OSError as exc: # Python >2.5
 
48
    if exc.errno == errno.EEXIST:
 
49
        pass
 
50
    else: raise
 
51
 
 
52
 
38
53
class Daemon:
39
54
 
40
55
    def __init__ (self):
 
56
        self._ct = 1
41
57
        self.error_connect = False
42
 
        self._pidgin = Pidgin()
 
58
        self._pidgin = Pidgin(self._update_on_major_change)
 
59
 
 
60
        self._recent = collections.deque((), 10)
43
61
 
44
62
        # The path for the Lens *must* also match the one in our .lens file
45
63
        self._lens = Unity.Lens.new ("/net/launchpad/lens/pidgin", "pidgin")
51
69
        self._lens.props.search_in_global = True;
52
70
        self._lens.props.sources_display_name = ("Pidgin Configs")
53
71
 
54
 
 
55
72
        self._lens.add_local_scope (self._scope);
56
73
        self._scope.connect("search-changed", self.on_search_changed)
 
74
        self._scope.connect("filters-changed", self.on_filter_changed)
57
75
        self._scope.connect("activate-uri", self.on_activate_uri);
58
76
        svg_dir = "/usr/share/icons/unity-icon-theme/places/svg/"
59
77
 
 
78
        self._filt_acct = filt = Unity.CheckOptionFilter.new("_filt_acct", "Accounts", None, False)
 
79
        self._lens.props.filters = [filt]
 
80
 
 
81
        self._set_filters()
 
82
 
60
83
        # Populate categories
61
84
        cats = []
62
 
        cats.append (Unity.Category.new ("Online Buddies",
 
85
        #cats.append (Unity.Category.new ("Recent",
 
86
        #                                 Gio.ThemedIcon.new(svg_dir+"group-recent.svg"),
 
87
        #                                 Unity.CategoryRenderer.HORIZONTAL_TILE))
 
88
        cats.append (Unity.Category.new ("Online",
 
89
                                         Gio.ThemedIcon.new(svg_dir+"group-installed.svg"),
 
90
                                         Unity.CategoryRenderer.HORIZONTAL_TILE))
 
91
        cats.append (Unity.Category.new ("Offline",
63
92
                                         Gio.ThemedIcon.new(svg_dir+"group-installed.svg"),
64
93
                                         Unity.CategoryRenderer.VERTICAL_TILE))
65
94
        self._lens.props.categories = cats
71
100
        self._scope.export ();
72
101
 
73
102
 
 
103
    def _set_filters(self):
 
104
        accts = sorted(self._pidgin.accounts().values(), key=lambda acct: acct.stable_name)
 
105
 
 
106
        allActive = False
 
107
 
 
108
        for o in self._filt_acct.options:
 
109
            self._filt_acct.remove_option(o.props.id)
 
110
 
 
111
        for account in accts:
 
112
            opt = self._filt_acct.add_option(`int(account.id)`, account.name, None)
 
113
 
 
114
 
 
115
    def _update_on_major_change(self):
 
116
        self._set_filters()
 
117
 
 
118
        self._lens.export ();
 
119
        self._scope.export ();
 
120
 
 
121
 
74
122
    def on_activate_uri (self, scope, uri):
75
123
        print "uri = %s" % uri
76
124
        result = urlparse.urlparse(uri)
77
125
        self._pidgin.new_message(result.netloc, result.path.lstrip('/'))
78
 
        # self._pidgin.
 
126
 
79
127
        return Unity.ActivationResponse(handled=Unity.HandledType.HIDE_DASH, goto_uri='')
80
128
 
 
129
 
 
130
    def on_filter_changed(self, scope):
 
131
        accts = scope.get_filter("_filt_acct")
 
132
        result = {}
 
133
        full = self._pidgin.accounts()
 
134
 
 
135
        scope.queue_search_changed(Unity.SearchType.DEFAULT)
 
136
 
 
137
 
81
138
    def on_search_changed (self, scope, search, search_type, *_):
 
139
        t = time.time()
82
140
        #        if search_type is Unity.SearchType.DEFAULT:
83
141
        search_string = search.props.search_string.strip()
 
142
        print "search changed: \"%s\"" % search_string
84
143
        model = search.props.results_model
85
144
        model.clear()
86
 
        self.update_results_model(model, search_string)
 
145
        self.update_results_model(scope, model, search_string)
87
146
        search.set_reply_hint ("no-results-hint", GLib.Variant.new_string("Sorry, there are no buddies that match your search."))
88
147
        search.finished()
89
 
 
90
 
    def update_results_model (self, model, search):
91
 
        if search == '':
92
 
            return
93
 
 
94
 
        buddies = self._pidgin.search(search)
95
 
 
96
 
        for buddy in buddies:
 
148
        print "total time: %s\n" % (time.time() - t)
 
149
 
 
150
 
 
151
 
 
152
    def _included_accounts(self, scope):
 
153
        acct_filter = scope.get_filter("_filt_acct")
 
154
        result = {}
 
155
        accounts = self._pidgin.accounts()
 
156
        for opt in acct_filter.options:
 
157
            if (not acct_filter.props.filtering) or opt.props.active:
 
158
                acctId = int(opt.props.id)
 
159
                result[acctId] = accounts[acctId]
 
160
 
 
161
        return result
 
162
            
 
163
                
 
164
 
 
165
 
 
166
    def update_results_model (self, scope, model, search):
 
167
        accts = self._included_accounts(scope)
 
168
 
 
169
        t = time.time()
 
170
        buddies = self._pidgin.search(search, accts)
 
171
        print "search time: %s" % (time.time() - t)
 
172
 
 
173
        def exists(path):
 
174
            try:
 
175
                if os.path.getsize(path) > 0:
 
176
                    return True
 
177
            except OSError:
 
178
                pass
 
179
 
 
180
            return False
 
181
 
 
182
 
 
183
        startTime = time.time()
 
184
        stale = time.time() - 3600
 
185
        for buddy in buddies.itervalues():
97
186
            icon = "stock_person"
 
187
            path = icon
98
188
            if buddy.icon:
99
 
                icon = "file://%s" % buddy.icon
 
189
                if time.time() < (startTime + 1):
 
190
                    storedPath = "%s/%s.png" % (CACHE, buddy.icon_hash())
 
191
                    if not exists(storedPath) or os.path.getmtime(storedPath) < stale:
 
192
                        statusPath = self._pidgin._get_status_path(buddy.status)
 
193
    
 
194
                        dst = GdkPixbuf.Pixbuf.new_from_file(buddy.icon)
 
195
                        dst = dst.scale_simple(128, 128, GdkPixbuf.InterpType.BILINEAR)
 
196
    
 
197
                        width = dst.get_width()
 
198
                        height = dst.get_height()
 
199
    
 
200
                        if statusPath:
 
201
                            emb = GdkPixbuf.Pixbuf.new_from_file(statusPath)
 
202
                            emb.composite(dst, 
 
203
                                    width - emb.get_width(), height - emb.get_height(),
 
204
                                    emb.get_width(), emb.get_height(),
 
205
                                    width - emb.get_width(), height - emb.get_height(),
 
206
                                    1, 1, 
 
207
                                    GdkPixbuf.InterpType.BILINEAR,
 
208
                                    255
 
209
                            )
 
210
    
 
211
                        protoPath = self._pidgin._get_proto_path(buddy.account)
 
212
                        if protoPath:
 
213
                            pro = GdkPixbuf.Pixbuf.new_from_file(protoPath)
 
214
                            pro = pro.scale_simple(32, 32, GdkPixbuf.InterpType.BILINEAR)
 
215
    
 
216
                            pro.composite(dst, 
 
217
                                    0, height - pro.get_height(),
 
218
                                    pro.get_width(), pro.get_height(),
 
219
                                    0, height - pro.get_height(),
 
220
                                    1, 1, 
 
221
                                    GdkPixbuf.InterpType.BILINEAR,
 
222
                                    255
 
223
                            )
 
224
    
 
225
    
 
226
                        dst.savev(storedPath, "png", (), ())
 
227
    
 
228
                    
 
229
                    icon = "file://" + storedPath 
 
230
    
100
231
 
101
232
            model.append("pidgin://%s/%s" % (buddy.account, buddy.name),
102
233
                         icon,
103
 
                         0,
 
234
                         0 == buddy.online,
104
235
                         "text/html",
105
236
                         buddy.alias,
106
 
                         buddy.name,
107
 
                         icon
 
237
                         "%s - %s" % (buddy.status, accts[buddy.account].name),
 
238
                         path
108
239
                         )
109
240
 
110
241
 
 
242
        print "icon time: %s" % (time.time() - startTime)
 
243
 
 
244
 
111
245
class Buddy:
112
246
    icon = None
113
247
    score = 1000
114
248
 
115
 
    def __init__(self, bid, account, name, alias):
 
249
    def __init__(self, bid, account, name, alias, proto):
116
250
        self.id = bid
117
251
        self.account = account
118
252
        self.name = name
119
253
        self.alias = alias
 
254
        self.online = None
 
255
        self.status = None
 
256
        self.proto = proto
 
257
 
 
258
 
 
259
 
 
260
    def icon_hash(self):
 
261
        """Returns a string suitable to use as a file name that is a hash of the buddy state, icon, and proto"""
 
262
        if self.icon:
 
263
            f = os.path.basename(self.icon)
 
264
            f = os.path.splitext(f)[0]
 
265
 
 
266
            return "x%s-%s-%s" % (f, self.proto, self.status)
 
267
 
 
268
        return None
 
269
 
 
270
 
 
271
class AccountInfo:
 
272
 
 
273
    def __init__(self, stableName, acctId, protoName, iconPath):
 
274
        self.stable_name = stableName
 
275
        self.id = acctId
 
276
        self.name = protoName
 
277
        self.icon_path = iconPath
120
278
 
121
279
 
122
280
class Pidgin:
 
281
    """Maintains our connection to the Pidg and provides access to the buddy list."""
123
282
 
124
 
    def __init__(self):
 
283
    def __init__(self, update_ui_callback):
125
284
        # self._parse_blist()
 
285
        self._update_ui_callback = update_ui_callback
 
286
        self._connection_update_id = None
 
287
 
126
288
        self._bus = dbus.SessionBus()
127
289
        obj = self._bus.get_object("im.pidgin.purple.PurpleService", "/im/pidgin/purple/PurpleObject")
128
290
        self._purple = dbus.Interface(obj, "im.pidgin.purple.PurpleInterface")
 
291
        self._purple.PurpleAccountsInit()
 
292
        self._purple.PurpleBuddyIconsInit()
 
293
 
 
294
        self._buddies = {}
 
295
        self._populate_accounts()
 
296
 
 
297
        self._purple.connect_to_signal("BuddySignedOff", self._on_buddy_status_change, member_keyword='dbus-msg')
 
298
        self._purple.connect_to_signal("BuddySignedOn", self._on_buddy_status_change, member_keyword='dbus-msg')
 
299
        self._purple.connect_to_signal("BuddyRemoved", self._on_buddy_addRemove)
 
300
        self._purple.connect_to_signal("BuddyAdded", self._on_buddy_addRemove)
 
301
 
 
302
        self._purple.connect_to_signal("AccountSignedOn", self._on_connection_change, member_keyword='dbus-msg')
 
303
        self._purple.connect_to_signal("AccountSignedOff", self._on_connection_change, member_keyword='dbus-msg')
 
304
 
 
305
 
 
306
    def _populate_accounts(self):
 
307
        self._accounts = {}
 
308
 
 
309
        for account in self._purple.PurpleAccountsGetAllActive():
 
310
            self._get_proto_path(account)
 
311
 
 
312
        self._gather_buddies()
 
313
 
 
314
 
 
315
    def _on_buddy_status_change(self, buddy_id, *k, **kw):
 
316
        print "%s change: %s" % (kw['dbus-msg'], buddy_id)
 
317
        buddy = self._buddies[buddy_id]
 
318
        self.fill(buddy)
 
319
 
 
320
 
 
321
    def _on_buddy_addRemove(self, buddy_id, **kw):
 
322
        print "%s change: %s" % (kw['dbus-msg'], buddy_id)
 
323
        self._gather_buddies()
 
324
        
 
325
 
 
326
    def _on_connection_change(self, *k, **kw):
 
327
        print "%s change" % kw['dbus-msg']
 
328
        
 
329
        if self._connection_update_id:
 
330
            print "resched"
 
331
            GLib.source_remove(self._connection_update_id)
 
332
            self._connection_update_id = None
 
333
 
 
334
        self._connection_update_id = GLib.timeout_add_seconds(2, self._update_accounts_and_ui)        
 
335
 
 
336
 
 
337
    def _update_accounts_and_ui(self):
 
338
        print "run"
 
339
        self._populate_accounts()
 
340
 
 
341
        self._update_ui_callback()
 
342
        
 
343
 
 
344
    def accounts(self):
 
345
        return self._accounts
 
346
 
129
347
 
130
348
    def _gather_buddies(self):
131
 
        self._buddies = []
 
349
        self._buddies = {}
132
350
        for account in self._purple.PurpleAccountsGetAllActive():
 
351
            proto = self._purple.PurpleAccountGetProtocolName(account)
133
352
            buddylist = self._purple.PurpleFindBuddies(account, '')
134
353
            for buddyid in buddylist:
135
 
                online = self._purple.PurpleBuddyIsOnline(buddyid)
136
 
                if online != 0:
137
 
                    name = self._purple.PurpleBuddyGetName(buddyid)
138
 
                    alias = self._purple.PurpleBuddyGetAlias(buddyid)
139
 
                    self._buddies.append(
140
 
                        Buddy(
141
 
                            buddyid,
142
 
                            account,
143
 
                            name,
144
 
                            alias)
145
 
                        )
 
354
                name = self._purple.PurpleBuddyGetName(buddyid)
 
355
                alias = self._purple.PurpleBuddyGetAlias(buddyid)
 
356
 
 
357
                buddy = Buddy(buddyid, account, name, alias, proto)
 
358
                self.fill(buddy)
 
359
 
 
360
                self._buddies[buddyid] = buddy
 
361
 
146
362
 
147
363
        return self._buddies
148
364
 
 
365
 
149
366
    def new_message(self, account, buddy):
150
367
        current = None
151
368
        for window in self._purple.PurpleGetIms():
158
375
        else:
159
376
            self._purple.PurpleConversationNew(1, int(account), buddy)
160
377
 
 
378
 
161
379
    def fill(self, buddy):
 
380
        buddy.online = self._purple.PurpleBuddyIsOnline(buddy.id)
 
381
        buddy.status = self._purple.PurpleStatusGetId(
 
382
                self._purple.PurplePresenceGetActiveStatus(
 
383
                        self._purple.PurpleBuddyGetPresence(buddy.id)
 
384
                )
 
385
        )
 
386
 
 
387
        buddy.groupName = self._purple.PurpleGroupGetName(
 
388
                self._purple.PurpleBuddyGetGroup(buddy.id)
 
389
        )
 
390
 
162
391
        icon = self._purple.PurpleBuddyGetIcon(buddy.id)
163
392
        if icon > 0:
164
393
            buddy.icon = self._purple.PurpleBuddyIconGetFullPath(icon)
167
396
 
168
397
 
169
398
    def is_active(self, buddy):
170
 
        presense = self._purple.PurpleBuddyGetPresence(buddy.id)
171
 
        status = self._purple.PurplePresenceGetActiveStatus(presense)
172
 
        return self._purple.PurpleStatusGetId(status) == "available"
173
 
 
174
 
    def search(self,  string):
175
 
        buddies = self._gather_buddies()
176
 
        results = []
177
 
 
178
 
        for buddy in buddies:
 
399
        return buddy.status == "available"
 
400
 
 
401
    def search(self, string, includedAccounts):
 
402
        results = {}
 
403
 
 
404
        for key, buddy in self._buddies.iteritems():
 
405
            if not buddy.account in includedAccounts:
 
406
                continue
 
407
 
179
408
            scorename = buddy.name.split('@')[0].lower().find(string)
180
409
            scorealias = buddy.alias.lower().find(string)
181
410
            if ((scorename >= 0) or (scorealias >= 0)):
184
413
                if scorealias < 0:
185
414
                    scorealias = 100
186
415
 
187
 
                self.fill(buddy)
188
 
 
189
416
                if self.is_active(buddy):
190
417
                    buddy.score = 0
191
418
                else:
192
419
                    buddy.score = 100
193
420
 
194
421
                buddy.score += min(scorename, scorealias)
195
 
                results.append(buddy)
196
 
 
197
 
        return sorted(results, key=attrgetter('score'))
198
 
 
 
422
                results[key] = buddy
 
423
 
 
424
        return results
 
425
 
 
426
    STATUS_MAP={
 
427
        "available" : "available",
 
428
        "active" : "available",
 
429
 
 
430
        "offline" : "offline",
 
431
        "away" : "away",
 
432
 
 
433
        "dnd" : "busy",
 
434
    }
 
435
 
 
436
    def _get_status_path(self, statusName):
 
437
        """Return a path to a status icon"""
 
438
        try:
 
439
            return  "/usr/share/pixmaps/pidgin/status/32/%s.png" % Pidgin.STATUS_MAP[statusName]
 
440
        except KeyError:
 
441
            print "Unknown status: %s" % statusName
 
442
            return None
 
443
 
 
444
 
 
445
    def _get_proto_path(self, account):
 
446
        """Return a path to the protocol icon"""
 
447
        if self._accounts.has_key(account):
 
448
            return self._accounts[account].icon_path
 
449
 
 
450
        proto = self._purple.PurpleAccountGetProtocolName(account)
 
451
 
 
452
        name = proto
 
453
        path = None
 
454
        if "Sametime" == proto:
 
455
            path = "meanwhile"
 
456
 
 
457
        elif "XMPP" == proto:
 
458
            disp = self._purple.PurpleAccountGetNameForDisplay(account)
 
459
            if disp.find("gmail.com") > -1:
 
460
                path = "google-talk"
 
461
                name = "GTalk"
 
462
            elif disp.find("facebook.com") > -1:
 
463
                path = "facebook"
 
464
                name = "Facebook"
 
465
            else:
 
466
                name = disp[disp.find("@") + 1 : disp.find("/")]
 
467
 
 
468
        if path:
 
469
            img = "/usr/share/pixmaps/pidgin/protocols/22/%s.png" % path
 
470
        else:
 
471
            img = None
 
472
 
 
473
        stableName = self._purple.PurpleAccountGetNameForDisplay(account)
 
474
        self._accounts[account] = AccountInfo(stableName, account, name, img)
 
475
 
 
476
        return img
 
477
        
 
478
 
 
479
def clear_cache():
 
480
    #os.listdir(CACHE)
 
481
    print "called"
 
482
    return False
 
483
    
199
484
 
200
485
if __name__ == "__main__":
 
486
    DBusGMainLoop(set_as_default=True)
 
487
 
 
488
    loop = GObject.MainLoop()
201
489
    session_bus_connection = Gio.bus_get_sync (Gio.BusType.SESSION, None)
202
490
    session_bus = Gio.DBusProxy.new_sync (session_bus_connection, 0, None,
203
491
                                          'org.freedesktop.DBus',
207
495
                                   GLib.Variant ("(su)", (BUS_NAME, 0x4)),
208
496
                                   0, -1, None)
209
497
 
 
498
 
210
499
    # Unpack variant response with signature "(u)". 1 means we got it.
211
500
    result = result.unpack()[0]
212
501
 
215
504
        raise SystemExit (1)
216
505
 
217
506
    daemon = Daemon()
218
 
    GObject.MainLoop().run()
 
507
    loop.run()