diff --git a/cassettes/test_follow_and_unfollow_list.yaml b/cassettes/test_follow_and_unfollow_list.yaml new file mode 100644 index 000000000..287e0667e --- /dev/null +++ b/cassettes/test_follow_and_unfollow_list.yaml @@ -0,0 +1,129 @@ +interactions: +- request: + body: '{"list_id": "84839422"}' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '23' + Content-Type: + - application/json + User-Agent: + - Python/3.10.0 Requests/2.26.0 Tweepy/4.1.0 + method: POST + uri: https://api.twitter.com/2/users/1072250532645998596/followed_lists + response: + body: + string: !!binary | + H4sIAAAAAAAAAKpWSkksSVSyqlZKy8/JyS/PzEtXsiopKk2trQUAAAD//wMA/FRoehsAAAA= + headers: + api-version: + - '2.27' + cache-control: + - no-cache, no-store, max-age=0 + content-disposition: + - attachment; filename=json.json + content-encoding: + - gzip + content-length: + - '53' + content-type: + - application/json; charset=utf-8 + date: + - Mon, 25 Oct 2021 07:24:49 UTC + server: + - tsa_b + set-cookie: + - personalization_id="v1_hlVVKDuf5oESwpgKPKVpkA=="; Max-Age=63072000; Expires=Wed, + 25 Oct 2023 07:24:49 GMT; Path=/; Domain=.twitter.com; Secure; SameSite=None + - guest_id=v1%3A163514668949543599; Max-Age=63072000; Expires=Wed, 25 Oct 2023 + 07:24:49 GMT; Path=/; Domain=.twitter.com; Secure; SameSite=None + strict-transport-security: + - max-age=631138519 + x-access-level: + - read-write-directmessages + x-connection-hash: + - bd61a0a6b7381c3a8f1601ff0c1e66c9ef64b2aa429e196fc71e553d3c848a65 + x-content-type-options: + - nosniff + x-frame-options: + - SAMEORIGIN + x-rate-limit-limit: + - '50' + x-rate-limit-remaining: + - '47' + x-rate-limit-reset: + - '1635146788' + x-response-time: + - '120' + x-xss-protection: + - '0' + status: + code: 200 + message: OK +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '0' + Cookie: + - guest_id=v1%3A163514668949543599; personalization_id="v1_hlVVKDuf5oESwpgKPKVpkA==" + User-Agent: + - Python/3.10.0 Requests/2.26.0 Tweepy/4.1.0 + method: DELETE + uri: https://api.twitter.com/2/users/1072250532645998596/followed_lists/84839422 + response: + body: + string: !!binary | + H4sIAAAAAAAAAKpWSkksSVSyqlZKy8/JyS/PzEtXskpLzClOra0FAAAA//8DAMXGczccAAAA + headers: + api-version: + - '2.27' + cache-control: + - no-cache, no-store, max-age=0 + content-disposition: + - attachment; filename=json.json + content-encoding: + - gzip + content-length: + - '54' + content-type: + - application/json; charset=utf-8 + date: + - Mon, 25 Oct 2021 07:24:49 UTC + server: + - tsa_b + strict-transport-security: + - max-age=631138519 + x-access-level: + - read-write-directmessages + x-connection-hash: + - bd61a0a6b7381c3a8f1601ff0c1e66c9ef64b2aa429e196fc71e553d3c848a65 + x-content-type-options: + - nosniff + x-frame-options: + - SAMEORIGIN + x-rate-limit-limit: + - '50' + x-rate-limit-remaining: + - '48' + x-rate-limit-reset: + - '1635146822' + x-response-time: + - '55' + x-xss-protection: + - '0' + status: + code: 200 + message: OK +version: 1 diff --git a/cassettes/test_manage_list.yaml b/cassettes/test_manage_list.yaml new file mode 100644 index 000000000..6fe1b77c6 --- /dev/null +++ b/cassettes/test_manage_list.yaml @@ -0,0 +1,443 @@ +interactions: +- request: + body: '{"name": "Test List", "private": true}' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '38' + Content-Type: + - application/json + User-Agent: + - Python/3.10.0 Requests/2.26.0 Tweepy/4.1.0 + method: POST + uri: https://api.twitter.com/2/lists + response: + body: + string: !!binary | + H4sIAAAAAAAAAKpWSkksSVSyqlbKTFGyUjI0MTUyNTYzMzQzMjC2tDQ3tDRQ0lHKS8xNBUqGpBaX + KPhkFpco1dYCAAAA//8DAAZ8t6c4AAAA + headers: + api-version: + - '2.27' + cache-control: + - no-cache, no-store, max-age=0 + content-disposition: + - attachment; filename=json.json + content-encoding: + - gzip + content-length: + - '81' + content-type: + - application/json; charset=utf-8 + date: + - Mon, 25 Oct 2021 07:24:50 UTC + location: + - https://api.twitter.com/2/lists/1452536616203997190 + server: + - tsa_b + set-cookie: + - personalization_id="v1_f6Q7tQ/Do3NkovyN4sJfog=="; Max-Age=63072000; Expires=Wed, + 25 Oct 2023 07:24:50 GMT; Path=/; Domain=.twitter.com; Secure; SameSite=None + - guest_id=v1%3A163514669039140044; Max-Age=63072000; Expires=Wed, 25 Oct 2023 + 07:24:50 GMT; Path=/; Domain=.twitter.com; Secure; SameSite=None + strict-transport-security: + - max-age=631138519 + x-access-level: + - read-write-directmessages + x-connection-hash: + - b20657636698db22333ea81a4e5d78f071184b672a5e536b229e6cada14038b3 + x-content-type-options: + - nosniff + x-frame-options: + - SAMEORIGIN + x-rate-limit-limit: + - '300' + x-rate-limit-remaining: + - '299' + x-rate-limit-reset: + - '1635147590' + x-response-time: + - '412' + x-xss-protection: + - '0' + status: + code: 201 + message: Created +- request: + body: '{"user_id": "783214"}' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '21' + Content-Type: + - application/json + Cookie: + - guest_id=v1%3A163514669039140044; personalization_id="v1_f6Q7tQ/Do3NkovyN4sJfog==" + User-Agent: + - Python/3.10.0 Requests/2.26.0 Tweepy/4.1.0 + method: POST + uri: https://api.twitter.com/2/lists/1452536616203997190/members + response: + body: + string: !!binary | + H4sIAAAAAAAAAKpWSkksSVSyqlbKLI7PTc1NSi1SsiopKk2trQUAAAD//wMAl7jcXBsAAAA= + headers: + api-version: + - '2.27' + cache-control: + - no-cache, no-store, max-age=0 + content-disposition: + - attachment; filename=json.json + content-encoding: + - gzip + content-length: + - '53' + content-type: + - application/json; charset=utf-8 + date: + - Mon, 25 Oct 2021 07:24:50 UTC + server: + - tsa_b + strict-transport-security: + - max-age=631138519 + x-access-level: + - read-write-directmessages + x-connection-hash: + - b20657636698db22333ea81a4e5d78f071184b672a5e536b229e6cada14038b3 + x-content-type-options: + - nosniff + x-frame-options: + - SAMEORIGIN + x-rate-limit-limit: + - '300' + x-rate-limit-remaining: + - '299' + x-rate-limit-reset: + - '1635147589' + x-response-time: + - '101' + x-xss-protection: + - '0' + status: + code: 200 + message: OK +- request: + body: '{"list_id": "1452536616203997190"}' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '34' + Content-Type: + - application/json + Cookie: + - guest_id=v1%3A163514669039140044; personalization_id="v1_f6Q7tQ/Do3NkovyN4sJfog==" + User-Agent: + - Python/3.10.0 Requests/2.26.0 Tweepy/4.1.0 + method: POST + uri: https://api.twitter.com/2/users/1072250532645998596/pinned_lists + response: + body: + string: !!binary | + H4sIAAAAAAAAAKpWSkksSVSyqlYqyMzLS01RsiopKk2trQUAAAD//wMAEmYA3BgAAAA= + headers: + api-version: + - '2.27' + cache-control: + - no-cache, no-store, max-age=0 + content-disposition: + - attachment; filename=json.json + content-encoding: + - gzip + content-length: + - '50' + content-type: + - application/json; charset=utf-8 + date: + - Mon, 25 Oct 2021 07:24:51 UTC + server: + - tsa_b + strict-transport-security: + - max-age=631138519 + x-access-level: + - read-write-directmessages + x-connection-hash: + - b20657636698db22333ea81a4e5d78f071184b672a5e536b229e6cada14038b3 + x-content-type-options: + - nosniff + x-frame-options: + - SAMEORIGIN + x-rate-limit-limit: + - '50' + x-rate-limit-remaining: + - '49' + x-rate-limit-reset: + - '1635147591' + x-response-time: + - '47' + x-xss-protection: + - '0' + status: + code: 200 + message: OK +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '0' + Cookie: + - guest_id=v1%3A163514669039140044; personalization_id="v1_f6Q7tQ/Do3NkovyN4sJfog==" + User-Agent: + - Python/3.10.0 Requests/2.26.0 Tweepy/4.1.0 + method: DELETE + uri: https://api.twitter.com/2/lists/1452536616203997190/members/783214 + response: + body: + string: !!binary | + H4sIAAAAAAAAAKpWSkksSVSyqlbKLI7PTc1NSi1SskpLzClOra0FAAAA//8DAPnKNe0cAAAA + headers: + api-version: + - '2.27' + cache-control: + - no-cache, no-store, max-age=0 + content-disposition: + - attachment; filename=json.json + content-encoding: + - gzip + content-length: + - '54' + content-type: + - application/json; charset=utf-8 + date: + - Mon, 25 Oct 2021 07:24:51 UTC + server: + - tsa_b + strict-transport-security: + - max-age=631138519 + x-access-level: + - read-write-directmessages + x-connection-hash: + - b20657636698db22333ea81a4e5d78f071184b672a5e536b229e6cada14038b3 + x-content-type-options: + - nosniff + x-frame-options: + - SAMEORIGIN + x-rate-limit-limit: + - '300' + x-rate-limit-remaining: + - '299' + x-rate-limit-reset: + - '1635147591' + x-response-time: + - '66' + x-xss-protection: + - '0' + status: + code: 200 + message: OK +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '0' + Cookie: + - guest_id=v1%3A163514669039140044; personalization_id="v1_f6Q7tQ/Do3NkovyN4sJfog==" + User-Agent: + - Python/3.10.0 Requests/2.26.0 Tweepy/4.1.0 + method: DELETE + uri: https://api.twitter.com/2/users/1072250532645998596/pinned_lists/1452536616203997190 + response: + body: + string: !!binary | + H4sIAAAAAAAAAKpWSkksSVSyqlYqyMzLS01RskpLzClOra0FAAAA//8DAIhhZ3AZAAAA + headers: + api-version: + - '2.27' + 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: + - Mon, 25 Oct 2021 07:24:51 UTC + server: + - tsa_b + strict-transport-security: + - max-age=631138519 + x-access-level: + - read-write-directmessages + x-connection-hash: + - b20657636698db22333ea81a4e5d78f071184b672a5e536b229e6cada14038b3 + x-content-type-options: + - nosniff + x-frame-options: + - SAMEORIGIN + x-rate-limit-limit: + - '50' + x-rate-limit-remaining: + - '49' + x-rate-limit-reset: + - '1635147591' + x-response-time: + - '46' + x-xss-protection: + - '0' + status: + code: 200 + message: OK +- request: + body: '{"description": "Test List Description"}' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '40' + Content-Type: + - application/json + Cookie: + - guest_id=v1%3A163514669039140044; personalization_id="v1_f6Q7tQ/Do3NkovyN4sJfog==" + User-Agent: + - Python/3.10.0 Requests/2.26.0 Tweepy/4.1.0 + method: PUT + uri: https://api.twitter.com/2/lists/1452536616203997190 + response: + body: + string: !!binary | + H4sIAAAAAAAAAKpWKi1ISSxJTVGyKikqTa0FAAAA//8DACu2J84QAAAA + headers: + api-version: + - '2.27' + cache-control: + - no-cache, no-store, max-age=0 + content-disposition: + - attachment; filename=json.json + content-encoding: + - gzip + content-length: + - '42' + content-type: + - application/json; charset=utf-8 + date: + - Mon, 25 Oct 2021 07:24:51 UTC + server: + - tsa_b + strict-transport-security: + - max-age=631138519 + x-access-level: + - read-write-directmessages + x-connection-hash: + - b20657636698db22333ea81a4e5d78f071184b672a5e536b229e6cada14038b3 + x-content-type-options: + - nosniff + x-frame-options: + - SAMEORIGIN + x-rate-limit-limit: + - '300' + x-rate-limit-remaining: + - '298' + x-rate-limit-reset: + - '1635147202' + x-response-time: + - '387' + x-xss-protection: + - '0' + status: + code: 200 + message: OK +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '0' + Cookie: + - guest_id=v1%3A163514669039140044; personalization_id="v1_f6Q7tQ/Do3NkovyN4sJfog==" + User-Agent: + - Python/3.10.0 Requests/2.26.0 Tweepy/4.1.0 + method: DELETE + uri: https://api.twitter.com/2/lists/1452536616203997190 + response: + body: + string: !!binary | + H4sIAAAAAAAAAKpWSkksSVSyqlZKSc1JLUlNUbIqKSpNra0FAAAA//8DAD5d+0oZAAAA + headers: + api-version: + - '2.27' + 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: + - Mon, 25 Oct 2021 07:24:51 UTC + server: + - tsa_b + strict-transport-security: + - max-age=631138519 + x-access-level: + - read-write-directmessages + x-connection-hash: + - b20657636698db22333ea81a4e5d78f071184b672a5e536b229e6cada14038b3 + x-content-type-options: + - nosniff + x-frame-options: + - SAMEORIGIN + x-rate-limit-limit: + - '300' + x-rate-limit-remaining: + - '299' + x-rate-limit-reset: + - '1635147591' + x-response-time: + - '50' + x-xss-protection: + - '0' + status: + code: 200 + message: OK +version: 1 diff --git a/docs/client.rst b/docs/client.rst index f4c488bee..4c3f6f7c5 100644 --- a/docs/client.rst +++ b/docs/client.rst @@ -120,6 +120,30 @@ Spaces lookup .. automethod:: Client.get_space +Lists +===== + +Manage Lists +------------ + +.. automethod:: Client.delete_list + +.. automethod:: Client.remove_list_member + +.. automethod:: Client.unfollow_list + +.. automethod:: Client.unpin_list + +.. automethod:: Client.update_list + +.. automethod:: Client.create_list + +.. automethod:: Client.add_list_member + +.. automethod:: Client.follow_list + +.. automethod:: Client.pin_list + Compliance ========== diff --git a/tests/test_client.py b/tests/test_client.py index 4d6f99c03..a7bea722c 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -147,6 +147,25 @@ def test_get_space(self): # https://twitter.com/TwitterSpaces/status/1436382283347283969 self.client.get_space(space_id) + @tape.use_cassette("test_follow_and_unfollow_list.yaml", + serializer = "yaml") + def test_follow_and_unfollow_list(self): + list_id = 84839422 # List ID for Official Twitter Accounts (@Twitter) + self.client.follow_list(list_id) + self.client.unfollow_list(list_id) + + @tape.use_cassette("test_manage_list.yaml", serializer="yaml") + def test_manage_list(self): + response = self.client.create_list("Test List", private=True) + list_id = response.data["id"] + user_id = 783214 # User ID for @Twitter + self.client.add_list_member(list_id, user_id) + self.client.pin_list(list_id) + self.client.remove_list_member(list_id, user_id) + self.client.unpin_list(list_id) + self.client.update_list(list_id, description="Test List Description") + self.client.delete_list(list_id) + @tape.use_cassette("test_create_and_get_compliance_job_and_jobs.yaml", serializer="yaml") def test_create_and_get_compliance_job_and_jobs(self): diff --git a/tweepy/client.py b/tweepy/client.py index efae557ca..55aaf04bf 100644 --- a/tweepy/client.py +++ b/tweepy/client.py @@ -1687,6 +1687,238 @@ def get_space(self, id, **params): ), data_type=Space ) + # Manage Lists + + def delete_list(self, id): + """Enables the authenticated user to delete a List that they own. + + Parameters + ---------- + id : Union[int, str] + The ID of the List to be deleted. + + Returns + ------- + Union[dict, requests.Response, Response] + + References + ---------- + https://developer.twitter.com/en/docs/twitter-api/lists/manage-lists/api-reference/delete-lists-id + """ + + return self._make_request( + "DELETE", f"/2/lists/{id}", user_auth=True + ) + + def remove_list_member(self, id, user_id): + """Enables the authenticated user to remove a member from a List they + own. + + Parameters + ---------- + id : Union[int, str] + The ID of the List you are removing a member from. + user_id : Union[int, str] + The ID of the user you wish to remove as a member of the List. + + Returns + ------- + Union[dict, requests.Response, Response] + + References + ---------- + https://developer.twitter.com/en/docs/twitter-api/lists/manage-lists/api-reference/delete-lists-id-members-user_id + """ + + return self._make_request( + "DELETE", f"/2/lists/{id}/members/{user_id}", user_auth=True + ) + + def unfollow_list(self, list_id): + """Enables the authenticated user to unfollow a List. + + Parameters + ---------- + list_id : Union[int, str] + The ID of the List that you would like the user to unfollow. + + Returns + ------- + Union[dict, requests.Response, Response] + + References + ---------- + https://developer.twitter.com/en/docs/twitter-api/lists/manage-lists/api-reference/delete-users-id-followed-lists-list_id + """ + id = self.access_token.partition('-')[0] + route = f"/2/users/{id}/followed_lists/{list_id}" + + return self._make_request( + "DELETE", route, user_auth=True + ) + + def unpin_list(self, list_id): + """Enables the authenticated user to unpin a List. + + Parameters + ---------- + list_id : Union[int, str] + The ID of the List that you would like the user to unpin. + + Returns + ------- + Union[dict, requests.Response, Response] + + References + ---------- + https://developer.twitter.com/en/docs/twitter-api/lists/manage-lists/api-reference/delete-users-id-pinned-lists-list_id + """ + id = self.access_token.partition('-')[0] + route = f"/2/users/{id}/pinned_lists/{list_id}" + + return self._make_request( + "DELETE", route, user_auth=True + ) + + def update_list(self, id, *, description=None, name=None, private=None): + """Enables the authenticated user to update the meta data of a + specified List that they own. + + Parameters + ---------- + id : Union[int, str] + The ID of the List to be updated. + description : str + Updates the description of the List. + name : str + Updates the name of the List. + private : str + Determines whether the List should be private. + + Returns + ------- + Union[dict, requests.Response, Response] + + References + ---------- + https://developer.twitter.com/en/docs/twitter-api/lists/manage-lists/api-reference/put-lists-id + """ + json = {} + + if description is not None: + json["description"] = description + + if name is not None: + json["name"] = name + + if private is not None: + json["private"] = private + + return self._make_request( + "PUT", f"/2/lists/{id}", json=json, user_auth=True + ) + + def create_list(self, name, *, description=None, private=None): + """Enables the authenticated user to create a List. + + Parameters + ---------- + name : str + The name of the List you wish to create. + description : str + Description of the List. + private : bool + Determine whether the List should be private. + + Returns + ------- + Union[dict, requests.Response, Response] + + References + ---------- + https://developer.twitter.com/en/docs/twitter-api/lists/manage-lists/api-reference/post-lists + """ + json = {"name": name} + + if description is not None: + json["description"] = description + + if private is not None: + json["private"] = private + + return self._make_request( + "POST", f"/2/lists", json=json, user_auth=True + ) + + def add_list_member(self, id, user_id): + """Enables the authenticated user to add a member to a List they own. + + Parameters + ---------- + id : Union[int, str] + The ID of the List you are adding a member to. + user_id : Union[int, str] + The ID of the user you wish to add as a member of the List. + + Returns + ------- + Union[dict, requests.Response, Response] + + References + ---------- + https://developer.twitter.com/en/docs/twitter-api/lists/manage-lists/api-reference/post-lists-id-members + """ + return self._make_request( + "POST", f"/2/lists/{id}/members", json={"user_id": str(user_id)}, + user_auth=True + ) + + def follow_list(self, list_id): + """Enables the authenticated user to follow a List. + + Parameters + ---------- + list_id : Union[int, str] + The ID of the List that you would like the user to follow. + + Returns + ------- + Union[dict, requests.Response, Response] + + References + ---------- + https://developer.twitter.com/en/docs/twitter-api/lists/manage-lists/api-reference/post-users-id-followed-lists + """ + id = self.access_token.partition('-')[0] + route = f"/2/users/{id}/followed_lists" + + return self._make_request( + "POST", route, json={"list_id": str(list_id)}, user_auth=True + ) + + def pin_list(self, list_id): + """Enables the authenticated user to pin a List. + + Parameters + ---------- + list_id : Union[int, str] + The ID of the List that you would like the user to pin. + + Returns + ------- + Union[dict, requests.Response, Response] + + References + ---------- + https://developer.twitter.com/en/docs/twitter-api/lists/manage-lists/api-reference/post-users-id-pinned-lists + """ + id = self.access_token.partition('-')[0] + route = f"/2/users/{id}/pinned_lists" + + return self._make_request( + "POST", route, json={"list_id": str(list_id)}, user_auth=True + ) + # Batch Compliance def get_compliance_jobs(self, type, **params):