~parinporecha/gtg/global_shortcut

« back to all changes in this revision

Viewing changes to GTG/backends/backend_twitter.py

  • Committer: Izidor Matušov
  • Date: 2013-01-10 15:03:42 UTC
  • Revision ID: izidor.matusov@gmail.com-20130110150342-ajwnwmc2trh9ia2v
Removing broken twitter and tweepy services

I don't know anybody uses them. They are broken, and pretty artificial (more proof of the concept). We should focus our efforts on normal synchronization.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# -*- coding: utf-8 -*-
2
 
# -----------------------------------------------------------------------------
3
 
# Getting Things GNOME! - a personal organizer for the GNOME desktop
4
 
# Copyright (c) 2008-2012 - Lionel Dricot & Bertrand Rousseau
5
 
#
6
 
# This program is free software: you can redistribute it and/or modify it under
7
 
# the terms of the GNU General Public License as published by the Free Software
8
 
# Foundation, either version 3 of the License, or (at your option) any later
9
 
# version.
10
 
#
11
 
# This program is distributed in the hope that it will be useful, but WITHOUT
12
 
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
13
 
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
14
 
# details.
15
 
#
16
 
# You should have received a copy of the GNU General Public License along with
17
 
# this program.  If not, see <http://www.gnu.org/licenses/>.
18
 
# -----------------------------------------------------------------------------
19
 
 
20
 
'''
21
 
Twitter backend: imports direct messages, replies and/or the user timeline.
22
 
Authenticates through OAuth.
23
 
'''
24
 
import os
25
 
import re
26
 
import sys
27
 
import uuid
28
 
import subprocess
29
 
from webbrowser import open as openurl
30
 
 
31
 
#the tweepy library is not packaged for Debian/Ubuntu. Thus, a copy of it is
32
 
# kept in the GTG/backends directory
33
 
sys.path.append("GTG/backends")
34
 
import tweepy as tweepy
35
 
 
36
 
from GTG                                import _
37
 
from GTG.backends.genericbackend        import GenericBackend
38
 
from GTG.core                           import CoreConfig
39
 
from GTG.backends.backendsignals        import BackendSignals
40
 
from GTG.backends.periodicimportbackend import PeriodicImportBackend
41
 
from GTG.backends.syncengine            import SyncEngine
42
 
from GTG.tools.logger                   import Log
43
 
 
44
 
 
45
 
class Backend(PeriodicImportBackend):
46
 
    '''
47
 
    Twitter backend: imports direct messages, replies and/or the user timeline.
48
 
    Authenticates through OAuth.
49
 
    '''
50
 
 
51
 
 
52
 
    _general_description = { \
53
 
        GenericBackend.BACKEND_NAME: "backend_twitter", \
54
 
        GenericBackend.BACKEND_HUMAN_NAME: _("Twitter"), \
55
 
        GenericBackend.BACKEND_AUTHORS: ["Luca Invernizzi"], \
56
 
        GenericBackend.BACKEND_TYPE: GenericBackend.TYPE_IMPORT, \
57
 
        GenericBackend.BACKEND_DESCRIPTION: \
58
 
            _("Imports your twitter  messages into your GTG " + \
59
 
              "tasks. You can choose to either import all your " + \
60
 
              "messages or just those with a set of hash tags. \n" + \
61
 
              "The message will be interpreted following this" + \
62
 
              " format: \n" + \
63
 
              "<b>my task title, task description #tag @anothertag</b>\n" + \
64
 
              " Tags can be  anywhere in the message"),\
65
 
        }
