~ken-vandine/askubuntu-lens/packaging2

« back to all changes in this revision

Viewing changes to askubuntu-place.py

  • Committer: Stefano Palazzo
  • Date: 2011-03-18 11:42:15 UTC
  • Revision ID: stefano.palazzo@gmail.com-20110318114215-2sebeif9v88yxrdv
Initial commit

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
#! /usr/bin/python
 
2
 
 
3
# Copyright (c) 2011 Stefano Palazzo <stefano.palazzo@gmail.com>
 
4
# This work is licensed under the terms of the GNU General Public
 
5
# License version 3. If you haven't received a copy of the GPL,
 
6
# go to <http://www.gnu.org/licenses/gpl.txt>.
 
7
 
 
8
import sys
 
9
 
 
10
# Unity imports
 
11
from gi.repository import GLib
 
12
from gi.repository import GObject
 
13
from gi.repository import Gio
 
14
from gi.repository import Dee
 
15
# FIXME: Some weird bug in Dee or PyGI makes Dee fail unless we probe
 
16
#        it *before* we import the Unity module... ?!
 
17
_m = dir(Dee.SequenceModel)
 
18
from gi.repository import Unity
 
19
 
 
20
# google API imports
 
21
import urllib
 
22
import urllib2
 
23
import json
 
24
 
 
25
 
 
26
# TODO
 
27
#    BIG ISSUES
 
28
#  - The google search api is deprecated, move this to the custom search API
 
29
#
 
30
#  - ---- Make sure to follow the terms of service PRECISELY! ----
 
31
#
 
32
#
 
33
#    Smaller Issues
 
34
#  - Show more results? Info on how to do that is here:
 
35
#      <http://code.google.com/apis/websearch/docs/reference.html#_intro_fonje>
 
36
#  - Get rid of Loading cursor on launcher after opening a link
 
37
#  - Hide launcher icon (open issue, should it be there by default?)
 
38
#  - Needs Nice icons (Person and Question should stay, monochrome for groups)
 
39
#  - Searching 'Everything' is too slow compared to searching 'Questions'
 
40
#  - Test this on a slower internet connection!
 
41
 
 
42
def log(message):
 
43
    print "\33[1m%s\33[m" % message
 
44
 
 
45
 
 
46
def google_search(q):
 
47
    print "+" + q + "+"
 
48
    try:
 
49
        query = urllib.urlencode({'q': q})
 
50
        url = ('http://ajax.googleapis.com/ajax/services/search/web?v=1.0&%s'
 
51
          % (query))
 
52
        search_results = urllib2.Request(
 
53
            url, None, {'Referer': "http://unity.ubuntu.com/"})  # TODO
 
54
        results = json.loads(urllib2.urlopen(search_results).read())
 
55
        return results['responseData']['results']
 
56
    except Exception, e:
 
57
        log(repr(e))
 
58
        return []
 
59
#
 
60
# The primary bus name we grab *must* match what we specify in our .place file
 
61
#
 
62
BUS_NAME = "net.launchpad.AskUbuntuPlace"
 
63
 
 
64
# Group ids. Must match the offset into the group models
 
65
GROUP_QUESTIONS = 0
 
66
GROUP_TAGS = 1
 
67
GROUP_EVERYTHING = 2
 
68
GROUP_USERS = 3
 
69
GROUP_BADGES = 4
 
70
 
 
71
# Section ids. Must match the offset into the section model
 
72
SECTION_EVERYTHING = 0
 
73
SECTION_QUESTIONS = 1
 
74
SECTION_TAGS = 2
 
75
SECTION_USERS = 3
 
76
SECTION_BADGES = 4
 
77
 
 
78
 
 
79
class Daemon (object):
 
80
 
 
81
    active_section = 0
 
82
 
 
83
    def __init__(self):
 
84
        # Safe for the signal handlers, I don't know what any of the things
 
85
        # in this method mean. Should have to ask someone.
 
86
 
 
87
        self._entry = Unity.PlaceEntryInfo.new(
 
88
            "/net/launchpad/askubuntuplace/mainentry")
 
89
 
 
90
        #
 
91
        # Set up all the datamodels we'll share with the Unity process
 
92
        # See https://wiki.ubuntu.com/Unity/Places
 
93
        #
 
