From 89a29e68dadfae301e82e226fcbab7b51814d03c Mon Sep 17 00:00:00 2001 From: trevor hobenshield Date: Sun, 18 Jun 2023 11:59:00 -0700 Subject: [PATCH] add more dm operations --- readme.md | 22 ++++++++---- setup.py | 23 ++++++++---- setup.sh | 4 +-- twitter/account.py | 83 +++++++++++++++++++++++++++++++++++++------- twitter/constants.py | 3 +- 5 files changed, 106 insertions(+), 29 deletions(-) diff --git a/readme.md b/readme.md index 44d086b..b8a859f 100644 --- a/readme.md +++ b/readme.md @@ -119,14 +119,24 @@ latest_timeline = account.home_latest_timeline(limit=500) # get bookmarks bookmarks = account.bookmarks() -# get all dms -dms = account.dm_history(['12345-67890']) +# get DM inbox metadata +inbox = account.dm_inbox() -# search dms -dms = account.dm_search('test') +# get DMs from all conversations +dms = account.dm_history() + +# get DMs from specific conversations +dms = account.dm_history(['123456-789012', '345678-901234']) + +# search DMs by keyword +dms = account.dm_search('test123') + +# delete entire conversation +account.dm_delete(conversation_id='123456-789012') + +# delete (hide) specific DM +account.dm_delete(message_id='123456') -# delete conversation -account.dm_delete('12345-67890') # example configuration account.update_settings({ diff --git a/setup.py b/setup.py index 7c45ddf..31cc402 100644 --- a/setup.py +++ b/setup.py @@ -14,7 +14,7 @@ install_requires = [ setup( name="twitter-api-client", - version="0.9.6", + version="0.9.7", python_requires=">=3.10.10", description="Twitter API", long_description=dedent(''' @@ -138,14 +138,23 @@ setup( # get bookmarks bookmarks = account.bookmarks() - # get all dms - dms = account.dm_history(['12345-67890']) + # get DM inbox metadata + inbox = account.dm_inbox() - # search dms - dms = account.dm_search('test') + # get DMs from all conversations + dms = account.dm_history() - # delete conversation - account.dm_delete('12345-67890') + # get DMs from specific conversations + dms = account.dm_history(['123456-789012', '345678-901234']) + + # search DMs by keyword + dms = account.dm_search('test123') + + # delete entire conversation + account.dm_delete(conversation_id='123456-789012') + + # delete (hide) specific DM + account.dm_delete(message_id='123456') # example configuration account.update_settings({ diff --git a/setup.sh b/setup.sh index d9504cd..308b2af 100644 --- a/setup.sh +++ b/setup.sh @@ -1,4 +1,4 @@ #!/usr/bin/bash -python3 -m build -python3 -m twine upload dist/* \ No newline at end of file +python -m build +python -m twine upload dist/* \ No newline at end of file diff --git a/twitter/account.py b/twitter/account.py index e4b9041..067bc9a 100644 --- a/twitter/account.py +++ b/twitter/account.py @@ -606,15 +606,37 @@ class Account: raise Exception('Session not authenticated. ' 'Please use an authenticated session or remove the `session` argument and try again.') - def dm_history(self, conversation_ids: list[str]) -> list[dict]: + def dm_inbox(self) -> dict: + """ + Get DM inbox metadata. + + @return: inbox as dict + """ + r = self.session.get( + f'{self.v1_api}/dm/inbox_initial_state.json', + headers=get_headers(self.session), + params=dm_params + ) + return r.json() + + def dm_history(self, conversation_ids: list[str] = None) -> list[dict]: + """ + Get DM history. + + Call without arguments to get all DMS from all conversations. + + @param conversation_ids: optional list of conversation ids + @return: list of messages as dicts + """ + async def get(session: AsyncClient, conversation_id: str): - params = deepcopy(dm_history_params) + params = deepcopy(dm_params) r = await session.get( f'{self.v1_api}/dm/conversation/{conversation_id}.json', params=params, ) res = r.json().get('conversation_timeline', {}) - data = [x['message'] for x in res.get('entries', [])] + data = [x.get('message') for x in res.get('entries', [])] entry_id = res.get('min_entry_id') while entry_id: params['max_id'] = entry_id @@ -627,33 +649,68 @@ class Account: entry_id = res.get('min_entry_id') return data - async def process(): + async def process(ids): limits = Limits(max_connections=100) headers, cookies = get_headers(self.session), self.session.cookies async with AsyncClient(limits=limits, headers=headers, cookies=cookies, timeout=20) as c: - return await tqdm_asyncio.gather(*(get(c, _id) for _id in conversation_ids), desc="Getting DMs") + return await tqdm_asyncio.gather(*(get(c, _id) for _id in ids), desc="Getting DMs") - return asyncio.run(process()) + if conversation_ids: + ids = conversation_ids + else: + # get all conversations + inbox = self.dm_inbox() + ids = list(inbox['inbox_initial_state']['conversations']) - def dm_delete(self, conversation_id: str): - return self.session.post( - f'{self.v1_api}/dm/conversation/{conversation_id}/delete.json', - headers=get_headers(self.session), - ) + return asyncio.run(process(ids)) + + def dm_delete(self, *, conversation_id: str = None, message_id: str = None) -> dict: + """ + Delete operations + + - delete (hide) a single DM + - delete an entire conversation + + @param conversation_id: the conversation id + @param message_id: the message id + @return: result metadata + """ + self.session.headers.update(headers=get_headers(self.session)) + results = {'conversation': None, 'message': None} + if conversation_id: + results['conversation'] = self.session.post( + f'{self.v1_api}/dm/conversation/{conversation_id}/delete.json', + ).text # not json response + if message_id: + # delete single message + _id, op = Operation.DMMessageDeleteMutation + results['message'] = self.session.post( + f'https://twitter.com/i/api/graphql/{_id}/{op}', + json={'queryId': _id, 'variables': {'messageId': message_id}}, + ).json() + return results + + def dm_search(self, query: str) -> dict: + """ + Search DMs by keyword + + @param query: search term + @return: search results as dict + """ - def dm_search(self, query: str): def get(cursor=None): if cursor: params['variables']['cursor'] = cursor.pop() _id, op = Operation.DmAllSearchSlice r = self.session.get( f'https://twitter.com/i/api/graphql/{_id}/{op}', - params=build_params(params) + params=build_params(params), ) res = r.json() cursor = find_key(res, 'next_cursor') return res, cursor + self.session.headers.update(headers=get_headers(self.session)) variables = deepcopy(Operation.default_variables) variables['count'] = 50 # strict limit, errors thrown if exceeded variables['query'] = query diff --git a/twitter/constants.py b/twitter/constants.py index d9c85ae..4d783f6 100644 --- a/twitter/constants.py +++ b/twitter/constants.py @@ -215,6 +215,7 @@ class Operation: DmAllSearchSlice = 'U-QXVRZ6iddb1QuZweh5DQ', 'DmAllSearchSlice' DmGroupSearchSlice = '5zpY1dCR-8NyxQJS_CFJoQ', 'DmGroupSearchSlice' DmMutedTimeline = 'lrcWa13oyrQc7L33wRdLAQ', 'DmMutedTimeline' + DMMessageDeleteMutation = 'BJ6DtxA2llfjnRoRjaiIiw', 'DMMessageDeleteMutation' DmNsfwMediaFilterUpdate = 'of_N6O33zfyD4qsFJMYFxA', 'DmNsfwMediaFilterUpdate' DmPeopleSearchSlice = 'xYSm8m5kJnzm_gFCn5GH-w', 'DmPeopleSearchSlice' EditBookmarkFolder = 'a6kPp1cS1Dgbsjhapz1PNw', 'EditBookmarkFolder' @@ -561,7 +562,7 @@ search_config = { 'ext': 'mediaStats,highlightedLabel,hasNftAvatar,voiceInfo,birdwatchPivot,enrichments,superFollowMetadata,unmentionInfo,editControl,collab_control,vibe' } -dm_history_params = { +dm_params = { 'context': 'FETCH_DM_CONVERSATION', 'include_profile_interstitial_type': '1', 'include_blocking': '1',