66
 
 
67
 
    _static_parameters = { \
68
 
        "period": { \
69
 
            GenericBackend.PARAM_TYPE: GenericBackend.TYPE_INT, \
70
 
            GenericBackend.PARAM_DEFAULT_VALUE: 2, },
71
 
        "import-tags": { \
72
 
            GenericBackend.PARAM_TYPE: GenericBackend.TYPE_LIST_OF_STRINGS, \
73
 
            GenericBackend.PARAM_DEFAULT_VALUE: ["#todo"], },
74
 
        "import-from-replies": { \
75
 
            GenericBackend.PARAM_TYPE: GenericBackend.TYPE_BOOL, \
76
 
            GenericBackend.PARAM_DEFAULT_VALUE: False, },
77
 
        "import-from-my-tweets": { \
78
 
            GenericBackend.PARAM_TYPE: GenericBackend.TYPE_BOOL, \
79
 
            GenericBackend.PARAM_DEFAULT_VALUE: False, },
80
 
        "import-from-direct-messages": { \
81
 
            GenericBackend.PARAM_TYPE: GenericBackend.TYPE_BOOL, \
82
 
            GenericBackend.PARAM_DEFAULT_VALUE: True, },
83
 
        }
84
 
 
85
 
    CONSUMER_KEY = "UDRov5YF3ZUinftvVBoeyA"
86
 
    #This is supposed to be secret (because of OAuth), but that's not possible.
87
 
    #A xAuth alternative is possible, but it's enabled on mail request if the
88
 
    # twitter staff considers your application worthy of such honour.
89
 
    CONSUMER_SECRET = "BApykCPskoZ0g4QpVS7yC7TrZntm87KruSeJwvqTg"
90
 
 
91
 
    def __init__(self, parameters):
92
 
        '''
93
 
        See GenericBackend for an explanation of this function.
94
 
        Re-loads the saved state of the synchronization
95
 
        '''
96
 
        super(Backend, self).__init__(parameters)
97
 
        #loading the list of already imported tasks
98
 
        self.data_path = os.path.join('backends/twitter/', "tasks_dict-%s" %\
99
 
                                     self.get_id())
100
 
        self.sync_engine = self._load_pickled_file(self.data_path, \
101
 
                                                   SyncEngine())
102
 
        #loading the parameters for oauth
103
 
        self.auth_path = os.path.join('backends/twitter/', "auth-%s" %\
104
 
                                     self.get_id())
105
 
        self.auth_params = self._load_pickled_file(self.auth_path, None)
106
 
        self.authenticated = False
107
 
        self.authenticating = False
108
 
 
109
 
    def save_state(self):
110
 
        '''
111
 
        See GenericBackend for an explanation of this function.
112
 
        Saves the state of the synchronization.
113
 
        '''
114
 
        self._store_pickled_file(self.data_path, self.sync_engine)
115
 
 
116
 
###############################################################################
117
 
### IMPORTING TWEETS ##########################################################
118
 
###############################################################################
119
 
    def do_periodic_import(self):
120
 
        '''
121
 
        See GenericBackend for an explanation of this function.
122
 
        '''
123
 
        #abort if authentication is in progress or hasn't been done (in which
124
 
        # case, start it)
125
 
        self.cancellation_point()
126
 
        if not self.authenticated:
127
 
            if not self.authenticating:
128
 
                self._start_authentication()
129
 
            return
130
 
        #select what to import
131
 
        tweets_to_import = []
132
 
        if self._parameters["import-from-direct-messages"]:
133
 
            tweets_to_import += self.api.direct_messages()
134
 
        if self._parameters["import-from-my-tweets"]:
135
 
            tweets_to_import += self.api.user_timeline()
136
 
        if self._parameters["import-from-replies"]:
137
 
            tweets_to_import += self.api.mentions()
138
 
        #do the import
139
 
        for tweet in tweets_to_import:
140
 
            self._process_tweet(tweet)
141
 
 
142
 
    def _process_tweet(self, tweet):
143
 
        '''
144
 
        Given a tweet, checks if a task representing it must be
145
 
        created in GTG and, if so, it creates it.
146
 
 
147
 
        @param tweet: a tweet.
148
 
        '''
149
 
        self.cancellation_point()
150
 
        tweet_id = str(tweet.id)
151
 
        is_syncable = self._is_tweet_syncable(tweet)
152
 
        #the "lambda" is because we don't consider tweets deletion (to be
153
 
        # faster)