94
        # Terminology:
 
95
        #
 
96
        #   - "sections" A set of disjoint browsable categories.
 
97
        #                Fx. "Books", "Film", and "Music"
 
98
        #
 
99
        #   - "groups" A set of labels that partition the result set into
 
100
        #              user-visible chunks. "Popular Books", "Recent Books"
 
101
        #
 
102
        sections_model = Dee.SharedModel.new(BUS_NAME + ".SectionsModel")
 
103
        sections_model.set_schema("s", "s")
 
104
        self._entry.props.sections_model = sections_model
 
105
 
 
106
        groups_model = Dee.SharedModel.new(BUS_NAME + ".GroupsModel")
 
107
        groups_model.set_schema("s", "s", "s")
 
108
        self._entry.props.entry_renderer_info.props.groups_model = groups_model
 
109
 
 
110
        global_groups_model = Dee.SharedModel.new(BUS_NAME +
 
111
            ".GlobalGroupsModel")
 
112
        global_groups_model.set_schema("s", "s", "s")
 
113
        self._entry.props.global_renderer_info.props.groups_model = (
 
114
            global_groups_model)
 
115
 
 
116
        results_model = Dee.SharedModel.new(BUS_NAME + ".ResultsModel")
 
117
        results_model.set_schema("s", "s", "u", "s", "s", "s")
 
118
        self._entry.props.entry_renderer_info.props.results_model = (
 
119
            results_model)
 
120
 
 
121
        global_results_model = Dee.SharedModel.new(BUS_NAME +
 
122
            ".GlobalResultsModel")
 
123
        global_results_model.set_schema("s", "s", "u", "s", "s", "s")
 
124
        self._entry.props.global_renderer_info.props.results_model = (
 
125
            global_results_model)
 
126
 
 
127
        #
 
128
        # Populate the sections and groups once we are in sync with Unity
 
129
        #
 
130
        sections_model.connect("notify::synchronized",
 
131
            self._on_sections_synchronized)
 
132
        groups_model.connect("notify::synchronized",
 
133
            self._on_groups_synchronized)
 
134
        global_groups_model.connect("notify::synchronized",
 
135
            self._on_global_groups_synchronized)
 
136
 
 
137
        #
 
138
        # Set up the signals we'll receive when Unity starts to talk to us
 
139
        #
 
140
 
 
141
        # The 'active-search' property is changed when the users searches
 
142
        # within this particular place
 
143
        self._entry.connect("notify::active-search", self._on_search_changed)
 
144
 
 
145
        # The 'active-global-search' property is changed when the user searches
 
146
        # from the Dash aka Home Screen
 
147
        self._entry.connect("notify::active-global-search",
 
148
            self._on_global_search_changed)
 
149
 
 
150
        # Listen for changes to the secion (Everything, Questions, Tags...)
 
151
        self._entry.connect("notify::active-section", self._on_section_change)
 
152
        #
 
153
        # PlaceEntries are housed by PlaceControllers.
 
154
        # You may have mutiple entries per controller if you like.
 
155
        # The controller *must* have the DBus Object path you specify
 
156
        # in your .place file
 
157
        #
 
158
        self._ctrl = Unity.PlaceController.new("/net/launchpad/askubuntuplace")
 
159
        self._ctrl.add_entry(self._entry)
 
160
        self._ctrl.export()
 
161
 
 
162
    def get_search_string(self):
 
163
        search = self._entry.props.active_search
 
164
        return search.get_search_string() if search else None
 
165
 
 
166
    def get_global_search_string(self):
 
167
        search = self._entry.props.active_global_search
 
168
        return search.get_search_string() if search else None
 
169
 
 
170
    def _on_section_change(self, entry_info, section):
 
171
        print "section change"
 
172
        self.active_section = self._entry.props.active_section
 
173
 
 
174
    def _on_sections_synchronized(self, sections_model, *args):
 
175
        # Column0: display name
 
176
        # Column1: GIcon in string format
 
177
        sections_model.clear()
 
178
        sections_model.append("Everything",
 
179
            Gio.ThemedIcon.new("help").to_string())
 
180
        sections_model.append("Questions",
 
181
            Gio.ThemedIcon.new("help").to_string())
 
182
        sections_model.append("Tags",
 
183
            Gio.ThemedIcon.new("help").to_string())
 
