|
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
|
|
|
|