Ejemplo n.º 1
0
 def test_calc_expected_status_length_with_wide_unicode(self):
     status = "…"
     len_status = calc_expected_status_length(status)
     assert len_status == 2
     status = "……"
     len_status = calc_expected_status_length(status)
     assert len_status == 4
Ejemplo n.º 2
0
def format_tweets(payload):
    """
    Split plain text content into a list.

    The length of every element can't be higher than CHARACTER_LIMIT (280 characters).

    The formatting rules defined here are:

    1. The first element (even if there's only one) must include its attribution (link and author) at the end.
       It must be at the end because Twitter only generated a card preview for the last link in a tweet.
    2. If there's more than 1 tweet, all the tweets include a placeholder ([…]) to indicate the continuation,
       except the last one.

    For more examples take a look at the test suite.
    """
    content = payload["plain"]
    content = " ".join(content.split())
    content = unescape(content)
    ending = """ {link} shared by @{username}""".format(
        link=payload["link"], username=payload["twitter_username"])

    if "hashtags" in payload and payload["hashtags"]:
        # hashtags: "#hash1,#hash2"
        # result: "#hash1 #hash2"
        ending = ending + " " + payload["hashtags"].replace(" ", "").replace(
            ",", " ")

    placeholder = " […]"
    current_line_length = 0
    line = []
    tweets = []
    words = re.split(r"\s", content)

    for word in words:
        current_line_length += word_length(word) + 1

        if len(tweets) == 0:
            if current_line_length > (
                    CHARACTER_LIMIT -
                    calc_expected_status_length(placeholder + ending)):
                tweets.append(" ".join(line) + placeholder + ending)
                line = [word]
                current_line_length = word_length(word)
            else:
                line.append(word)
        else:
            if current_line_length > (
                    CHARACTER_LIMIT -
                    calc_expected_status_length(placeholder)):
                tweets.append(" ".join(line) + placeholder)
                line = [word]
                current_line_length = word_length(word)
            else:
                line.append(word)
    if len(tweets) == 0:
        tweets.append(" ".join(line) + ending)
    else:
        tweets.append(" ".join(line))
    return tweets
Ejemplo n.º 3
0
def truncate_status(status):
    if twitter_utils.calc_expected_status_length(status) <= 280:
        return status
    else:
        while twitter_utils.calc_expected_status_length(status) > 278:
            status = status[:len(status)-1]

        status += '…'
        return status
Ejemplo n.º 4
0
def thread_parser(s, **options):
    """
    Splits the thread by delimiter + \n, then processes that thread by parsing out media paths
    :type s: str
    :rtype: List[status object]
    """
    base_parsed_thread = s.split(options['d'] + '\n')
    base_parsed_thread = list(map(lambda e: e.strip(), base_parsed_thread))
    status = []

    for tweet in base_parsed_thread:
        if len(tweet) > 0:
            status.append(tweet_parser(tweet))
        #debug
        #print (calc_expected_status_length(tweet))

    invalid_lengths = list(
        filter(lambda e: calc_expected_status_length(e.tweet) > 280, status))

    if len(invalid_lengths) != 0:
        for bad_tweet in invalid_lengths:
            print(bad_tweet.tweet)
        raise Exception('Above tweets have invalid lengths')

    return status
    async def split_tweet_content(self, content, counter=None, counter_max=None, tweet_id=None):
        # add twitter counter at the beginning
        if counter is not None and counter_max is not None:
            content = f"{counter}/{counter_max} {content}"
            counter += 1

        # get the current content size
        post_size = calc_expected_status_length(content)

        # check if the current content size can be posted
        if post_size > CHARACTER_LIMIT:

            # calculate the number of post required for the whole content
            if not counter_max:
                counter_max = post_size // CHARACTER_LIMIT
                counter = 1

            # post the current tweet
            post = self.twitter_api.PostUpdate(status=content[:CHARACTER_LIMIT], in_reply_to_status_id=tweet_id)

            # recursive call for all post while content > CHARACTER_LIMIT
            await self.split_tweet_content(content[CHARACTER_LIMIT:],
                                           counter=counter,
                                           counter_max=counter_max,
                                           tweet_id=tweet_id)

            return post
        else:
            return self.twitter_api.PostUpdate(status=content[:CHARACTER_LIMIT], in_reply_to_status_id=tweet_id)
