Source code for trackers.twitter
"""Module containing class for tracking mentions on X/Twitter."""
from datetime import datetime
import tweepy
from trackers.base import BaseMentionTracker
[docs]
class TwitterTracker(BaseMentionTracker):
"""Tracker for Twitter mentions of the bot account.
:var TwitterTracker.client: authenticated Twitter client
:type TwitterTracker.client: :class:`tweepy.Client`
:var TwitterTracker.target_user_id: Twitter user ID to track
:type TwitterTracker.target_user_id: str`
"""
def __init__(self, parse_message_callback, config):
"""Initialize Twitter tracker.
:param parse_message_callback: function to call when mention is found
:type parse_message_callback: callable
:param config: configuration dictionary for X API
:type config: dict
"""
super().__init__("twitter", parse_message_callback)
self.client = tweepy.Client(
bearer_token=config["bearer_token"],
consumer_key=config["consumer_key"],
consumer_secret=config["consumer_secret"],
access_token=config["access_token"],
access_token_secret=config["access_token_secret"],
)
self.target_user_id = config["target_user_id"]
self.logger.info("Twitter tracker initialized")
self.log_action(
"initialized", f"Tracking mentions for user ID: {self.target_user_id}"
)
def _get_original_tweet_info(self, referenced_tweet_id):
"""Get original tweet information for reply mentions.
:param referenced_tweet_id: ID of the referenced tweet
:type referenced_tweet_id: str
:var original_tweet: response from Twitter API for the referenced tweet
:type original_tweet: :class:`tweepy.models.Response`
:var contribution_url: URL to the original tweet
:type contribution_url: str
:var contributor: username of the original tweet author
:type contributor: str
:var contribution: text of the original tweet
:type contribution: str
:var original_user_map: mapping of user IDs to user objects
:type original_user_map: dict
:var author: user object of the original tweet author
:type author: :class:`tweepy.models.User`
:return: tuple of (contribution_url, contributor_username, contribution)
:rtype: tuple
"""
try:
original_tweet = self.client.get_tweet(
referenced_tweet_id,
tweet_fields=["created_at", "author_id", "text"],
expansions=["author_id"],
)
if not original_tweet.data:
return "", "", ""
contribution_url = (
f"https://twitter.com/i/web/status/{original_tweet.data.id}"
)
contribution = original_tweet.data.text or ""
contributor = ""
if original_tweet.includes and "users" in original_tweet.includes:
users = original_tweet.includes["users"]
original_user_map = {u.id: u.username for u in users}
author_id = original_tweet.data.author_id
contributor = original_user_map.get(author_id, "")
return contribution_url, contributor, contribution
except Exception as e:
self.logger.warning(
f"Failed to get original tweet {referenced_tweet_id}: {e}"
)
return "", "", ""
def _extract_reply_mention_data(self, tweet, user_map):
"""Extract data for reply mentions.
:param tweet: Twitter tweet object
:type tweet: :class:`tweepy.models.Tweet`
:param user_map: mapping of user IDs to usernames
:type user_map: dict
:var contribution_url: URL to the contribution tweet
:type contribution_url: str
:var contributor: username of the contributor
:type contributor: str
:var contribution: text of the original tweet
:type contribution: str
:var ref: referenced tweet object
:type ref: :class:`tweepy.models.ReferencedTweet`
:return: tuple of (contribution_url, contributor, contribution)
:rtype: tuple
"""
contribution_url, contributor, contribution = "", "", ""
if tweet.referenced_tweets:
for ref in tweet.referenced_tweets:
if ref.type == "replied_to":
(
contribution_url,
contributor,
contribution,
) = self._get_original_tweet_info(ref.id)
break
return contribution_url, contributor, contribution
def _get_content(self, tweet):
"""Safely get content preview from tweet text.
:param tweet: Twitter tweet object
:type tweet: :class:`tweepy.models.Tweet`
:return: content preview string
:rtype: str
"""
if hasattr(tweet, "text") and tweet.text:
return tweet.text
return ""
def _get_timestamp(self, tweet):
"""Safely get timestamp from tweet.
:param tweet: Twitter tweet object
:type tweet: :class:`tweepy.models.Tweet`
:return: seconds since epoch
:rtype: int
"""
if hasattr(tweet, "created_at") and tweet.created_at:
return int(tweet.created_at.timestamp())
return int(datetime.now().timestamp())
[docs]
def extract_mention_data(self, tweet, user_map):
"""Extract standardized data from Twitter mention.
:param tweet: Twitter tweet object
:type tweet: :class:`tweepy.models.Tweet`
:param user_map: mapping of user IDs to usernames
:type user_map: dict
:var suggester_username: username of the user who mentioned the bot
:type suggester_username: str
:var contribution_url: URL to the contribution tweet
:type contribution_url: str
:var contributor: username of the contributor
:type contributor: str
:var contribution: text of the original tweet
:type contribution: str
:var suggestion_url: URL to the suggestion tweet
:type suggestion_url: str
:var data: extracted data dictionary
:type data: dict
:return: standardized mention data
:rtype: dict
"""
# Get suggester username from user_map
suggester_username = user_map.get(tweet.author_id, "")
# Handle reply mentions
contribution_url, contributor, contribution = self._extract_reply_mention_data(
tweet, user_map
)
# If not a reply, use current tweet as contribution
if not contribution_url:
contribution_url = f"https://twitter.com/i/web/status/{tweet.id}"
contributor = suggester_username
suggestion_url = f"https://twitter.com/i/web/status/{tweet.id}"
data = {
"suggester": suggester_username,
"suggestion_url": suggestion_url,
"contribution_url": contribution_url,
"contributor": contributor or suggester_username,
"type": "tweet",
"content": self._get_content(tweet),
"contribution": contribution,
"timestamp": self._get_timestamp(tweet),
"item_id": tweet.id,
}
return data
[docs]
def check_mentions(self):
"""Check for new mentions on Twitter.
:var mention_count: number of new mentions found
:type mention_count: int
:var mentions: recent mentions from Twitter API
:type mentions: :class:`tweepy.models.Response`
:var user_map: mapping of user IDs to usernames from API response
:type user_map: dict
:var tweet: individual tweet from mentions
:type tweet: :class:`tweepy.models.Tweet`
:var data: extracted mention data
:type data: dict
:return: number of new mentions processed
:rtype: int
"""
mention_count = 0
try:
# Get recent mentions
mentions = self.client.get_users_mentions(
self.target_user_id,
tweet_fields=[
"created_at",
"conversation_id",
"author_id",
"text",
"referenced_tweets",
],
expansions=["author_id"],
max_results=20,
)
if mentions.data:
user_map = {
u.id: u.username for u in mentions.includes.get("users", [])
}
for tweet in mentions.data:
if not self.is_processed(tweet.id):
data = self.extract_mention_data(tweet, user_map)
if self.process_mention(
tweet.id, data, f"@{data.get('contributor')}"
):
mention_count += 1
self.log_action("mentions_checked", f"Found {mention_count} new mentions")
except Exception as e:
self.logger.error(f"Error checking Twitter mentions: {e}")
self.log_action("twitter_check_error", f"Error: {str(e)}")
return mention_count
[docs]
def run(self, poll_interval_minutes=15, max_iterations=None):
"""Run Twitter mentions tracker.
Uses the shared base tracker loop for polling and processing mentions.
:param poll_interval_minutes: how often to check for mentions
:type poll_interval_minutes: int or float
:param max_iterations: maximum number of polls before stopping
(``None`` for infinite loop)
:type max_iterations: int or None
"""
super().run(
poll_interval_minutes=poll_interval_minutes,
max_iterations=max_iterations,
)