184
        sections_model.append("Users",
 
185
            Gio.ThemedIcon.new("stock-person").to_string())
 
186
        sections_model.append("Badges",
 
187
            Gio.ThemedIcon.new("help").to_string())
 
188
 
 
189
    def _on_groups_synchronized(self, groups_model, *args):
 
190
        # Column0: group renderer
 
191
        # Column1: display name
 
192
        # Column2: GIcon in string format
 
193
        groups_model.clear()
 
194
 
 
195
        # !! The row offsets must match the GROUP_* constants !!
 
196
        groups_model.append("UnityDefaultRenderer",
 
197
            "Questions",
 
198
            Gio.ThemedIcon.new("help").to_string())
 
199
        groups_model.append("UnityDefaultRenderer",
 
200
            "Tags",
 
201
            Gio.ThemedIcon.new("tag-new").to_string())
 
202
        groups_model.append("UnityDefaultRenderer",
 
203
            "Search",
 
204
            Gio.ThemedIcon.new("distributor-logo").to_string())
 
205
        groups_model.append("UnityDefaultRenderer",
 
206
            "Users",
 
207
            Gio.ThemedIcon.new("stock-person").to_string())
 
208
        groups_model.append("UnityDefaultRenderer",
 
209
            "Badges",
 
210
            Gio.ThemedIcon.new("distributor-logo").to_string())
 
211
 
 
212
    def _on_global_groups_synchronized(self, global_groups_model, *args):
 
213
        # Just the same as the normal groups
 
214
        self._on_groups_synchronized(global_groups_model)
 
215
 
 
216
    def _on_search_changed(self, *args):
 
217
        search = self.get_search_string()
 
218
        results = self._entry.props.entry_renderer_info.props.results_model
 
219
        print "Search changed to: '%s'" % search
 
220
        self._update_results_model(search, results)
 
221
 
 
222
    def _on_global_search_changed(self, entry, param_spec):
 
223
        search = self.get_global_search_string()
 
224
        results = self._entry.props.global_renderer_info.props.results_model
 
225
        print "Global search changed to: '%s'" % search
 
226
        self._update_results_model(search, results)
 
227
 
 
228
    def add_default_results(self, search, model):
 
229
        icon_hint = Gio.ThemedIcon.new("distributor-logo").to_string()
 
230
        model.append("http://askubuntu.com/questions/ask",
 
231
            Gio.ThemedIcon.new("distributor-logo").to_string(),
 
232
            GROUP_EVERYTHING,
 
233
            "text/html", "Ask your own question",
 
234
            "Ask a Question")
 
235
        if search:
 
236
            model.append("http://askubuntu.com/search?q=" + search.strip(),
 
237
                Gio.ThemedIcon.new("search").to_string(), GROUP_EVERYTHING,
 
238
                "text/html", "Search on AskUbuntu.com",
 
239
                "Find '%s' on AskUbuntu.com" % search)
 
240
 
 
241
    def clean_title(self, title):
 
242
        # This method is very sensitive to changes in the google search API.
 
243
        # It should be the first place to look if results "look odd".
 
244
        title = title.replace("- Ask Ubuntu", "")
 
245
        title = title.replace("- Ask ...", "")
 
246
        title = title.replace("- Stack Exchange", "")
 
247
        title = title.replace("- Stack ...", "")
 
248
        title = title.replace("&#39;", "'")
 
249
        title = title.replace("&amp;", "&")
 
250
        title = title.replace("&lt;", "<")
 
251
        title = title.replace("&gt;", ">")
 
252
        return title.strip()
 
253
 
 
254
    def add_questions_results(self, search, model):
 
255
        icon_hint = Gio.ThemedIcon.new("help").to_string()
 
256
        print "starting search on google"
 
257
        data = google_search("%s site:askubuntu.com/questions/" %
 
258
           search)
 
259
        print "finished searching"
 
260
        for i in data:
 
261
            url = i['url']
 
262
            if (url.startswith("http://askubuntu.com/questions/") and
 
263
                not "/tagged" in url):
 
264
                title = i['titleNoFormatting']
 
265
                if (title.count(" - ") >= 1 and title.split(" - ")[0] ==
 
266
                    title.split(" - ")[0].lower()):  # this is a hack, but it's
 