Ejemplo n.º 6
0
def test_tweet(self):
    consumer_key = settings.TWITTER_CONSUMER_KEY
    consumer_secret = settings.TWITTER_CONSUMER_SECRET
    access_key = settings.TWITTER_ACCESS_TOKEN_KEY
    access_secret = settings.TWITTER_ACCESS_TOKEN_SECRET
    if any(not i for i in [
            consumer_key,
            consumer_secret,
            access_key,
            access_secret,
    ]):
        LOG.warning("Twitter API credentials not set!")
        return
    api = ThreadedApi(
        consumer_key=consumer_key,
        consumer_secret=consumer_secret,
        access_token_key=access_key,
        access_token_secret=access_secret,
    )
    message = "This is a test long tweet\r\n{}".format("\r\n".join(
        f"This is line {line}" for line in range(2, 30)))
    try:
        if calc_expected_status_length(message) > CHARACTER_LIMIT:
            api.PostUpdates(message, continuation="…", threaded=True)
        else:
            api.PostUpdate(message)
    except TwitterError as exc:
        LOG.warning("Hit twitter error: %s", exc)
        if str(exc) in RETRYABLE_ERRORS:
            raise self.retry(exc=exc)
        delay = get_twitter_rate_limit_delay(api)
        if delay is None:
            LOG.error("No idea what to do with twitter error %s", exc)
            raise
        raise self.retry(countdown=delay, exc=exc)
Ejemplo n.º 7
0
def send_tweet_entrance(message, file=None):
    if file is None:
        # normal text tweet, could be too long
        text = message.text
        if calc_expected_status_length(text) > CHARACTER_LIMIT:
            # pre calculation
            is_long = False
            for part in re.split(r"\n+", message.text):
                if calc_expected_status_length(part) > CHARACTER_LIMIT:
                    logging.warning("%s chars for thread tweet!",
                                    calc_expected_status_length(part))
                    bot.reply_to(message,
                                 f"`{part}` is too long",
                                 parse_mode="markdown")
                    is_long = True
            if is_long:
                return
            # normal process
            url = ""
            first_thread = []
            for part in re.split(r"\n+", message.text):
                new_message = copy.copy(message)
                if url:
                    replied = types.Message.de_json(reply_json % url)
                    setattr(new_message, "reply_to_message", replied)

                new_message.text = part
                result = send_tweet(new_message)
                first_thread.append(result)
                url = tweet_format.format(screen_name="abc", id=result["id"])
            # send response
            send_tweet_message(first_thread[0], message)
            return
        else:
            result = send_tweet(message)
    elif isinstance(file, list):
        result = send_tweet(message, file)
        [f.close() for f in file]
    else:
        # one image message, file object
        result = send_tweet(message, [file])
        file.close()

    send_tweet_message(result, message)
Ejemplo n.º 8
0
def tweet_too_big(tweet):
    '''
    Verify if the message respects Twitter's limitation of characters
    :param tweet: (string) the message the user wants to publish on Twitter
    :return: True if the message is longer than 280 characters, False otherwise
    '''
    if twitter_utils.calc_expected_status_length(tweet) > 280:
        return True
    else:
        return False
Ejemplo n.º 9
0
def split_tweet(tweet, start_sep=TWEET_TBC, end_sep=TWEET_TBC):
  toks = tweet.split(' ')
  safety_len = len(end_sep) + 5

  cur_tweet = ''
  prev_tok = None
  tweets = []
  for tok in toks:
    extended_tweet = cur_tweet
    if prev_tok is not None and prev_tok != '\n':
      extended_tweet += ' '
    extended_tweet += tok
    prev_tok = tok

    if twitter_utils.calc_expected_status_length(extended_tweet) + safety_len > MAX_TWEET_LEN:
      tweets.append(cur_tweet + end_sep)
      cur_tweet = start_sep + tok
    else:
      cur_tweet = extended_tweet

  tweets.append(cur_tweet)
  return tweets
