Source code for twikit.tweet
from __future__ import annotations
import re
from datetime import datetime
from typing import TYPE_CHECKING
from .user import User
from .utils import find_dict, timestamp_to_datetime
if TYPE_CHECKING:
from httpx import Response
from .client import Client
from .community import Community
from .utils import Result
[docs]
class Tweet:
"""
Attributes
----------
id : :class:`str`
The unique identifier of the tweet.
created_at : :class:`str`
The date and time when the tweet was created.
created_at_datetime : :class:`datetime`
The created_at converted to datetime.
user: :class:`User`
Author of the tweet.
text : :class:`str`
The full text of the tweet.
lang : :class:`str`
The language of the tweet.
in_reply_to : :class:`str`
The tweet ID this tweet is in reply to, if any
is_quote_status : :class:`bool`
Indicates if the tweet is a quote status.
quote : :class:`Tweet`
The Tweet being quoted (if any)
retweeted_tweet : :class:`bool`
Whether the tweet is a retweet
possibly_sensitive : :class:`bool`
Indicates if the tweet content may be sensitive.
possibly_sensitive_editable : :class:`bool`
Indicates if the tweet's sensitivity can be edited.
quote_count : :class:`int`
The count of quotes for the tweet.
media : :class:`list`
A list of media entities associated with the tweet.
reply_count : :class:`int`
The count of replies to the tweet.
favorite_count : :class:`int`
The count of favorites or likes for the tweet.
favorited : :class:`bool`
Indicates if the tweet is favorited.
view_count: :class:`int` | None
The count of views.
retweet_count : :class:`int`
The count of retweets for the tweet.
editable_until_msecs : :class:`int`
The timestamp until which the tweet is editable.
is_translatable : :class:`bool`
Indicates if the tweet is translatable.
is_edit_eligible : :class:`bool`
Indicates if the tweet is eligible for editing.
edits_remaining : :class:`int`
The remaining number of edits allowed for the tweet.
state : :class:`str` | None
The state of the tweet views.
replies: Result[:class:`Tweet`] | None
Replies to the tweet.
reply_to: list[:class:`Tweet`] | None
A list of Tweet objects representing the tweets to which to reply.
related_tweets : list[:class:`Tweet`] | None
Related tweets.
hashtags: list[:class:`str`]
Hashtags included in the tweet text.
poll : :class:`Poll`
Poll attached to the tweet.
has_card : :class:`bool`
Indicates if the tweet contains a card.
thumbnail_title : :class:`str` | None
The title of the webpage displayed inside tweet's card.
thumbnail_url : :class:`str` | None
Link to the image displayed in the tweet's card.
urls : :class:`list`
Information about URLs contained in the tweet.
full_text : :class:`str` | None
The full text of the tweet.
"""
def __init__(self, client: Client, data: dict, user: User = None) -> None:
self._client = client
self._data = data
self.user = user
self.replies: Result[Tweet] | None = None
self.reply_to: list[Tweet] | None = None
self.related_tweets: list[Tweet] | None = None
self.thread: list[Tweet] | None = None
self.id: str = data['rest_id']
legacy = data['legacy']
self.created_at: str = legacy['created_at']
self.text: str = legacy['full_text']
self.lang: str = legacy['lang']
self.is_quote_status: bool = legacy['is_quote_status']
self.in_reply_to: str | None = self._data['legacy'].get(
'in_reply_to_status_id_str'
)
if data.get('quoted_status_result'):
quoted_tweet = data.pop('quoted_status_result')['result']
if 'tweet' in quoted_tweet:
quoted_tweet = quoted_tweet['tweet']
if quoted_tweet.get('__typename') != 'TweetTombstone':
quoted_user = User(
client, quoted_tweet['core']['user_results']['result']
)
self.quote: Tweet = Tweet(client, quoted_tweet, quoted_user)
else:
self.quote = None
if legacy.get('retweeted_status_result'):
retweeted_tweet = legacy.pop('retweeted_status_result')['result']
if 'tweet' in retweeted_tweet:
retweeted_tweet = retweeted_tweet['tweet']
retweeted_user = User(
client, retweeted_tweet['core']['user_results']['result']
)
self.retweeted_tweet: Tweet = Tweet(
client, retweeted_tweet, retweeted_user
)
else:
self.retweeted_tweet = None
note_tweet_results = find_dict(data, 'note_tweet_results')
self.full_text: str = self.text
if note_tweet_results:
text_list = find_dict(note_tweet_results, 'text')
if text_list:
self.full_text = text_list[0]
entity_set = note_tweet_results[0]['result']['entity_set']
self.urls: list = entity_set.get('urls')
hashtags = entity_set.get('hashtags', [])
else:
self.urls: list = legacy['entities'].get('urls')
hashtags = legacy['entities'].get('hashtags', [])
self.hashtags: list[str] = [
i['text'] for i in hashtags
]
self.is_quote_status: bool = legacy['is_quote_status']
self.possibly_sensitive: bool = legacy.get('possibly_sensitive')
self.possibly_sensitive_editable: bool = legacy.get(
'possibly_sensitive_editable')
self.quote_count: int = legacy['quote_count']
self.media: list = legacy['entities'].get('media')
self.reply_count: int = legacy['reply_count']
self.favorite_count: int = legacy['favorite_count']
self.favorited: bool = legacy['favorited']
self.view_count: int = (data['views'].get('count')
if 'views' in data else None)
self.retweet_count: int = legacy['retweet_count']
self.editable_until_msecs: int = data['edit_control'].get(
'editable_until_msecs')
self.is_translatable: bool = data.get('is_translatable')
self.is_edit_eligible: bool = data['edit_control'].get(
'is_edit_eligible')
self.edits_remaining: int = data['edit_control'].get('edits_remaining')
self.state: str = (data['views'].get('state')
if 'views' in data else None)
self.has_community_notes: bool = data.get('has_birdwatch_notes')
self.community_note = None
if 'birdwatch_pivot' in data:
community_note_data = data['birdwatch_pivot']
if 'note' in community_note_data:
self.community_note = {
'id': community_note_data['note']['rest_id'],
'text': community_note_data['subtitle']['text']
}
if (
'card' in data and
'legacy' in data['card'] and
'name' in data['card']['legacy'] and
data['card']['legacy']['name'].startswith('poll')
):
self._poll_data = data['card']
else:
self._poll_data = None
self.thumbnail_url = None
self.thumbnail_title = None
self.has_card = 'card' in data
if (
'card' in data and
'legacy' in data['card'] and
'binding_values' in data['card']['legacy']
):
card_data = data['card']['legacy']['binding_values']
if isinstance(card_data, list):
binding_values = {
i.get('key'): i.get('value')
for i in card_data
}
if (
'title' in binding_values and
'string_value' in binding_values['title']
):
self.thumbnail_title = binding_values['title']['string_value']
if (
'thumbnail_image_original' in binding_values and
'image_value' in binding_values['thumbnail_image_original'] and
'url' in binding_values['thumbnail_image_original'
]['image_value']
):
self.thumbnail_url = binding_values['thumbnail_image_original'
]['image_value']['url']
@property
def created_at_datetime(self) -> datetime:
return timestamp_to_datetime(self.created_at)
@property
def poll(self) -> Poll:
return self._poll_data and Poll(self._client, self._poll_data, self)
[docs]
def delete(self) -> Response:
"""Deletes the tweet.
Returns
-------
:class:`httpx.Response`
Response returned from twitter api.
Examples
--------
>>> tweet.delete()
"""
return self._client.delete_tweet(self.id)
[docs]
def favorite(self) -> Response:
"""
Favorites the tweet.
Returns
-------
:class:`httpx.Response`
Response returned from twitter api.
See Also
--------
Client.favorite_tweet
"""
return self._client.favorite_tweet(self.id)
[docs]
def unfavorite(self) -> Response:
"""
Favorites the tweet.
Returns
-------
:class:`httpx.Response`
Response returned from twitter api.
See Also
--------
Client.unfavorite_tweet
"""
return self._client.unfavorite_tweet(self.id)
[docs]
def retweet(self) -> Response:
"""
Retweets the tweet.
Returns
-------
:class:`httpx.Response`
Response returned from twitter api.
See Also
--------
Client.retweet
"""
return self._client.retweet(self.id)
[docs]
def delete_retweet(self) -> Response:
"""
Deletes the retweet.
Returns
-------
:class:`httpx.Response`
Response returned from twitter api.
See Also
--------
Client.delete_retweet
"""
return self._client.delete_retweet(self.id)
[docs]
def bookmark(self) -> Response:
"""
Adds the tweet to bookmarks.
Returns
-------
:class:`httpx.Response`
Response returned from twitter api.
See Also
--------
Client.bookmark_tweet
"""
return self._client.bookmark_tweet(self.id)
[docs]
def delete_bookmark(self) -> Response:
"""
Removes the tweet from bookmarks.
Returns
-------
:class:`httpx.Response`
Response returned from twitter api.
See Also
--------
Client.delete_bookmark
"""
return self._client.delete_bookmark(self.id)
[docs]
def reply(
self,
text: str = '',
media_ids: list[str] | None = None,
**kwargs
) -> Tweet:
"""
Replies to the tweet.
Parameters
----------
text : :class:`str`, default=''
The text content of the reply.
media_ids : list[:class:`str`], default=None
A list of media IDs or URIs to attach to the reply.
Media IDs can be obtained by using the `upload_media` method.
Returns
-------
:class:`Tweet`
The created tweet.
Examples
--------
>>> tweet_text = 'Example text'
>>> media_ids = [
... client.upload_media('image1.png'),
... client.upload_media('image2.png')
... ]
>>> tweet.reply(
... tweet_text,
... media_ids=media_ids
... )
See Also
--------
`Client.upload_media`
"""
return self._client.create_tweet(
text, media_ids, reply_to=self.id, **kwargs
)
[docs]
def get_retweeters(
self, count: str = 40, cursor: str | None = None
) -> Result[User]:
"""
Retrieve users who retweeted the tweet.
Parameters
----------
count : :class:`int`, default=40
The maximum number of users to retrieve.
cursor : :class:`str`, default=None
A string indicating the position of the cursor for pagination.
Returns
-------
Result[:class:`User`]
A list of users who retweeted the tweet.
Examples
--------
>>> tweet_id = '...'
>>> retweeters = tweet.get_retweeters()
>>> print(retweeters)
[<User id="...">, <User id="...">, ..., <User id="...">]
>>> more_retweeters = retweeters.next() # Retrieve more retweeters.
>>> print(more_retweeters)
[<User id="...">, <User id="...">, ..., <User id="...">]
"""
return self._client.get_retweeters(self.id, count, cursor)
[docs]
def get_favoriters(
self, count: str = 40, cursor: str | None = None
) -> Result[User]:
"""
Retrieve users who favorited a specific tweet.
Parameters
----------
tweet_id : :class:`str`
The ID of the tweet.
count : int, default=40
The maximum number of users to retrieve.
cursor : :class:`str`, default=None
A string indicating the position of the cursor for pagination.
Returns
-------
Result[:class:`User`]
A list of users who favorited the tweet.
Examples
--------
>>> tweet_id = '...'
>>> favoriters = tweet.get_favoriters()
>>> print(favoriters)
[<User id="...">, <User id="...">, ..., <User id="...">]
>>> more_favoriters = favoriters.next() # Retrieve more favoriters.
>>> print(more_favoriters)
[<User id="...">, <User id="...">, ..., <User id="...">]
"""
return self._client.get_favoriters(self.id, count, cursor)
[docs]
def get_similar_tweets(self) -> list[Tweet]:
"""
Retrieves tweets similar to the tweet (Twitter premium only).
Returns
-------
list[:class:`Tweet`]
A list of Tweet objects representing tweets
similar to the tweet.
"""
return self._client.get_similar_tweets(self.id)
[docs]
def update(self) -> None:
new = self._client.get_tweet_by_id(self.id)
self.__dict__.update(new.__dict__)
def __repr__(self) -> str:
return f'<Tweet id="{self.id}">'
def __eq__(self, __value: object) -> bool:
return isinstance(__value, Tweet) and self.id == __value.id
def __ne__(self, __value: object) -> bool:
return not self == __value
[docs]
def tweet_from_data(client: Client, data: dict) -> Tweet:
tweet_data_ = find_dict(data, 'result', True)
if not tweet_data_:
return None
tweet_data = tweet_data_[0]
if tweet_data.get('__typename') == 'TweetTombstone':
return None
if 'tweet' in tweet_data:
tweet_data = tweet_data['tweet']
if 'core' not in tweet_data:
return None
if 'result' not in tweet_data['core']['user_results']:
return None
if 'legacy' not in tweet_data:
return None
user_data = tweet_data['core']['user_results']['result']
return Tweet(client, tweet_data, User(client, user_data))
[docs]
class ScheduledTweet:
def __init__(self, client: Client, data: dict) -> None:
self._client = client
self.id = data['rest_id']
self.execute_at: int = data['scheduling_info']['execute_at']
self.state: str = data['scheduling_info']['state']
self.type: str = data['tweet_create_request']['type']
self.text: str = data['tweet_create_request']['status']
self.media = [i['media_info'] for i in data.get('media_entities', [])]
[docs]
def delete(self) -> Response:
"""
Delete the scheduled tweet.
Returns
-------
:class:`httpx.Response`
Response returned from twitter api.
"""
return self._client.delete_scheduled_tweet(self.id)
def __repr__(self) -> str:
return f'<ScheduledTweet id="{self.id}">'
def __eq__(self, __value: object) -> bool:
return isinstance(__value, ScheduledTweet) and self.id == __value.id
def __ne__(self, __value: object) -> bool:
return not self == __value
[docs]
class TweetTombstone:
def __init__(self, client: Client, tweet_id: str, data: dict) -> None:
self._client = client
self.id = tweet_id
self.text: str = data['text']['text']
def __repr__(self) -> str:
return f'<TweetTombstone id="{self.id}">'
def __eq__(self, __value: object) -> bool:
return isinstance(__value, TweetTombstone) and self.id == __value.id
def __ne__(self, __value: object) -> bool:
return not self == __value
[docs]
class Poll:
"""Represents a poll associated with a tweet.
Attributes
----------
tweet : :class:`Tweet`
The tweet associated with the poll.
id : :class:`str`
The unique identifier of the poll.
name : :class:`str`
The name of the poll.
choices : list[:class:`dict`]
A list containing dictionaries representing poll choices.
Each dictionary contains 'label' and 'count' keys
for choice label and count.
duration_minutes : :class:`int`
The duration of the poll in minutes.
end_datetime_utc : :class:`str`
The end date and time of the poll in UTC format.
last_updated_datetime_utc : :class:`str`
The last updated date and time of the poll in UTC format.
selected_choice : :class:`str` | None
Number of the selected choice.
"""
def __init__(
self, client: Client, data: dict, tweet: Tweet | None = None
) -> None:
self._client = client
self.tweet = tweet
legacy = data['legacy']
binding_values = legacy['binding_values']
if isinstance(legacy['binding_values'], list):
binding_values = {
i.get('key'): i.get('value')
for i in legacy['binding_values']
}
self.id: str = data['rest_id']
self.name: str = legacy['name']
choices_number = int(re.findall(
r'poll(\d)choice_text_only', self.name
)[0])
choices = []
for i in range(1, choices_number + 1):
choice_label = binding_values[f'choice{i}_label']
choice_count = binding_values[f'choice{i}_count']
choices.append({
'number': str(i),
'label': choice_label['string_value'],
'count': choice_count.get('string_value', '0')
})
self.choices = choices
duration_minutes = binding_values['duration_minutes']['string_value']
self.duration_minutes = int(duration_minutes)
end = binding_values['end_datetime_utc']['string_value']
updated = binding_values['last_updated_datetime_utc']['string_value']
self.end_datetime_utc: str = end
self.last_updated_datetime_utc: str = updated
counts_are_final = binding_values['counts_are_final']['boolean_value']
self.counts_are_final: bool = counts_are_final
if 'selected_choice' in binding_values:
selected_choice = binding_values['selected_choice']['string_value']
self.selected_choice: str = selected_choice
else:
self.selected_choice = None
[docs]
def vote(self, selected_choice: str) -> Poll:
"""
Vote on the poll with the specified selected choice.
Parameters
----------
selected_choice : :class:`str`
The label of the selected choice for the vote.
Returns
-------
:class:`Poll`
The Poll object representing the updated poll after voting.
"""
return self._client.vote(
selected_choice,
self.id,
self.tweet.id,
self.name
)
def __repr__(self) -> str:
return f'<Poll id="{self.id}">'
def __eq__(self, __value: object) -> bool:
return isinstance(__value, Poll) and self.id == __value.id
def __ne__(self, __value: object) -> bool:
return not self == __value
[docs]
class CommunityNote:
"""Represents a community note.
Attributes
----------
id : :class:`str`
The ID of the community note.
text : :class:`str`
The text content of the community note.
misleading_tags : list[:class:`str`]
A list of tags indicating misleading information.
trustworthy_sources : :class:`bool`
Indicates if the sources are trustworthy.
helpful_tags : list[:class:`str`]
A list of tags indicating helpful information.
created_at : :class:`int`
The timestamp when the note was created.
can_appeal : :class:`bool`
Indicates if the note can be appealed.
appeal_status : :class:`str`
The status of the appeal.
is_media_note : :class:`bool`
Indicates if the note is related to media content.
media_note_matches : :class:`str`
Matches related to media content.
birdwatch_profile : :class:`dict`
Birdwatch profile associated with the note.
tweet_id : :class:`str`
The ID of the tweet associated with the note.
"""
def __init__(self, client: Client, data: dict) -> None:
self._client = client
self.id: str = data['rest_id']
data_v1 = data['data_v1']
self.text: str = data_v1['summary']['text']
self.misleading_tags: list[str] = data_v1.get('misleading_tags')
self.trustworthy_sources: bool = data_v1.get('trustworthy_sources')
self.helpful_tags: list[str] = data.get('helpful_tags')
self.created_at: int = data.get('created_at')
self.can_appeal: bool = data.get('can_appeal')
self.appeal_status: str = data.get('appeal_status')
self.is_media_note: bool = data.get('is_media_note')
self.media_note_matches: str = data.get('media_note_matches')
self.birdwatch_profile: dict = data.get('birdwatch_profile')
self.tweet_id: str = data['tweet_results']['result']['rest_id']
[docs]
def update(self) -> None:
new = self._client.get_community_note(self.id)
self.__dict__.update(new.__dict__)
def __repr__(self) -> str:
return f'<CommunityNote id="{self.id}">'
def __eq__(self, __value: object) -> bool:
return isinstance(__value, CommunityNote) and self.id == __value.id
def __ne__(self, __value: object) -> bool:
return not self == __value