From 18e900dc49ecc9300c429f1f11bafb811e3a2bac Mon Sep 17 00:00:00 2001 From: davek Date: Wed, 28 May 2025 20:17:29 -0700 Subject: [PATCH] docs: note tweet thread auto split --- README.md | 1 + services/runner/tasks/tweet_task.py | 86 ++++++++++++++++------------- 2 files changed, 49 insertions(+), 38 deletions(-) diff --git a/README.md b/README.md index fdf9c62e..a49ca762 100644 --- a/README.md +++ b/README.md @@ -73,6 +73,7 @@ aibtcdev-backend/ ### 3. Social Media Integration - Twitter automation and monitoring +- Tweets longer than 280 characters are automatically threaded - Telegram bot integration - Discord notifications - Automated content generation diff --git a/services/runner/tasks/tweet_task.py b/services/runner/tasks/tweet_task.py index 394dd3a1..b8da3a2b 100644 --- a/services/runner/tasks/tweet_task.py +++ b/services/runner/tasks/tweet_task.py @@ -42,6 +42,22 @@ def __init__(self, config: Optional[RunnerConfig] = None): self._pending_messages: Optional[List[QueueMessage]] = None self.twitter_service = None + def _split_text_into_chunks(self, text: str, limit: int = 280) -> List[str]: + """Split text into chunks not exceeding the limit without cutting words.""" + words = text.split() + chunks = [] + current = "" + for word in words: + if len(current) + len(word) + (1 if current else 0) <= limit: + current = f"{current} {word}".strip() + else: + if current: + chunks.append(current) + current = word + if current: + chunks.append(current) + return chunks + def _get_extension(self, url: str) -> str: path = urlparse(url).path.lower() for ext in [".png", ".jpg", ".jpeg", ".gif"]: @@ -56,9 +72,7 @@ def _post_tweet_with_media( reply_id: Optional[str] = None, ): try: - headers = { - "User-Agent": "Mozilla/5.0" - } + headers = {"User-Agent": "Mozilla/5.0"} response = requests.get(image_url, headers=headers, timeout=10) response.raise_for_status() auth = tweepy.OAuth1UserHandler( @@ -201,15 +215,6 @@ async def _validate_message( dao_id=None, ) - # Check tweet length - if len(tweet_text) > 280: # Twitter's character limit - return TweetProcessingResult( - success=False, - message=f"Tweet exceeds character limit: {len(tweet_text)} chars", - tweet_id=message.tweet_id, - dao_id=message.dao_id, - ) - # No need to modify the message structure, keep it as is return None @@ -257,30 +262,35 @@ async def _process_tweet_message( tweet_text = re.sub(re.escape(image_url), "", original_text).strip() tweet_text = re.sub(r"\s+", " ", tweet_text) - # Prepare tweet parameters - tweet_params = {"text": tweet_text} - if message.tweet_id: - tweet_params["reply_in_reply_to_tweet_id"] = message.tweet_id - - if image_url: - tweet_response = self._post_tweet_with_media( - image_url=image_url, - text=tweet_text, - reply_id=message.tweet_id, - ) - else: - tweet_response = await self.twitter_service._apost_tweet(**tweet_params) + # Split tweet text if necessary + chunks = self._split_text_into_chunks(tweet_text) + previous_tweet_id = message.tweet_id + tweet_response = None + + for index, chunk in enumerate(chunks): + if index == 0 and image_url: + tweet_response = self._post_tweet_with_media( + image_url=image_url, + text=chunk, + reply_id=previous_tweet_id, + ) + else: + tweet_response = await self.twitter_service._apost_tweet( + text=chunk, + reply_in_reply_to_tweet_id=previous_tweet_id, + ) - if not tweet_response: - return TweetProcessingResult( - success=False, - message="Failed to send tweet", - dao_id=message.dao_id, - tweet_id=message.tweet_id, - ) + if not tweet_response: + return TweetProcessingResult( + success=False, + message="Failed to send tweet", + dao_id=message.dao_id, + tweet_id=previous_tweet_id, + ) - logger.info(f"Successfully posted tweet {tweet_response.id}") - logger.debug(f"Tweet ID: {tweet_response.id}") + logger.info(f"Successfully posted tweet {tweet_response.id}") + logger.debug(f"Tweet ID: {tweet_response.id}") + previous_tweet_id = tweet_response.id # Discord Service try: @@ -290,10 +300,10 @@ async def _process_tweet_message( embeds = None if image_url: embeds = [{"image": {"url": image_url}}] - discord_result = discord_service.send_message(tweet_text, embeds=embeds) - logger.info( - f"Discord message sent: {discord_result['success']}" + discord_result = discord_service.send_message( + tweet_text, embeds=embeds ) + logger.info(f"Discord message sent: {discord_result['success']}") except Exception as e: logger.warning(f"Failed to send Discord message: {str(e)}") @@ -301,7 +311,7 @@ async def _process_tweet_message( return TweetProcessingResult( success=True, message="Successfully sent tweet", - tweet_id=tweet_response.id, + tweet_id=previous_tweet_id, dao_id=message.dao_id, )