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
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
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
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
# -----------------------------------------------------------------------------
21
Identi.ca backend: imports direct messages, replies and/or the user timeline.
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
38
#The Ubuntu version of python twitter is not updated:
39
# it does not have identi.ca support. Meanwhile, we ship the right version
41
import GTG.backends.twitter as twitter
44
class Backend(PeriodicImportBackend):
46
Identi.ca backend: imports direct messages, replies and/or the user
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" + \
62
"<b>my task title, task description #tag @anothertag</b>\n" + \
63
" Tags can be anywhere in the message"),
66
base_url = "http://identi.ca/api/"
68
_static_parameters = {
70
GenericBackend.PARAM_TYPE: GenericBackend.TYPE_STRING,
71
GenericBackend.PARAM_DEFAULT_VALUE: "", },
73
GenericBackend.PARAM_TYPE: GenericBackend.TYPE_PASSWORD,
74
GenericBackend.PARAM_DEFAULT_VALUE: "", },
76
GenericBackend.PARAM_TYPE: GenericBackend.TYPE_INT,
77
GenericBackend.PARAM_DEFAULT_VALUE: 2, },
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, },
92
def __init__(self, parameters):
94
See GenericBackend for an explanation of this function.
95
Re-loads the saved state of the synchronization
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" %\
101
self.sync_engine = self._load_pickled_file(self.data_path,
104
def save_state(self):
106
See GenericBackend for an explanation of this function.
107
Saves the state of the synchronization.
109
self._store_pickled_file(self.data_path, self.sync_engine)
111
def do_periodic_import(self):
113
See GenericBackend for an explanation of this function.
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)
118
with self.controlled_execution(self._parameters['username'],
119
self._parameters['password'],
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"])
133
for tweet in tweets_to_import:
134
self._process_tweet(tweet)
136
def _process_tweet(self, tweet):
138
Given a tweet, checks if a task representing it must be
139
created in GTG and, if so, it creates it.
141
@param tweet: a tweet.
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
148
action, tid = self.sync_engine.analyze_remote_id(
150
self.datastore.has_task,
151
lambda tweet_id: True,
153
Log.debug("processing tweet (%s, %s)" % (action, is_syncable))
155
self.cancellation_point()
156
if action == None or action == SyncEngine.UPDATE:
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,
169
self.datastore.push_task(task)
171
elif action == SyncEngine.LOST_SYNCABILITY:
172
self.sync_engine.break_relationship(remote_id = tweet_id)
173
self.datastore.request_task_deletion(tid)
177
def _populate_task(self, task, message):
179
Given a twitter message and a GTG task, fills the task with the content
183
#this works only for some messages
184
task.add_tag("@" + message.GetSenderScreenName())
187
text = message.GetText()
189
#convert #hastags to @tags
190
matches = re.finditer("(?<![^|\s])(#\w+)", text)
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
195
for tag in self._extract_tags_from_text(text):
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])
203
task.add_remote_id(self.get_id(), str(message.GetId()))
205
def _is_tweet_syncable(self, tweet):
207
Returns True if the given tweet matches the user-specified tags to be
210
@param tweet: a tweet
212
if CoreConfig.ALLTASKS_TAG in self._parameters["import-tags"]:
215
tags = set(Backend._extract_tags_from_text(tweet.GetText()))
216
return tags.intersection(set(self._parameters["import-tags"])) \
220
def _extract_tags_from_text(text):
222
Given a string, returns a list of @tags and #hashtags
224
return list(re.findall(r'(?:^|[\s])((?:#|@)\w+)', text))
226
###############################################################################
227
### AUTHENTICATION ############################################################
228
###############################################################################
229
class controlled_execution(object):
231
This class performs the login to identica and execute the appropriate
232
response if something goes wrong during authentication or at network
236
def __init__(self, username, password, base_url, backend):
238
Sets the login parameters
240
self.username = username
241
self.password = password
242
self.backend = backend
243
self.base_url = base_url
247
Logins to identica and returns the Api object
249
return twitter.Api(self.username, self.password,
250
base_url = self.base_url)
252
def __exit__(self, type, value, traceback):
254
Analyzes the eventual exception risen during the connection to
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()
270
def signal_authentication_wrong(self):
271
self.backend.quit(disable = True)
272
BackendSignals().backend_failed(self.backend.get_id(),
273
BackendSignals.ERRNO_AUTHENTICATION)
275
def signal_network_down(self):
276
BackendSignals().backend_failed(self.backend.get_id(),
277
BackendSignals.ERRNO_NETWORK)