~parinporecha/gtg/global_shortcut

« back to all changes in this revision

Viewing changes to GTG/backends/backend_identica.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
 
Identi.ca backend: imports direct messages, replies and/or the user timeline.
22
 
'''
23
 
 
24
 
import os
25
 
import re
26
 
import sys
27
 
import uuid
28
 
import urllib2
29
 
 
30
 
from GTG                                import _
31
 
from GTG.backends.genericbackend        import GenericBackend
32
 
from GTG.core                           import CoreConfig
33
 
from GTG.backends.backendsignals        import BackendSignals
34
 
from GTG.backends.periodicimportbackend import PeriodicImportBackend
35
 
from GTG.backends.syncengine            import SyncEngine
36
 
from GTG.tools.logger                   import Log
37
 
 
38
 
#The Ubuntu version of python twitter is not updated:
39
 
# it does not have identi.ca support. Meanwhile, we ship the right version
40
 
# with our code.
41
 
import GTG.backends.twitter as twitter
42
 
 
43
 
 
44
 
class Backend(PeriodicImportBackend):
45
 
    '''
46
 
    Identi.ca backend: imports direct messages, replies and/or the user
47
 
    timeline.
48
 
    '''
49
 
 
50
 
 
51
 
    _general_description = {
52
 
        GenericBackend.BACKEND_NAME: "backend_identica",
53
 
        GenericBackend.BACKEND_HUMAN_NAME: _("Identi.ca"),
54
 
        GenericBackend.BACKEND_AUTHORS: ["Luca Invernizzi"],
55
 
        GenericBackend.BACKEND_TYPE: GenericBackend.TYPE_IMPORT,
56
 
        GenericBackend.BACKEND_DESCRIPTION:
57
 
            _("Imports your identi.ca  messages into your GTG " + \
58
 
              "tasks. You can choose to either import all your " + \
59
 
              "messages or just those with a set of hash tags. \n" + \
60
 
              "The message will be interpreted following this" + \
61
 
              " format: \n" + \
62
 
              "<b>my task title, task description #tag @anothertag</b>\n" + \
63
 
              " Tags can be  anywhere in the message"),
64
 
        }
65
 
 
66
 
    base_url = "http://identi.ca/api/"
67
 
 
68
 
    _static_parameters = {
69
 
        "username": {
70
 
            GenericBackend.PARAM_TYPE: GenericBackend.TYPE_STRING,
71
 
            GenericBackend.PARAM_DEFAULT_VALUE: "", },
72
 
        "password": {
73
 
            GenericBackend.PARAM_TYPE: GenericBackend.TYPE_PASSWORD,
74
 
            GenericBackend.PARAM_DEFAULT_VALUE: "", },
75
 
        "period": {
76
 
            GenericBackend.PARAM_TYPE: GenericBackend.TYPE_INT,
77
 
            GenericBackend.PARAM_DEFAULT_VALUE: 2, },
78
 
        "import-tags": {
79
 
            GenericBackend.PARAM_TYPE: GenericBackend.TYPE_LIST_OF_STRINGS,
80
 
            GenericBackend.PARAM_DEFAULT_VALUE: ["#todo"], },
81
 
        "import-from-replies": {
82
 
            GenericBackend.PARAM_TYPE: GenericBackend.TYPE_BOOL,
83
 
            GenericBackend.PARAM_DEFAULT_VALUE: False, },
84
 
        "import-from-my-tweets": {
85
 
            GenericBackend.PARAM_TYPE: GenericBackend.TYPE_BOOL,
86
 
            GenericBackend.PARAM_DEFAULT_VALUE: False, },
87
 
        "import-from-direct-messages": {
88
 
            GenericBackend.PARAM_TYPE: GenericBackend.TYPE_BOOL,
89
 
            GenericBackend.PARAM_DEFAULT_VALUE: True, },
90
 
        }
91
 
 
92
 
    def __init__(self, parameters):
93
 
        '''