154
 
        action, tid = self.sync_engine.analyze_remote_id(\
155
 
                                        tweet_id, \
156
 
                                        self.datastore.has_task, \
157
 
                                        lambda tweet_id: True, \
158
 
                                        is_syncable)
159
 
        Log.debug("processing tweet (%s, %s)" % (action, is_syncable))
160
 
 
161
 
        self.cancellation_point()
162
 
        if action == None or action == SyncEngine.UPDATE:
163
 
            return
164
 
 
165
 
        elif action == SyncEngine.ADD:
166
 
            tid = str(uuid.uuid4())
167
 
            task = self.datastore.task_factory(tid)
168
 
            self._populate_task(task, tweet)
169
 
            #we care only to add tweets and if the list of tags which must be
170
 
            #imported changes (lost-syncability can happen). Thus, we don't
171
 
            # care about SyncMeme(s)
172
 
            self.sync_engine.record_relationship(local_id = tid,\
173
 
                                     remote_id = tweet_id, \
174
 
                                     meme = None)
175
 
            self.datastore.push_task(task)
176
 
 
177
 
        elif action == SyncEngine.LOST_SYNCABILITY:
178
 
            self.sync_engine.break_relationship(remote_id = tweet_id)
179
 
            self.datastore.request_task_deletion(tid)
180
 
 
181
 
        self.save_state()
182
 
 
183
 
    def _populate_task(self, task, message):
184
 
        '''
185
 
        Given a twitter message and a GTG task, fills the task with the content
186
 
        of the message
187
 
        '''
188
 
        #adding the sender as a tag
189
 
        #this works only for some messages types (not for the user timeline)
190
 
        user = None
191
 
        try:
192
 
            user = message.user.screen_name
193
 
        except:
194
 
            pass
195
 
        if user:
196
 
            task.add_tag("@" + user)
197
 
 
198
 
        #setting title, text and tags
199
 
        text = message.text
200
 
        #convert #hastags to @tags
201
 
        matches = re.finditer("(?<![^|\s])(#\w+)", text)
202
 
        for g in matches:
203
 
            text = text[:g.start()] + '@' + text[g.start() + 1:]
204
 
        #add tags objects (it's not enough to have @tag in the text to add a
205
 
        # tag
206
 
        for tag in self._extract_tags_from_text(text):
207
 
            task.add_tag(tag)
208
 
 
209
 
        split_text = text.split(",", 1)
210
 
        task.set_title(split_text[0])
211
 
        if len(split_text) > 1:
212
 
            task.set_text(split_text[1])
213
 
 
214
 
        task.add_remote_id(self.get_id(), str(message.id))
215
 
 
216
 
    def _is_tweet_syncable(self, tweet):
217
 
        '''
218
 
        Returns True if the given tweet matches the user-specified tags to be
219
 
        synced
220
 
 
221
 
        @param tweet: a tweet
222
 
        '''
223
 
        if CoreConfig.ALLTASKS_TAG in self._parameters["import-tags"]:
224
 
            return True
225
 
        else:
226
 
            tags = set(Backend._extract_tags_from_text(tweet.text))
227
 
            return tags.intersection(set(self._parameters["import-tags"])) \
228
 
                    != set()
229
 
 
230
 
    @staticmethod
231
 
    def _extract_tags_from_text(text):
232
 
        '''
233
 
        Given a string, returns a list of @tags and #hashtags
234
 
        '''
235
 
        return list(re.findall(r'(?:^|[\s])((?:#|@)\w+)', text))
236
 
 
237
 
###############################################################################
238
 
### AUTHENTICATION ############################################################
239
 
###############################################################################
240
 
    def _start_authentication(self):
241
 
        '''
242
 
        Fist step of authentication: opening the browser with the oauth page
243
 
        '''
244
 
 
245
 
        #NOTE: just found out that tweepy works with identi.ca (update:
246
 
        #      currently broken!).
247
 
        #      However, twitter is moving to oauth only authentication, while
248
 
        #      identica uses standard login. For now, I'll keep the backends
