1
|
|
|
# -*- coding: utf-8 -*- |
2
|
|
|
from __future__ import absolute_import |
3
|
|
|
|
4
|
|
|
import twitter |
5
|
|
|
import logging |
6
|
|
|
|
7
|
|
|
from pyjobsweb.model import JobAlchemy |
8
|
|
|
from pyjobsweb.lib.helpers import get_job_url |
9
|
|
|
from pyjobsweb.lib.lock import acquire_inter_process_lock |
10
|
|
|
|
11
|
|
|
|
12
|
|
|
class TwitterBot(object): |
13
|
|
|
MAX_TWEET_LENGTH = 140 |
14
|
|
|
MAX_URL_LENGTH = 23 |
15
|
|
|
MAX_TWEETS_TO_PUSH = 250 |
16
|
|
|
|
17
|
|
|
def __init__(self, credentials): |
18
|
|
|
err_msg = '' |
19
|
|
|
exception = None |
20
|
|
|
|
21
|
|
|
self._logger = logging.getLogger(__name__) |
22
|
|
|
|
23
|
|
|
try: |
24
|
|
|
self._twitter_api = twitter.Api( |
25
|
|
|
consumer_key=credentials['consumer_key'], |
26
|
|
|
consumer_secret=credentials['consumer_secret'], |
27
|
|
|
access_token_key=credentials['access_token_key'], |
28
|
|
|
access_token_secret=credentials['access_token_secret'] |
29
|
|
|
) |
30
|
|
|
except twitter.TwitterError as exc: |
31
|
|
|
err_msg = 'The following error: %s, occurred while connecting ' \ |
32
|
|
|
'to the twitter API.' % exc.message |
33
|
|
|
exception = exc |
34
|
|
|
except KeyError as exc: |
35
|
|
|
err_msg = 'Malformed credentials dictionary: %s.' % exc.message |
36
|
|
|
exception = exc |
37
|
|
|
except Exception as exc: |
38
|
|
|
err_msg = 'An unhandled error: %s, occurred while connecting ' \ |
39
|
|
|
'to the twitter API.' % exc |
40
|
|
|
exception = exc |
41
|
|
|
|
42
|
|
|
if err_msg: |
43
|
|
|
logging.getLogger(__name__).log(logging.ERROR, err_msg) |
44
|
|
|
raise exception |
45
|
|
|
|
46
|
|
|
def _logging(self, logging_level, message): |
47
|
|
|
self._logger.log(logging_level, message) |
48
|
|
|
|
49
|
|
|
def _format_tweet(self, job_id, job_title): |
50
|
|
|
self._logging(logging.INFO, 'Formatting tweet.') |
51
|
|
|
# The Twitter API automatically shrinks URLs to 23 characters |
52
|
|
|
url = get_job_url(job_id, job_title, absolute=True) |
53
|
|
|
|
54
|
|
|
# Tweet format string |
55
|
|
|
tweet_format = u'%s. %s' |
56
|
|
|
|
57
|
|
|
# The number of punctuation characters in the tweet string format |
58
|
|
|
punctuation = len(tweet_format.replace(u'%s', u'')) |
59
|
|
|
|
60
|
|
|
total_length = len(job_title) + self.MAX_URL_LENGTH + punctuation |
61
|
|
|
|
62
|
|
|
# Make sure our tweet doesn't exceed max_length |
63
|
|
|
if total_length > self.MAX_TWEET_LENGTH: |
64
|
|
|
diff = total_length - self.MAX_TWEET_LENGTH |
65
|
|
|
job_title = job_title[:-diff] |
66
|
|
|
|
67
|
|
|
# Return the formatted tweet |
68
|
|
|
return tweet_format % (job_title, url) |
69
|
|
|
|
70
|
|
|
def _push_job_offers_to_twitter(self, num_tweets_to_push): |
71
|
|
|
# Do not push every job offer at once. The Twitter API won't allow it. |
72
|
|
|
# We thus push them num_offers_to_push at a time. |
73
|
|
|
if num_tweets_to_push > self.MAX_TWEETS_TO_PUSH: |
74
|
|
|
err_msg = 'Cannot push %s tweets at once, pushing %s tweets ' \ |
75
|
|
|
'instead.' % (num_tweets_to_push, self.MAX_TWEETS_TO_PUSH) |
76
|
|
|
self._logging(logging.WARNING, err_msg) |
77
|
|
|
|
78
|
|
|
num_tweets_to_push = self.MAX_TWEETS_TO_PUSH |
79
|
|
|
|
80
|
|
|
self._logging(logging.INFO, 'Acquiring unpublished job offers.') |
81
|
|
|
to_push = JobAlchemy.get_not_pushed_on_twitter(num_tweets_to_push) |
82
|
|
|
|
83
|
|
|
for job_offer in to_push: |
84
|
|
|
tweet = self._format_tweet(job_offer.id, job_offer.title) |
85
|
|
|
|
86
|
|
|
try: |
87
|
|
|
self._logging(logging.INFO, 'Publishing to Twitter.') |
88
|
|
|
self._twitter_api.PostUpdate(tweet) |
89
|
|
|
except twitter.TwitterError as exc: |
90
|
|
|
err_msg = '[Job offer id: %s] The following error: %s, ' \ |
91
|
|
|
'occurred while pushing the following tweet: %s.' \ |
92
|
|
|
% (job_offer.id, exc.message, tweet) |
93
|
|
|
self._logging(logging.WARNING, err_msg) |
94
|
|
|
except Exception as exc: |
95
|
|
|
err_msg = '[Job offer id: %s] An unhandled error: %s, ' \ |
96
|
|
|
'occurred while pushing the following tweet: %s.' \ |
97
|
|
|
% (job_offer.id, exc, tweet) |
98
|
|
|
self._logging(logging.ERROR, err_msg) |
99
|
|
|
else: |
100
|
|
|
# The tweet has been pushed successfully. Mark the job offer as |
101
|
|
|
# pushed on Twitter in the Postgresql database, so we don't push |
102
|
|
|
# it again on Twitter later on. |
103
|
|
|
self._logging(logging.INFO, 'Marking as published on Twitter.') |
104
|
|
|
JobAlchemy.set_pushed_on_twitter(job_offer.id, True) |
105
|
|
|
|
106
|
|
|
def run(self, num_tweets_to_push): |
107
|
|
|
self._logging(logging.INFO, 'Starting the Twitter bot.') |
108
|
|
|
|
109
|
|
|
with acquire_inter_process_lock('twitter_bot') as acquired: |
110
|
|
|
if not acquired: |
111
|
|
|
err_msg = 'Another instance of the Twitter bot is already ' \ |
112
|
|
|
'running, aborting now.' |
113
|
|
|
self._logging(logging.WARNING, err_msg) |
114
|
|
|
else: |
115
|
|
|
self._push_job_offers_to_twitter(num_tweets_to_push) |
116
|
|
|
|