94
 
        See GenericBackend for an explanation of this function.
95
 
        Re-loads the saved state of the synchronization
96
 
        '''
97
 
        super(Backend, self).__init__(parameters)
98
 
        #loading the list of already imported tasks
99
 
        self.data_path = os.path.join('backends/identica/', "tasks_dict-%s" %\
100
 
                                     self.get_id())
101
 
        self.sync_engine = self._load_pickled_file(self.data_path,
102
 
                                                        SyncEngine())
103
 
 
104
 
    def save_state(self):
105
 
        '''
106
 
        See GenericBackend for an explanation of this function.
107
 
        Saves the state of the synchronization.
108
 
        '''
109
 
        self._store_pickled_file(self.data_path, self.sync_engine)
110
 
 
111
 
    def do_periodic_import(self):
112
 
        '''
113
 
        See GenericBackend for an explanation of this function.
114
 
        '''
115
 
        #we need to authenticate only to see the direct messages or the replies
116
 
        # (why the replies? Don't know. But python-twitter requires that)
117
 
        # (invernizzi)
118
 
        with self.controlled_execution(self._parameters['username'],
119
 
                                       self._parameters['password'],
120
 
                                       self.base_url,
121
 
                                       self) as api:
122
 
            #select what to import
123
 
            tweets_to_import = []
124
 
            if self._parameters["import-from-direct-messages"]:
125
 
                tweets_to_import += api.GetDirectMessages()
126
 
            if self._parameters["import-from-my-tweets"]:
127
 
                tweets_to_import += \
128
 
                        api.GetUserTimeline(self._parameters["username"])
129
 
            if self._parameters["import-from-replies"]:
130
 
                tweets_to_import += \
131
 
                    api.GetReplies(self._parameters["username"])
132
 
            #do the import
133
 
            for tweet in tweets_to_import:
134
 
                self._process_tweet(tweet)
135
 
 
136
 
    def _process_tweet(self, tweet):
137
 
        '''
138
 
        Given a tweet, checks if a task representing it must be
139
 
        created in GTG and, if so, it creates it.
140
 
 
141
 
        @param tweet: a tweet.
142
 
        '''
143
 
        self.cancellation_point()
144
 
        tweet_id = str(tweet.GetId())
145
 
        is_syncable = self._is_tweet_syncable(tweet)
146
 
        #the "lambda" is because we don't consider tweets deletion (to be
147
 
        # faster)
148
 
        action, tid = self.sync_engine.analyze_remote_id(
149
 
                                        tweet_id,
150
 
                                        self.datastore.has_task,
151
 
                                        lambda tweet_id: True,
152
 
                                        is_syncable)
153
 
        Log.debug("processing tweet (%s, %s)" % (action, is_syncable))
154
 
 
155
 
        self.cancellation_point()
156
 
        if action == None or action == SyncEngine.UPDATE:
157
 
            return
158
 
 
159
 
        elif action == SyncEngine.ADD:
160
 
            tid = str(uuid.uuid4())
161
 
            task = self.datastore.task_factory(tid)
162
 
            self._populate_task(task, tweet)
163
 
            #we care only to add tweets and if the list of tags which must be
164
 
            #imported changes (lost-syncability can happen). Thus, we don't
165
 
            # care about SyncMeme(s)
166
 
            self.sync_engine.record_relationship(local_id = tid,
167
 
                                     remote_id = tweet_id,
168
 
                                     meme = None)
169
 
            self.datastore.push_task(task)
170
 
 
171
 
        elif action == SyncEngine.LOST_SYNCABILITY:
172
 
            self.sync_engine.break_relationship(remote_id = tweet_id)
173
 
            self.datastore.request_task_deletion(tid)
174
 
 
175
 
        self.save_state()
176
 
 
177
 
    def _populate_task(self, task, message):
178
 
        '''
179
 
        Given a twitter message and a GTG task, fills the task with the content