Ejemplo n.º 10
0
def send_tweets(tweets):
    """
    Publish tweets as a thread.
    """
    total = len(tweets)
    for i, tweet in enumerate(tweets):
        length = calc_expected_status_length(tweet)
        nro = i + 1
        print("Tweet {nro}/{total} ({length} chars):\n{tweet}".format(
            nro=nro, total=total, length=length, tweet=tweet))

    api = twitter.Api(
        consumer_key=CONSUMER_KEY,
        consumer_secret=CONSUMER_SECRET,
        access_token_key=ACCESS_TOKEN_KEY,
        access_token_secret=ACCESS_TOKEN_SECRET,
    )
    last_reply_to_id = None
    for tweet in tweets:
        status = api.PostUpdate(tweet, in_reply_to_status_id=last_reply_to_id)
        last_reply_to_id = status.id
    return
Ejemplo n.º 11
0
def remain_char(tweet: str) -> str:
    length = calc_expected_status_length(tweet)
    return f"{length}/{CHARACTER_LIMIT}. Click this message to tweet."
Ejemplo n.º 12
0
    def tweet(self):
        """ Create message, reduce it until it fits in a tweet, and then tweet
        it with a link to Google maps and tweet location included.
        """
        def generate_tag_string(hashtags):
            '''create hashtag string'''
            tag_string = ''
            if hashtags:
                for hashtag in hashtags:
                    tag_string += ' #{}'.format(hashtag)
            return tag_string

        try:
            api = twitter.Api(consumer_key=config.TWITTER_CONSUMER_KEY,
                              consumer_secret=config.TWITTER_CONSUMER_SECRET,
                              access_token_key=config.TWITTER_ACCESS_KEY,
                              access_token_secret=config.TWITTER_ACCESS_SECRET)
        except Exception:
            self.logger.exception('Failed to create a Twitter API object.')

        tag_string = generate_tag_string(self.hashtags)

        if self.expire_time:
            tweet_text = (
                'A {d} {n} appeared! It will be {p} until {e}. {t} {u}'
            ).format(d=self.description,
                     n=self.name,
                     p=self.place,
                     e=self.expire_time,
                     t=tag_string,
                     u=self.map_link)
        else:
            tweet_text = (
                'A {d} {n} appeared {p}! It will expire sometime between '
                '{e1} and {e2}. {t} {u}').format(d=self.description,
                                                 n=self.name,
                                                 p=self.place,
                                                 e1=self.min_expire_time,
                                                 e2=self.max_expire_time,
                                                 t=tag_string,
                                                 u=self.map_link)

        if calc_expected_status_length(tweet_text) > 140:
            tweet_text = tweet_text.replace(' meters ', 'm ')

        # remove hashtags until length is short enough
        while calc_expected_status_length(tweet_text) > 140:
            if self.hashtags:
                hashtag = self.hashtags.pop()
                tweet_text = tweet_text.replace(' #' + hashtag, '')
            else:
                break

        if (calc_expected_status_length(tweet_text) > 140
                and self.landmark.shortname):
            tweet_text = tweet_text.replace(self.landmark.name,
                                            self.landmark.shortname)

        if calc_expected_status_length(tweet_text) > 140:
            place = self.landmark.shortname or self.landmark.name
            phrase = self.landmark.phrase
            if self.place.startswith(phrase):
                place_string = '{ph} {pl}'.format(ph=phrase, pl=place)
            else:
                place_string = 'near {}'.format(place)
            tweet_text = tweet_text.replace(self.place, place_string)

        if calc_expected_status_length(tweet_text) > 140:
            if self.expire_time:
                tweet_text = 'A {d} {n} will be {p} until {e}. {u}'.format(
                    d=self.description,
                    n=self.name,
                    p=place_string,
                    e=self.expire_time,
                    u=self.map_link)
            else:
                tweet_text = (
                    "A {d} {n} appeared {p}! It'll expire between {e1} & {e2}."
                    ' {u}').format(d=self.description,
                                   n=self.name,
                                   p=place_string,
                                   e1=self.min_expire_time,
                                   e2=self.max_expire_time,
                                   u=self.map_link)

        if calc_expected_status_length(tweet_text) > 140:
            if self.expire_time:
                tweet_text = 'A {d} {n} will expire at {e}. {u}'.format(
                    n=self.name, e=self.expire_time, u=self.map_link)
            else:
                tweet_text = (
                    'A {d} {n} will expire between {e1} & {e2}. {u}').format(
                        d=self.description,
                        n=self.name,
                        e1=self.min_expire_time,
                        e2=self.max_expire_time,
                        u=self.map_link)

        image = None
        if config.TWEET_IMAGES:
            try:
                image = PokeImage(self.pokemon_id, self.iv, self.moves,
                                  self.time_of_day).create()
            except Exception:
                self.logger.exception('Failed to create a Tweet image.')

        try:
            api.PostUpdate(tweet_text,
                           media=image,
                           latitude=self.coordinates[0],
                           longitude=self.coordinates[1],
                           display_coordinates=True)
        except Exception:
            self.logger.exception('Failed to Tweet about {}.'.format(
                self.name))
            return False
        else:
            self.logger.info('Sent a tweet about {}.'.format(self.name))
            return True
        finally:
            try:
                image.close()
            except AttributeError:
                pass