249
 
        #      separate, using two different libraries (Invernizzi)
250
 
                #auth = tweepy.BasicAuthHandler(username, password,
251
 
                #host ='identi.ca', api_root = '/api',
252
 
                #secure=True)
253
 
        self.auth = tweepy.OAuthHandler(self.CONSUMER_KEY, \
254
 
                                        self.CONSUMER_SECRET)
255
 
        self.cancellation_point()
256
 
        if self.auth_params == None:
257
 
            #no previous contact with the server has been made: no stored
258
 
            # oauth token found
259
 
            self.authenticating = True
260
 
            Log.info("Openning browser for twitter authentification")
261
 
            openurl(self.auth.get_authorization_url())
262
 
            BackendSignals().interaction_requested(self.get_id(),
263
 
                "You need to authenticate to <b>Twitter</b>. A browser"
264
 
                " is opening with the correct page. When you have "
265
 
                " received a PIN code, press 'Continue'.", \
266
 
                BackendSignals().INTERACTION_TEXT,
267
 
                "on_authentication_step")
268
 
        else:
269
 
            #we have gone through authentication successfully before.
270
 
            self.cancellation_point()
271
 
            try:
272
 
                self.auth.set_access_token(self.auth_params[0],\
273
 
                                       self.auth_params[1])
274
 
            except tweepy.TweepError, e:
275
 
                self._on_auth_error(e)
276
 
                return
277
 
            self.cancellation_point()
278
 
            self._end_authentication()
279
 
 
280
 
    def on_authentication_step(self, step_type = "", pin = ""):
281
 
        '''
282
 
        Handles the various steps of authentication. It's the only callback
283
 
        function the UI knows about this backend.
284
 
 
285
 
        @param step_type: if "get_ui_dialog_text", returns the text to be put
286
 
                          in the dialog requesting the pin.
287
 
                          if "set_text", the UI is feeding the backend with
288
 
                          the pin the user provided
289
 
        @param pin: contains the pin if step_type == "set_text"
290
 
        '''
291
 
        if step_type == "get_ui_dialog_text":
292
 
            return "PIN request", "Insert the PIN you should have received "\
293
 
                                  "through your web browser here:"
294
 
        elif step_type == "set_text":
295
 
            try:
296
 
                token = self.auth.get_access_token(verifier = pin)
297
 
            except tweepy.TweepError, e:
298
 
                self._on_auth_error(e)
299
 
                return
300
 
            self.auth_params = (token.key, token.secret)
301
 
            self._store_pickled_file(self.auth_path, self.auth_params)
302
 
            self._end_authentication()
303
 
 
304
 
    def _end_authentication(self):
305
 
        '''
306
 
        Last step of authentication. Creates the API objects and starts
307
 
        importing tweets
308
 
        '''
309
 
        self.authenticated = True
310
 
        self.authenticating = False
311
 
        self.api = tweepy.API(auth_handler = self.auth, \
312
 
                              secure = True, \
313
 
                              retry_count = 3)
314
 
        self.cancellation_point()
315
 
        self.start_get_tasks()
316
 
 
317
 
    def _on_auth_error(self, exception):
318
 
        '''
319
 
        On authentication error, informs the user.
320
 
 
321
 
        @param exception: the Exception object that was raised during
322
 
                          authentication
323
 
        '''
324
 
        if isinstance(exception, tweepy.TweepError):
325
 
            if exception.reason == "HTTP Error 401: Unauthorized":
326
 
                self.auth_params = None
327
 
                self._store_pickled_file(self.auth_path, self.auth_params)
328
 
                self.quit(disable = True)
329
 
                BackendSignals().backend_failed(self.get_id(), \
330
 
                                BackendSignals.ERRNO_AUTHENTICATION)
331
 
 
332
 
    def signal_network_down(self):
333
 
        '''
334
 
        If the network is unresponsive, inform the user
335
 
        '''
336
 
        BackendSignals().backend_failed(self.get_id(), \
337
 
                        BackendSignals.ERRNO_NETWORK)