180
 
        of the message
181
 
        '''
182
 
        try:
183
 
            #this works only for some messages
184
 
            task.add_tag("@" + message.GetSenderScreenName())
185
 
        except:
186
 
            pass
187
 
        text = message.GetText()
188
 
 
189
 
        #convert #hastags to @tags
190
 
        matches = re.finditer("(?<![^|\s])(#\w+)", text)
191
 
        for g in matches:
192
 
            text = text[:g.start()] + '@' + text[g.start() + 1:]
193
 
        #add tags objects (it's not enough to have @tag in the text to add a
194
 
        # tag
195
 
        for tag in self._extract_tags_from_text(text):
196
 
            task.add_tag(tag)
197
 
 
198
 
        split_text = text.split(",", 1)
199
 
        task.set_title(split_text[0])
200
 
        if len(split_text) > 1:
201
 
            task.set_text(split_text[1])
202
 
 
203
 
        task.add_remote_id(self.get_id(), str(message.GetId()))
204
 
 
205
 
    def _is_tweet_syncable(self, tweet):
206
 
        '''
207
 
        Returns True if the given tweet matches the user-specified tags to be
208
 
        synced
209
 
 
210
 
        @param tweet: a tweet
211
 
        '''
212
 
        if CoreConfig.ALLTASKS_TAG in self._parameters["import-tags"]:
213
 
            return True
214
 
        else:
215
 
            tags = set(Backend._extract_tags_from_text(tweet.GetText()))
216
 
            return tags.intersection(set(self._parameters["import-tags"])) \
217
 
                    != set()
218
 
 
219
 
    @staticmethod
220
 
    def _extract_tags_from_text(text):
221
 
        '''
222
 
        Given a string, returns a list of @tags and #hashtags
223
 
        '''
224
 
        return list(re.findall(r'(?:^|[\s])((?:#|@)\w+)', text))
225
 
 
226
 
###############################################################################
227
 
### AUTHENTICATION ############################################################
228
 
###############################################################################
229
 
    class controlled_execution(object):
230
 
        '''
231
 
        This class performs the login to identica and execute the appropriate
232
 
        response if something goes wrong during authentication or at network
233
 
        level
234
 
        '''
235
 
 
236
 
        def __init__(self, username, password, base_url, backend):
237
 
            '''
238
 
            Sets the login parameters
239
 
            '''
240
 
            self.username = username
241
 
            self.password = password
242
 
            self.backend = backend
243
 
            self.base_url = base_url
244
 
 
245
 
        def __enter__(self):
246
 
            '''
247
 
            Logins to identica and returns the Api object
248
 
            '''
249
 
            return twitter.Api(self.username, self.password,
250
 
                            base_url = self.base_url)
251
 
 
252
 
        def __exit__(self, type, value, traceback):
253
 
            '''
254
 
            Analyzes the eventual exception risen during the connection to
255
 
            identica
256
 
            '''
257
 
            if isinstance(value, urllib2.HTTPError):
258
 
                if value.getcode() == 401:
259
 
                    self.signal_authentication_wrong()
260
 
                if value.getcode() in [502, 404]:
261
 
                    self.signal_network_down()
262
 
            elif isinstance(value, twitter.TwitterError):
263
 
                self.signal_authentication_wrong()
264
 
            elif isinstance(value, urllib2.URLError):
265
 
                self.signal_network_down()
266
 
            else:
267
 
                return False
268
 
            return True
269
 
 
270
 
        def signal_authentication_wrong(self):
271
 
            self.backend.quit(disable = True)
272
 
            BackendSignals().backend_failed(self.backend.get_id(),
273
 
                            BackendSignals.ERRNO_AUTHENTICATION)
274
 
 
275
 
        def signal_network_down(self):
276
 
            BackendSignals().backend_failed(self.backend.get_id(),
277
 
                            BackendSignals.ERRNO_NETWORK)