def akari_publish(text, **kwargs): from twitter import twitter akari = Akari(text, **kwargs) if cfg('twitter:text_in_status:bool'): twitter.post(status=akari.caption, media=akari.filename) else: twitter.post(media=akari.filename)
def unupdate(id): resp = twitter.post('statuses/destroy/%d.json' % id) if resp.status != 200: for error in resp.data['errors']: flash(error['message']) else: flash('Successfully deleted your new tweet') return redirect(url_for('show_index'))
def delete(): if g.user is None: return redirect(url_for("login", next=request.url)) ids = request.form.getlist("tweet_id") if not ids: return redirect(url_for("index")) for tweet_id in ids: resp = twitter.post("statuses/destroy/{0}.json".format(tweet_id)) if resp.status != 200: flash("There was an error deleting tweet!") return render_template("delete.html")
def unblock(id): resp = twitter.post('blocks/destroy.json', data={'user_id': id}) if resp.status == 401: session.pop('twitter_token') flash('Unauthorized account access.') return redirect(url_for('show_index')) if resp.status != 200: for error in resp.data['errors']: flash(error['message']) else: flash('Successfully unblocked.') return redirect(request.referrer or url_for('show_index'))
def follow(id): resp = twitter.post('friendships/create.json', data={'user_id': id}) if resp.status == 401: session.pop('twitter_token') flash('Unauthorized account access.') return redirect(url_for('show_index')) if resp.status != 200: for error in resp.data['errors']: flash(error['message']) else: flash('Successfully followed, or your request has been sent.') return redirect(request.referrer or url_for('show_index'))
def favorite(id): resp = twitter.post('favorites/create.json', data={'id': id}) if resp.status == 401: session.pop('twitter_token') flash('Unauthorized account access.') return redirect(url_for('show_index')) if resp.status != 200: for error in resp.data['errors']: flash(error['message']) else: flash('Successfully favorited.') return redirect(request.referrer or url_for('show_index'))
def retweet(id): resp = twitter.post('statuses/retweet/%d.json' % id) if resp.status == 401: session.pop('twitter_token') flash('Unauthorized account access.') return redirect(url_for('show_index')) if resp.status != 200: for error in resp.data['errors']: flash(error['message']) else: flash('Successfully retweeted.') return redirect(request.referrer or url_for('show_index'))
def unfavorite(id): resp = twitter.post('favorites/destroy.json', data={'id': id}) if resp.status == 401: session.pop('twitter_token') flash('Unauthorized account access.') return redirect(url_for('show_index')) if resp.status != 200: for error in resp.data['errors']: flash(error['message']) else: flash('Successfully unfavorited.') return redirect(request.referrer or url_for('show_index'))
def update(): resp = twitter.post('statuses/update.json', data={ 'status': request.form['status'], 'in_reply_to_status_id': request.form['in_reply_to'] }) if resp.status == 401: session.pop('twitter_token') flash('Unauthorized account access.') return redirect(url_for('show_index')) if resp.status != 200: for error in resp.data['errors']: flash(error['message']) else: flash('Successfully tweeted your new status') return redirect(url_for('show_index'))
def unretweet(id): resp = twitter.get('statuses/show/%d.json?include_my_retweet=1' % id) if resp.status == 401: session.pop('twitter_token') flash('Unauthorized account access.') return redirect(url_for('show_index')) if resp.status != 200: for error in resp.data['errors']: flash(error['message']) else: retweet_id = resp.data['current_user_retweet']['id'] resp = twitter.post('statuses/destroy/%d.json' % retweet_id) if resp.status == 200: flash('Successfully unretweeted.') else: flash('Unretweet failed.') return redirect(request.referrer or url_for('show_index'))
def akari_cron(): Akari.warmup() # if there's an override, try to post it, but if it fails, continue # normally. try: cron_override = cfg('twitter:cron_override') if cron_override and akari_cron_override(cron_override): return except Exception: pass from twitter import twitter ids = [] # get a random line. will error out if there are none, which is okay. with open('pending.txt', errors='replace') as file: for line in file.read().splitlines(): id_, text = line.split(' ', 1) # if the blacklist is enabled, ignore tweets that match it blacklist = cfg('twitter:text_blacklist:re_list') if any(x.search(text) for x in blacklist): continue # alright, this tweet is a candidate ids.append(id_) # this function generates a score for each tweet def score(status): favs = status.favorite_count rts = status.retweet_count followers = status.user.followers_count if (followers == 0 or # avoid division by zero later on status.user.protected): # don't post from protected users return -1 # decay coefficient. promotes newer tweets to compensate for the # lower amount of favs they have received (fewer people have seen # them, in theory) diff = (datetime.utcnow() - status.created_at).total_seconds() score = utils.decay(diff, cfg('twitter:cron_interval:int') * 60, 1.5) score *= (favs + rts * 2) / followers # apply penalties. some tweets carry a penalty but are not removed # right away, in case there isn't anything better. # at least 80% of letters in the status must be /a-zA-Z/ clean_text = utils.clean(status.text, urls=True, replies=True, rts=True) meat = sum(c in string.ascii_letters for c in clean_text) or -1 # also, get the author's lang and filter him if it's not in the wl lang_wl = cfg('twitter:cron_lang_whitelist:list') # filter quoted tweets filter_quotes = cfg('twitter:cron_filter_quotes:bool') if ((lang_wl and status.user.lang not in lang_wl) or (filter_quotes and status.is_quote_status) or meat / len(clean_text) < 0.8 or followers < (followers_median * 1.5) or favs < favs_median): score /= 10 return score # 100 at a time is the max statuses_lookup() can do. statuses = [] for i in range(0, len(ids), 100): group = ids[i:i + 100] statuses.extend(tuple(twitter.api.statuses_lookup(group))) followers_median = statistics.median(status.user.followers_count for status in statuses) favs_median = statistics.median(status.favorite_count for status in statuses if status.favorite_count > 0) statuses.sort(key=score, reverse=True) # try to generate an image for the first status. if that fails, keep # trying with the next one until you have succeeded or until you have # run out of attempts. for status in statuses[:10]: try: caption = utils.clean(status.text, urls=True, replies=True, rts=True) utils.logger.info('Posting "%s" from %s', caption, twitter.status_to_url(status)) akari = Akari(caption, type='animation', shuffle_results=False) break except Exception: utils.logger.exception('Error generating a caption.') continue # this will crash it there's no caption available thus far, that's fine, # as the amount of tries has been exceeded and there was nothing left to do # anyway. if cfg('twitter:text_in_status:bool'): twitter.post(status=akari.caption, media=akari.filename) else: twitter.post(media=akari.filename) # if a new caption has been successfully published, empty the file with open('pending.txt', 'w'): pass
def process_request(queue): request_blacklist = cfg('twitter:request_blacklist:re_list') user_images = cfg('twitter:user_images:bool') delete_triggers = cfg('twitter:delete_triggers:re_list') load_avg_still = cfg('twitter:load_avg_still:int') no_results_image = cfg('twitter:no_results_image') error_image = cfg('twitter:error_image') def process_self_delete(status): if not status.in_reply_to_status_id: utils.logger.warning('This status has no "in reply to" field.') return False try: status_del = twitter.api.get_status(status.in_reply_to_status_id) except tweepy.error.TweepError: utils.logger.exception('Failed to get the status pointed by ' 'the "in reply to" field.') return False if not status_del.text.startswith('@%s ' % status.user.screen_name): utils.logger.warning('The status pointed by the "in reply to" ' "wasn't in reply to a status made by the " 'user who requested the removal.') return False try: twitter.api.destroy_status(status_del.id) except tweepy.error.TweepError: utils.logger.exception('Failed to remove the status pointed by ' 'the "in reply to" field.') return False else: utils.logger.info('Deleted: %d "%s"', status_del.id, status_del.text) return True while True: status = queue.get() print_status(status) text = utils.clean(status.text, urls=True, replies=True, rts=True) # see if the text in this request is blacklisted. if so do nothing. if (request_blacklist and any(x.search(text) for x in request_blacklist)): utils.logger.warning('Text is blacklisted, request ignored') queue.task_done() continue # see if there's an image (and if that's allowed) image_url = None try: if user_images: image_url = status.entities['media'][0]['media_url'] + ':orig' except KeyError: pass # if there's a user-provided image but there's no text and we are # generating still images, don't do anything at all (in this case, # we would just copy the image around without doing anything useful) if image_url and not text and len(cache.get('akari:frames')) < 2: utils.logger.warning('Refusing to generate a still image from a ' 'still image') queue.task_done() continue # if after being cleaned up the status turns out to be empty and # there's no image, return if not text and not image_url: utils.logger.info('No text and no image. Nothing to do.') queue.task_done() continue if (delete_triggers and any(x.search(text) for x in delete_triggers)): if process_self_delete(status): queue.task_done() continue # if removal is not successful, we will generate a caption. # apply a strict ratelimit to people with fewer than 25 followers rate_limit_slow = utils.ratelimit_hit('twitter', 'global_slow', 5, 60) if (status.author.followers_count < 25 and not rate_limit_slow['allowed']): utils.logger.info('%d - Ignoring because of low follower count', status.id) queue.task_done() continue # apply a lax ratelimit to the rest of users rate_limit = utils.ratelimit_hit('twitter', 'global', 20, 60) if not rate_limit['allowed']: utils.logger.info('%d - Ignoring because of ratelimit', status.id) queue.task_done() continue # so we'll generate something for this guy... # follow the user if he's new. if he does not follow back, he'll # be unfollowed by followers.unfollow_my_unfollowers sometime later. if is_eligible(status.author): try: twitter.api.create_friendship(status.author.screen_name) except tweepy.error.TweepError: pass # if the one-minute load avg is greater than load_avg_still, generate # still captions try: load_avg = os.getloadavg()[0] if load_avg_still and load_avg > load_avg_still: utils.logger.warning('Load average too high! (%i > %i)', load_avg, load_avg_still) akari_type = 'still' else: akari_type = 'animation' except KeyError: pass error = False try: akari = Akari(text, type=akari_type, shuffle_results=True, image_url=image_url) text = akari.caption image = akari.filename except ImageSearchNoResultsError: utils.logger.exception('No results') msgs = ('I found nothing.', 'No results.', "I didn't find anything.", 'There are no results.') text = random.choice(msgs) image = no_results_image error = True except KeyboardInterrupt: raise except Exception: utils.logger.exception('Error composing the image') msgs = ("Can't hear ya...", "Ooops, I'm busy at the moment.", "I don't feel so well right now.", 'Sorry, I fell asleep.') text = '%s Try again a bit later.' % random.choice(msgs) image = error_image error = True # start building a reply. prepend @nick of whoever we are replying to if cfg('twitter:text_in_status:bool') or error: reply = '@%s %s' % (status.author.screen_name, text) else: reply = '@%s' % (status.author.screen_name) # post it try: twitter.post(status=reply, media=image, in_reply_to_status_id=status.id) except KeyboardInterrupt: raise except tweepy.error.TweepError as exc: utils.logger.exception('Error posting.') if exc.api_code == 326: # account temporarily locked twitter.handle_exception(exc) except Exception: utils.logger.exception('Error posting.') queue.task_done()