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
Twitter backend: imports direct messages, replies and/or the user timeline.
22
Authenticates through OAuth.
29
from webbrowser import open as openurl
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
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
45
class Backend(PeriodicImportBackend):
47
Twitter backend: imports direct messages, replies and/or the user timeline.
48
Authenticates through OAuth.
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" + \
63
"<b>my task title, task description #tag @anothertag</b>\n" + \
64
" Tags can be anywhere in the message"),\
67
_static_parameters = { \
69
GenericBackend.PARAM_TYPE: GenericBackend.TYPE_INT, \
70
GenericBackend.PARAM_DEFAULT_VALUE: 2, },
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, },
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"
91
def __init__(self, parameters):
93
See GenericBackend for an explanation of this function.
94
Re-loads the saved state of the synchronization
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" %\
100
self.sync_engine = self._load_pickled_file(self.data_path, \
102
#loading the parameters for oauth
103
self.auth_path = os.path.join('backends/twitter/', "auth-%s" %\
105
self.auth_params = self._load_pickled_file(self.auth_path, None)
106
self.authenticated = False
107
self.authenticating = False
109
def save_state(self):
111
See GenericBackend for an explanation of this function.
112
Saves the state of the synchronization.
114
self._store_pickled_file(self.data_path, self.sync_engine)
116
###############################################################################
117
### IMPORTING TWEETS ##########################################################
118
###############################################################################
119
def do_periodic_import(self):
121
See GenericBackend for an explanation of this function.
123
#abort if authentication is in progress or hasn't been done (in which
125
self.cancellation_point()
126
if not self.authenticated:
127
if not self.authenticating:
128
self._start_authentication()
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()
139
for tweet in tweets_to_import:
140
self._process_tweet(tweet)
142
def _process_tweet(self, tweet):
144
Given a tweet, checks if a task representing it must be
145
created in GTG and, if so, it creates it.
147
@param tweet: a tweet.
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
154
action, tid = self.sync_engine.analyze_remote_id(\
156
self.datastore.has_task, \
157
lambda tweet_id: True, \
159
Log.debug("processing tweet (%s, %s)" % (action, is_syncable))
161
self.cancellation_point()
162
if action == None or action == SyncEngine.UPDATE:
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, \
175
self.datastore.push_task(task)
177
elif action == SyncEngine.LOST_SYNCABILITY:
178
self.sync_engine.break_relationship(remote_id = tweet_id)
179
self.datastore.request_task_deletion(tid)
183
def _populate_task(self, task, message):
185
Given a twitter message and a GTG task, fills the task with the content
188
#adding the sender as a tag
189
#this works only for some messages types (not for the user timeline)
192
user = message.user.screen_name
196
task.add_tag("@" + user)
198
#setting title, text and tags
200
#convert #hastags to @tags
201
matches = re.finditer("(?<![^|\s])(#\w+)", text)
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
206
for tag in self._extract_tags_from_text(text):
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])
214
task.add_remote_id(self.get_id(), str(message.id))
216
def _is_tweet_syncable(self, tweet):
218
Returns True if the given tweet matches the user-specified tags to be
221
@param tweet: a tweet
223
if CoreConfig.ALLTASKS_TAG in self._parameters["import-tags"]:
226
tags = set(Backend._extract_tags_from_text(tweet.text))
227
return tags.intersection(set(self._parameters["import-tags"])) \
231
def _extract_tags_from_text(text):
233
Given a string, returns a list of @tags and #hashtags
235
return list(re.findall(r'(?:^|[\s])((?:#|@)\w+)', text))
237
###############################################################################
238
### AUTHENTICATION ############################################################
239
###############################################################################
240
def _start_authentication(self):
242
Fist step of authentication: opening the browser with the oauth page
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',
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
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")
269
#we have gone through authentication successfully before.
270
self.cancellation_point()
272
self.auth.set_access_token(self.auth_params[0],\
274
except tweepy.TweepError, e:
275
self._on_auth_error(e)
277
self.cancellation_point()
278
self._end_authentication()
280
def on_authentication_step(self, step_type = "", pin = ""):
282
Handles the various steps of authentication. It's the only callback
283
function the UI knows about this backend.
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"
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":
296
token = self.auth.get_access_token(verifier = pin)
297
except tweepy.TweepError, e:
298
self._on_auth_error(e)
300
self.auth_params = (token.key, token.secret)
301
self._store_pickled_file(self.auth_path, self.auth_params)
302
self._end_authentication()
304
def _end_authentication(self):
306
Last step of authentication. Creates the API objects and starts
309
self.authenticated = True
310
self.authenticating = False
311
self.api = tweepy.API(auth_handler = self.auth, \
314
self.cancellation_point()
315
self.start_get_tasks()
317
def _on_auth_error(self, exception):
319
On authentication error, informs the user.
321
@param exception: the Exception object that was raised during
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)
332
def signal_network_down(self):
334
If the network is unresponsive, inform the user
336
BackendSignals().backend_failed(self.get_id(), \
337
BackendSignals.ERRNO_NETWORK)