From 7884e3a7253d9a821ff46160ec0d3811f299327f Mon Sep 17 00:00:00 2001 From: Harmon Date: Wed, 3 Nov 2021 16:48:57 -0500 Subject: [PATCH] Add support for managing Tweets with API v2 --- cassettes/test_create_and_delete_tweet.yaml | 132 ++++++++++++++++++++ docs/client.rst | 16 +++ tests/test_client.py | 6 + tweepy/client.py | 128 +++++++++++++++++++ 4 files changed, 282 insertions(+) create mode 100644 cassettes/test_create_and_delete_tweet.yaml diff --git a/cassettes/test_create_and_delete_tweet.yaml b/cassettes/test_create_and_delete_tweet.yaml new file mode 100644 index 0000000..2c70562 --- /dev/null +++ b/cassettes/test_create_and_delete_tweet.yaml @@ -0,0 +1,132 @@ +interactions: +- request: + body: '{"text": "Test Tweet"}' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '22' + Content-Type: + - application/json + User-Agent: + - Python/3.10.0 Requests/2.26.0 Tweepy/4.2.0 + method: POST + uri: https://api.twitter.com/2/tweets + response: + body: + string: !!binary | + H4sIAAAAAAAAAKpWSkksSVSyqlbKTFGyUjI0MTUzMDSyNDe2MDY1t7AwsjRW0lEqSa0oAUqGpBaX + KISUp6aWKNXWAgAAAP//AwBe5WlnOQAAAA== + headers: + api-version: + - '2.28' + cache-control: + - no-cache, no-store, max-age=0 + content-disposition: + - attachment; filename=json.json + content-encoding: + - gzip + content-length: + - '82' + content-type: + - application/json; charset=utf-8 + date: + - Wed, 03 Nov 2021 21:38:39 UTC + location: + - https://api.twitter.com/2/tweets/1456012973835788293 + server: + - tsa_b + set-cookie: + - personalization_id="v1_E3WlXXfQ1xB5rs0qHbxukQ=="; Max-Age=63072000; Expires=Fri, + 03 Nov 2023 21:38:39 GMT; Path=/; Domain=.twitter.com; Secure; SameSite=None + - guest_id=v1%3A163597551896174473; Max-Age=63072000; Expires=Fri, 03 Nov 2023 + 21:38:39 GMT; Path=/; Domain=.twitter.com; Secure; SameSite=None + strict-transport-security: + - max-age=631138519 + x-access-level: + - read-write-directmessages + x-connection-hash: + - f86af2f37d975724d196465ae7a8bb5b420f7b2bb1b541276f76c3fe0569779e + x-content-type-options: + - nosniff + x-frame-options: + - SAMEORIGIN + x-rate-limit-limit: + - '200' + x-rate-limit-remaining: + - '199' + x-rate-limit-reset: + - '1635976418' + x-response-time: + - '226' + x-xss-protection: + - '0' + status: + code: 201 + message: Created +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '0' + Cookie: + - guest_id=v1%3A163597551896174473; personalization_id="v1_E3WlXXfQ1xB5rs0qHbxukQ==" + User-Agent: + - Python/3.10.0 Requests/2.26.0 Tweepy/4.2.0 + method: DELETE + uri: https://api.twitter.com/2/tweets/1456012973835788293 + response: + body: + string: !!binary | + H4sIAAAAAAAAAKpWSkksSVSyqlZKSc1JLUlNUbIqKSpNra0FAAAA//8DAD5d+0oZAAAA + headers: + api-version: + - '2.28' + cache-control: + - no-cache, no-store, max-age=0 + content-disposition: + - attachment; filename=json.json + content-encoding: + - gzip + content-length: + - '51' + content-type: + - application/json; charset=utf-8 + date: + - Wed, 03 Nov 2021 21:38:39 UTC + server: + - tsa_b + strict-transport-security: + - max-age=631138519 + x-access-level: + - read-write-directmessages + x-connection-hash: + - f86af2f37d975724d196465ae7a8bb5b420f7b2bb1b541276f76c3fe0569779e + x-content-type-options: + - nosniff + x-frame-options: + - SAMEORIGIN + x-rate-limit-limit: + - '50' + x-rate-limit-remaining: + - '49' + x-rate-limit-reset: + - '1635976419' + x-response-time: + - '44' + x-xss-protection: + - '0' + status: + code: 200 + message: OK +version: 1 diff --git a/docs/client.rst b/docs/client.rst index b809647..5665dd3 100644 --- a/docs/client.rst +++ b/docs/client.rst @@ -32,6 +32,12 @@ +--------------------------------------------------------------+----------------------------------------+ | `POST /2/users/:id/likes`_ | :meth:`Client.like` | +--------------------------------------------------------------+----------------------------------------+ + | .. centered:: |Manage Tweets|_ | + +--------------------------------------------------------------+----------------------------------------+ + | `DELETE /2/tweets/:id`_ | :meth:`Client.delete_tweet` | + +--------------------------------------------------------------+----------------------------------------+ + | `POST /2/tweets`_ | :meth:`Client.create_tweet` | + +--------------------------------------------------------------+----------------------------------------+ | .. centered:: |Retweets|_ | +--------------------------------------------------------------+----------------------------------------+ | `DELETE /2/users/:id/retweets/:source_tweet_id`_ | :meth:`Client.unretweet` | @@ -156,6 +162,9 @@ .. _GET /2/tweets/:id/liking_users: https://developer.twitter.com/en/docs/twitter-api/tweets/likes/api-reference/get-tweets-id-liking_users .. _GET /2/users/:id/liked_tweets: https://developer.twitter.com/en/docs/twitter-api/tweets/likes/api-reference/get-users-id-liked_tweets .. _POST /2/users/:id/likes: https://developer.twitter.com/en/docs/twitter-api/tweets/likes/api-reference/post-users-id-likes +.. |Manage Tweets| replace:: *Manage Tweets* +.. _DELETE /2/tweets/:id: https://developer.twitter.com/en/docs/twitter-api/tweets/manage-tweets/api-reference/delete-tweets-id +.. _POST /2/tweets: https://developer.twitter.com/en/docs/twitter-api/tweets/manage-tweets/api-reference/post-tweets .. |Retweets| replace:: *Retweets* .. _DELETE /2/users/:id/retweets/:source_tweet_id: https://developer.twitter.com/en/docs/twitter-api/tweets/retweets/api-reference/delete-users-id-retweets-tweet_id .. _GET /2/tweets/:id/retweeted_by: https://developer.twitter.com/en/docs/twitter-api/tweets/retweets/api-reference/get-tweets-id-retweeted_by @@ -232,6 +241,13 @@ Likes .. automethod:: Client.like +Manage Tweets +------------- + +.. automethod:: Client.delete_tweet + +.. automethod:: Client.create_tweet + Retweets -------- diff --git a/tests/test_client.py b/tests/test_client.py index d92c7ec..499912e 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -37,6 +37,12 @@ class TweepyTestCase(unittest.TestCase): user_id = 783214 # User ID for @Twitter self.client.get_liked_tweets(user_id) + @tape.use_cassette("test_create_and_delete_tweet.yaml", serializer="yaml") + def test_create_and_delete_tweet(self): + response = self.client.create_tweet(text="Test Tweet") + tweet_id = response.data["id"] + self.client.delete_tweet(tweet_id) + @tape.use_cassette("test_retweet_and_unretweet.yaml", serializer="yaml") def test_retweet_and_unretweet(self): tweet_id = 1415348607813832708 # @TwitterDev Tweet announcing API v2 Retweet endpoints diff --git a/tweepy/client.py b/tweepy/client.py index bd23dc8..4a32bd7 100644 --- a/tweepy/client.py +++ b/tweepy/client.py @@ -390,6 +390,134 @@ class Client: "POST", route, json={"tweet_id": str(tweet_id)}, user_auth=True ) + # Manage Tweets + + def delete_tweet(self, id): + """Allows an authenticated user ID to delete a Tweet. + + Parameters + ---------- + id : Union[int, str] + The Tweet ID you are deleting. + + Returns + ------- + Union[dict, requests.Response, Response] + + References + ---------- + https://developer.twitter.com/en/docs/twitter-api/tweets/manage-tweets/api-reference/delete-tweets-id + """ + return self._make_request( + "DELETE", f"/2/tweets/{id}", user_auth=True + ) + + def create_tweet( + self, *, direct_message_deep_link=None, for_super_followers_only=None, + place_id=None, media_ids=None, media_tagged_user_ids=None, + poll_duration_minutes=None, poll_options=None, quote_tweet_id=None, + exclude_reply_user_ids=None, in_reply_to_tweet_id=None, + reply_settings=None, text=None + ): + """Creates a Tweet on behalf of an authenticated user. + + Parameters + ---------- + direct_message_deep_link : Optional[str] + `Tweets a link directly to a Direct Message conversation`_ with an + account. + for_super_followers_only : Optional[bool] + Allows you to Tweet exclusively for `Super Followers`_. + place_id : Optional[int] + Place ID being attached to the Tweet for geo location. + media_ids : Optional[List[int, str]] + A list of Media IDs being attached to the Tweet. This is only + required if the request includes the ``tagged_user_ids``. + media_tagged_user_ids : Optional[Union[int, str]] + A list of User IDs being tagged in the Tweet with Media. If the + user you're tagging doesn't have photo-tagging enabled, their names + won't show up in the list of tagged users even though the Tweet is + successfully created. + poll_duration_minutes : Optional[int] + Duration of the poll in minutes for a Tweet with a poll. This is + only required if the request includes ``poll.options``. + poll_options : Optional[List[str]] + A list of poll options for a Tweet with a poll. + quote_tweet_id : Optional[Union[int, str]] + Link to the Tweet being quoted. + exclude_reply_user_ids : Optional[List[Union[int, str]]] + A list of User IDs to be excluded from the reply Tweet thus + removing a user from a thread. + in_reply_to_tweet_id : Optional[Union[int, str]] + Tweet ID of the Tweet being replied to. This is only required if + the request includes the ``reply.exclude_reply_user_ids``. + reply_settings : Optional[str] + `Settings`_ to indicate who can reply to the Tweet. Limited to + "mentionedUsers" and "following". If the field isn’t specified, it + will default to everyone. + text : Optional[str] + Text of the Tweet being created. This field is required if + ``media.media_ids`` is not present. + + Returns + ------- + Union[dict, requests.Response, Response] + + References + ---------- + https://developer.twitter.com/en/docs/twitter-api/tweets/manage-tweets/api-reference/post-tweets + + .. _Tweets a link directly to a Direct Message conversation: https://business.twitter.com/en/help/campaign-editing-and-optimization/public-to-private-conversation.html + .. _Super Followers: https://help.twitter.com/en/using-twitter/super-follows + .. _Settings: https://blog.twitter.com/en_us/topics/product/2020/new-conversation-settings-coming-to-a-tweet-near-you + """ + json = {} + + if direct_message_deep_link is not None: + json["direct_message_deep_link"] = direct_message_deep_link + + if for_super_followers_only is not None: + json["for_super_followers_only"] = for_super_followers_only + + if place_id is not None: + json["geo"] = {"place_id": place_id} + + if media_ids is not None: + json["media"] = { + "media_ids": [str(media_id) for media_id in media_ids] + } + if media_tagged_user_ids is not None: + json["media"]["tagged_user_ids"] = [ + str(media_tagged_user_id) + for media_tagged_user_id in media_tagged_user_ids + ] + + if poll_options is not None: + json["poll"] = {"options": poll_options} + if poll_duration_minutes is not None: + json["poll"]["duration_minutes"] = poll_duration_minutes + + if quote_tweet_id is not None: + json["quote_tweet_id"] = str(quote_tweet_id) + + if in_reply_to_tweet_id is not None: + json["reply"] = {"in_reply_to_tweet_id": str(in_reply_to_tweet_id)} + if exclude_reply_user_ids is not None: + json["reply"]["exclude_reply_user_ids"] = [ + str(exclude_reply_user_id) + for exclude_reply_user_id in exclude_reply_user_ids + ] + + if reply_settings is not None: + json["reply_settings"] = reply_settings + + if text is not None: + json["text"] = text + + return self._make_request( + "POST", f"/2/tweets", json=json, user_auth=True + ) + # Retweets def unretweet(self, source_tweet_id): -- 2.25.1