Ejemplo n.º 13
0
 def test_calc_expected_status_length_with_url_and_extra_spaces(self):
     status = 'hi a tweet          there               example.com'
     len_status = calc_expected_status_length(status)
     self.assertEqual(len_status, 63)
Ejemplo n.º 14
0
def tweet_about_beers(self, beer_pks):
    if not beer_pks:
        LOG.warning("nothing to do")
        return
    LOG.debug("Tweeting about beer PKs: %s", beer_pks)
    consumer_key = settings.TWITTER_CONSUMER_KEY
    consumer_secret = settings.TWITTER_CONSUMER_SECRET
    access_key = settings.TWITTER_ACCESS_TOKEN_KEY
    access_secret = settings.TWITTER_ACCESS_TOKEN_SECRET
    if any(not i for i in [
            consumer_key,
            consumer_secret,
            access_key,
            access_secret,
    ]):
        LOG.warning("Twitter API credentials not set!")
        return
    api = ThreadedApi(
        consumer_key=consumer_key,
        consumer_secret=consumer_secret,
        access_token_key=access_key,
        access_token_secret=access_secret,
    )
    # Mark beers which have been removed from the tap list as tweeted about
    Beer.objects.filter(
        tweeted_about=False,
        taps__isnull=True,
    ).update(tweeted_about=True)
    beers = list(
        Beer.objects.filter(
            id__in=beer_pks,
            tweeted_about=False,
        ).select_related(
            "manufacturer",
            "style",
        ).prefetch_related(
            Prefetch(
                "taps",
                queryset=Tap.objects.select_related("venue"),
            ), ).order_by("id"))
    already_tweeted_about = set(i.id for i in Beer.objects.filter(
        tweeted_about=True,
        id__in=beer_pks,
    ))
    unknown_pks = set(beer_pks).difference(already_tweeted_about)
    LOG.debug("Got %s beers", len(beers))
    if not beers:
        if unknown_pks:
            LOG.warning("No beers found! Trying again shortly")
            raise self.retry(countdown=300)
        LOG.info("everything was already tweeted about. No big deal")
        return
    if len(beers) > 10:
        LOG.info("Too many beers to tweet about at once: %s", len(beers))
        beers = beers[:10]
    if len(beers) == 1:
        beer = beers[0]
        if beer.tweeted_about:
            LOG.info("%s has already been tweeted about; skipping.", beer)
            return
        message = format_beer(beer, SINGLE_BEER_TEMPLATE).strip()
        messages = [message]
    else:
        extra_beers = len(beer_pks) - len(beers) - len(already_tweeted_about)
        messages = [
            MULTI_BEER_OUTER.format(
                len(beers),
                "({} still to come!)".format(extra_beers)
                if extra_beers > 0 else "",
            ).strip()
        ] + format_beers(beers)
        if len(messages) == 1:
            LOG.info("All beers already tweeted about")
            return
    message = "\r\n".join(messages)
    LOG.info("Going to tweet: %s", message)

    try:
        if calc_expected_status_length(message) > CHARACTER_LIMIT:
            api.PostUpdates(message, continuation="…", threaded=True)
        else:
            api.PostUpdate(message)
    except TwitterError as exc:
        LOG.warning("Hit twitter error: %s", exc)
        if str(exc) in RETRYABLE_ERRORS:
            raise self.retry(exc=exc)
        LOG.error("Tweet(s) that caused error was %s", message)
        delay = get_twitter_rate_limit_delay(api)
        if delay is None:
            LOG.error("No idea what to do with twitter error %s", exc)
            raise
        raise self.retry(countdown=delay, exc=exc)
    Beer.objects.filter(id__in=[i.id
                                for i in beers]).update(tweeted_about=True)
    LOG.debug("Done tweeting")