267
                    title = ' - '.join(title.split(" - ")[1:])  # acceptable.
 
268
                title = self.clean_title(title)
 
269
                model.append(url, icon_hint, GROUP_QUESTIONS, "text/html",
 
270
                        title, title)
 
271
 
 
272
    def add_tags_results(self, search, model):
 
273
        icon_hint = Gio.ThemedIcon.new("tag-new").to_string()
 
274
        print "starting search tags on google"
 
275
        data = google_search("%s site:http://askubuntu.com/tags/" %
 
276
           search)
 
277
        print "finished searching tags"
 
278
        for i in data:
 
279
            url = i['url']
 
280
            title = i['titleNoFormatting'].replace("&#39;", "'")
 
281
            if "Hottest" in title and "Answers" in title:
 
282
                title = title.split("Answers")[0].replace("Hottest ", "")
 
283
                title = title.strip()[1:-1]  # hopefully removes quotes
 
284
                title = self.clean_title(title)
 
285
                model.append(url, icon_hint, GROUP_TAGS, "text/html",
 
286
                    title, title)
 
287
 
 
288
    def add_badges_results(self, search, model):
 
289
        icon_hint = Gio.ThemedIcon.new("help").to_string()
 
290
        print "starting search for badges on google"
 
291
        data = google_search("%s site:askubuntu.com/badges/" %
 
292
           search)
 
293
        print "finished searching"
 
294
        for i in data:
 
295
            url = i['url']
 
296
            if url.startswith("http://askubuntu.com/badges/"):
 
297
                title = i['titleNoFormatting']
 
298
                if " - " in title:
 
299
                    title = title.split(" - ")[0]
 
300
                title = self.clean_title(title)
 
301
                model.append(url, icon_hint, GROUP_BADGES, "text/html",
 
302
                        title, title)
 
303
 
 
304
    def add_users_results(self, search, model):
 
305
        icon_hint = Gio.ThemedIcon.new("stock_person").to_string()
 
306
        print "starting search for users on google"
 
307
        data = google_search("%s site:askubuntu.com/users/" %
 
308
           search)
 
309
        print "finished searching"
 
310
        for i in data:
 
311
            url = i['url']
 
312
            print url
 
313
            if (url.startswith("http://askubuntu.com/users/") and
 
314
                not "/stats" in url):
 
315
                title = i['titleNoFormatting']
 
316
                if " - " in title:
 
317
                    title = title.split(" - ")[0]
 
318
                title = self.clean_title(title)
 
319
                model.append(url, icon_hint, GROUP_USERS, "text/html",
 
320
                        title, title)
 
321
 
 
322
    def _update_results_model(self, search, model):
 
323
        model.clear()
 
324
        if self.active_section == SECTION_EVERYTHING:
 
325
            self.add_default_results(search, model)
 
326
        if search:
 
327
            if self.active_section in (SECTION_EVERYTHING, SECTION_QUESTIONS):
 
328
                self.add_questions_results(search, model)
 
329
            if self.active_section in (SECTION_EVERYTHING, SECTION_TAGS):
 
330
                self.add_tags_results(search, model)
 
331
            if self.active_section in (SECTION_EVERYTHING, SECTION_BADGES):
 
332
                self.add_badges_results(search, model)
 
333
            if self.active_section in (SECTION_EVERYTHING, SECTION_USERS):
 
334
                self.add_users_results(search, model)
 
335
 
 
336
 
 
337
if __name__ == "__main__":
 
338
    session_bus_connection = Gio.bus_get_sync(Gio.BusType.SESSION, None)
 
339
    session_bus = Gio.DBusProxy.new_sync(session_bus_connection, 0, None,
 
340
        'org.freedesktop.DBus', '/org/freedesktop/DBus',
 
341
        'org.freedesktop.DBus', None)
 
342
    result = session_bus.call_sync('RequestName',
 
343
        GLib.Variant("(su)", (BUS_NAME, 0x4)), 0, -1, None)
 
344
    result = result.unpack()[0]
 
345
    if result != 1:
 
346
        print >> sys.stderr, "Failed to own name %s. Bailing out." % BUS_NAME
 
347
        raise SystemExit(1)
 
348
    daemon = Daemon()
 
349
    GObject.MainLoop().run()