Ejemplo n.º 15
0
def tweet_about_beers(self, beer_pks):
    if not beer_pks:
        LOG.warning('nothing to do')
        return
    LOG.debug('Tweeting about beer PKs: %s', beer_pks)
    consumer_key = settings.TWITTER_CONSUMER_KEY
    consumer_secret = settings.TWITTER_CONSUMER_SECRET
    access_key = settings.TWITTER_ACCESS_TOKEN_KEY
    access_secret = settings.TWITTER_ACCESS_TOKEN_SECRET
    if any(not i for i in [
            consumer_key,
            consumer_secret,
            access_key,
            access_secret,
    ]):
        LOG.warning('Twitter API credentials not set!')
        return
    api = ThreadedApi(
        consumer_key=consumer_key,
        consumer_secret=consumer_secret,
        access_token_key=access_key,
        access_token_secret=access_secret,
    )
    beers = list(
        Beer.objects.filter(
            id__in=beer_pks,
            tweeted_about=False,
        ).select_related(
            'manufacturer',
            'style',
        ).prefetch_related(
            Prefetch(
                'taps',
                queryset=Tap.objects.select_related('venue'),
            ), ).order_by('id'))
    already_tweeted_about = set(i.id for i in Beer.objects.filter(
        tweeted_about=True,
        id__in=beer_pks,
    ))
    unknown_pks = set(beer_pks).difference(already_tweeted_about)
    LOG.debug('Got %s beers', len(beers))
    if not beers:
        if unknown_pks:
            LOG.warning('No beers found! Trying again shortly')
            raise self.retry(countdown=300)
        LOG.info('everything was already tweeted about. No big deal')
        return
    if len(beers) > 10:
        LOG.info('Too many beers to tweet about at once: %s', len(beers))
        beers = beers[:10]
    if len(beers) == 1:
        beer = beers[0]
        if beer.tweeted_about:
            LOG.info('%s has already been tweeted about; skipping.', beer)
            return
        message = format_beer(beer, SINGLE_BEER_TEMPLATE).strip()
        messages = [message]
    else:
        extra_beers = len(beer_pks) - len(beers) - len(already_tweeted_about)
        messages = [
            MULTI_BEER_OUTER.format(
                len(beers), '({} still to come!)'.format(extra_beers)
                if extra_beers > 0 else '').strip()
        ] + format_beers(beers)
        if len(messages) == 1:
            LOG.info('All beers already tweeted about')
            return
    message = '\r\n'.join(messages)
    LOG.info('Going to tweet: %s', message)

    try:
        if calc_expected_status_length(message) > CHARACTER_LIMIT:
            api.PostUpdates(message, continuation='…', threaded=True)
        else:
            api.PostUpdate(message)
    except TwitterError as exc:
        LOG.warning('Hit twitter error: %s', exc)
        if str(exc) in RETRYABLE_ERRORS:
            raise self.retry(exc=exc)
        delay = get_twitter_rate_limit_delay(api)
        if delay is None:
            LOG.error('No idea what to do with twitter error %s', exc)
            raise
        raise self.retry(countdown=delay, exc=exc)
    Beer.objects.filter(id__in=[i.id
                                for i in beers]).update(tweeted_about=True)
    LOG.debug('Done tweeting')
Ejemplo n.º 16
0
 def test_calc_expected_status_length_with_url_and_extra_spaces(self):
     status = 'hi a tweet          there               example.com'
     len_status = calc_expected_status_length(status)
     self.assertEqual(len_status, 63)
Ejemplo n.º 17
0
 def test_calc_expected_status_length_with_url(self):
     status = 'hi a tweet there example.com'
     len_status = calc_expected_status_length(status)
     self.assertEqual(len_status, 40)
Ejemplo n.º 18
0
 def test_calc_expected_status_length(self):
     status = 'hi a tweet there'
     len_status = calc_expected_status_length(status)
     self.assertEqual(len_status, 16)
Ejemplo n.º 19
0
 def test_calc_expected_status_length_with_url(self):
     status = 'hi a tweet there example.com'
     len_status = calc_expected_status_length(status)
     self.assertEqual(len_status, 40)
Ejemplo n.º 20
0
 def test_calc_expected_status_length(self):
     status = 'hi a tweet there'
     len_status = calc_expected_status_length(status)
     self.assertEqual(len_status, 16)