Skip to content

Commit 66e2912

Browse files
authored
Add files via upload
1 parent 539b1a3 commit 66e2912

File tree

8 files changed

+417
-42
lines changed

8 files changed

+417
-42
lines changed

twikit/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@
66
A Python library for interacting with the Twitter API.
77
"""
88

9-
__version__ = '1.5.5'
9+
__version__ = '1.5.6'
1010

11+
from .bookmark import BookmarkFolder
1112
from .client import Client
1213
from .community import (Community, CommunityCreator, CommunityMember,
1314
CommunityRule)

twikit/client.py

Lines changed: 184 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from fake_useragent import UserAgent
1212
from httpx import Response
1313

14+
from .bookmark import BookmarkFolder
1415
from .community import Community, CommunityMember
1516
from .errors import (
1617
CouldNotTweet,
@@ -30,6 +31,7 @@
3031
from .tweet import CommunityNote, Poll, ScheduledTweet, Tweet
3132
from .user import User
3233
from .utils import (
34+
BOOKMARK_FOLDER_TIMELINE_FEATURES,
3335
COMMUNITY_TWEETS_FEATURES,
3436
COMMUNITY_NOTE_FEATURES,
3537
JOIN_COMMUNITY_FEATURES,
@@ -1432,6 +1434,8 @@ def get_tweet_by_id(
14321434
show_replies = None
14331435
# Reply to reply
14341436
for reply in entry['content']['items'][1:]:
1437+
if 'tweetcomposer' in reply['entryId']:
1438+
continue
14351439
if 'tweet' in find_dict(reply, 'result'):
14361440
reply = reply['tweet']
14371441
if 'tweet' in reply.get('entryId'):
@@ -2134,14 +2138,18 @@ def delete_retweet(self, tweet_id: str) -> Response:
21342138
)
21352139
return response
21362140

2137-
def bookmark_tweet(self, tweet_id: str) -> Response:
2141+
def bookmark_tweet(
2142+
self, tweet_id: str, folder_id: str | None = None
2143+
) -> Response:
21382144
"""
21392145
Adds the tweet to bookmarks.
21402146
21412147
Parameters
21422148
----------
21432149
tweet_id : :class:`str`
21442150
The ID of the tweet to be bookmarked.
2151+
folder_id : :class:`str` | None, default=None
2152+
The ID of the folder to add the bookmark to.
21452153
21462154
Returns
21472155
-------
@@ -2152,18 +2160,20 @@ def bookmark_tweet(self, tweet_id: str) -> Response:
21522160
--------
21532161
>>> tweet_id = '...'
21542162
>>> client.bookmark_tweet(tweet_id)
2155-
2156-
See Also
2157-
--------
2158-
.bookmark_tweet
21592163
"""
2164+
variables = {'tweet_id': tweet_id}
2165+
if folder_id is None:
2166+
endpoint = Endpoint.CREATE_BOOKMARK
2167+
else:
2168+
endpoint = Endpoint.BOOKMARK_TO_FOLDER
2169+
variables['bookmark_collection_id'] = folder_id
21602170

21612171
data = {
2162-
'variables': {'tweet_id': tweet_id},
2172+
'variables': variables,
21632173
'queryId': get_query_id(Endpoint.CREATE_BOOKMARK)
21642174
}
21652175
response = self.http.post(
2166-
Endpoint.CREATE_BOOKMARK,
2176+
endpoint,
21672177
json=data,
21682178
headers=self._base_headers
21692179
)
@@ -2204,17 +2214,18 @@ def delete_bookmark(self, tweet_id: str) -> Response:
22042214
return response
22052215

22062216
def get_bookmarks(
2207-
self, count: int = 20, cursor: str | None = None
2217+
self, count: int = 20,
2218+
cursor: str | None = None, folder_id: str | None = None
22082219
) -> Result[Tweet]:
22092220
"""
22102221
Retrieves bookmarks from the authenticated user's Twitter account.
22112222
22122223
Parameters
22132224
----------
22142225
count : :class:`int`, default=20
2215-
The number of bookmarks to retrieve (default is 20).
2216-
cursor : :class:`str`, default=None
2217-
A cursor to paginate through the bookmarks (default is None).
2226+
The number of bookmarks to retrieve.
2227+
folder_id : :class:`str` | None, default=None
2228+
Folder to retrieve bookmarks.
22182229
22192230
Returns
22202231
-------
@@ -2240,17 +2251,24 @@ def get_bookmarks(
22402251
'count': count,
22412252
'includePromotedContent': True
22422253
}
2254+
if folder_id is None:
2255+
endpoint = Endpoint.BOOKMARKS
2256+
features = FEATURES | {
2257+
'graphql_timeline_v2_bookmark_timeline': True
2258+
}
2259+
else:
2260+
endpoint = Endpoint.BOOKMARK_FOLDER_TIMELINE
2261+
variables['bookmark_collection_id'] = folder_id
2262+
features = BOOKMARK_FOLDER_TIMELINE_FEATURES
2263+
22432264
if cursor is not None:
22442265
variables['cursor'] = cursor
2245-
features = FEATURES | {
2246-
'graphql_timeline_v2_bookmark_timeline': True
2247-
}
22482266
params = flatten_params({
22492267
'variables': variables,
22502268
'features': features
22512269
})
22522270
response = self.http.get(
2253-
Endpoint.BOOKMARKS,
2271+
endpoint,
22542272
params=params,
22552273
headers=self._base_headers
22562274
).json()
@@ -2260,7 +2278,13 @@ def get_bookmarks(
22602278
return Result([])
22612279
items = items_[0]
22622280
next_cursor = items[-1]['content']['value']
2263-
previous_cursor = items[-2]['content']['value']
2281+
if folder_id is None:
2282+
previous_cursor = items[-2]['content']['value']
2283+
fetch_previous_result = partial(self.get_bookmarks, count,
2284+
previous_cursor, folder_id)
2285+
else:
2286+
previous_cursor = None
2287+
fetch_previous_result = None
22642288

22652289
results = []
22662290
for item in items:
@@ -2272,9 +2296,9 @@ def get_bookmarks(
22722296

22732297
return Result(
22742298
results,
2275-
partial(self.get_bookmarks, count, next_cursor),
2299+
partial(self.get_bookmarks, count, next_cursor, folder_id),
22762300
next_cursor,
2277-
partial(self.get_bookmarks, count, previous_cursor),
2301+
fetch_previous_result,
22782302
previous_cursor
22792303
)
22802304

@@ -2302,6 +2326,148 @@ def delete_all_bookmarks(self) -> Response:
23022326
)
23032327
return response
23042328

2329+
def get_bookmark_folders(
2330+
self, cursor: str | None = None
2331+
) -> Result[BookmarkFolder]:
2332+
"""
2333+
Retrieves bookmark folders.
2334+
2335+
Returns
2336+
-------
2337+
Result[:class:`BookmarkFolder`]
2338+
Result object containing a list of bookmark folders.
2339+
2340+
Examples
2341+
--------
2342+
>>> folders = client.get_bookmark_folders()
2343+
>>> print(folders)
2344+
[<BookmarkFolder id="...">, ..., <BookmarkFolder id="...">]
2345+
>>> more_folders = folders.next() # Retrieve more folders
2346+
"""
2347+
variables = {}
2348+
if cursor is not None:
2349+
variables['cursor'] = cursor
2350+
params = flatten_params({'variables': variables})
2351+
response = self.http.get(
2352+
Endpoint.BOOKMARK_FOLDERS,
2353+
params=params,
2354+
headers=self._base_headers
2355+
).json()
2356+
2357+
slice = find_dict(response, 'bookmark_collections_slice')[0]
2358+
results = []
2359+
for item in slice['items']:
2360+
results.append(BookmarkFolder(self, item))
2361+
2362+
if 'next_cursor' in slice['slice_info']:
2363+
next_cursor = slice['slice_info']['next_cursor']
2364+
fetch_next_result = partial(self.get_bookmark_folders, next_cursor)
2365+
else:
2366+
next_cursor = None
2367+
fetch_next_result = None
2368+
2369+
return Result(
2370+
results,
2371+
fetch_next_result,
2372+
next_cursor
2373+
)
2374+
2375+
def edit_bookmark_folder(
2376+
self, folder_id: str, name: str
2377+
) -> BookmarkFolder:
2378+
"""
2379+
Edits a bookmark folder.
2380+
2381+
Parameters
2382+
----------
2383+
folder_id : :class:`str`
2384+
ID of the folder to edit.
2385+
name : :class:`str`
2386+
New name for the folder.
2387+
2388+
Returns
2389+
-------
2390+
:class:`BookmarkFolder`
2391+
Updated bookmark folder.
2392+
2393+
Examples
2394+
--------
2395+
>>> client.edit_bookmark_folder('123456789', 'MyFolder')
2396+
"""
2397+
variables = {
2398+
'bookmark_collection_id': folder_id,
2399+
'name': name
2400+
}
2401+
data = {
2402+
'variables': variables,
2403+
'queryId': get_query_id(Endpoint.EDIT_BOOKMARK_FOLDER)
2404+
}
2405+
response = self.http.post(
2406+
Endpoint.EDIT_BOOKMARK_FOLDER,
2407+
json=data,
2408+
headers=self._base_headers
2409+
).json()
2410+
return BookmarkFolder(
2411+
self, response['data']['bookmark_collection_update']
2412+
)
2413+
2414+
def delete_bookmark_folder(self, folder_id: str) -> Response:
2415+
"""
2416+
Deletes a bookmark folder.
2417+
2418+
Parameters
2419+
----------
2420+
folder_id : :class:`str`
2421+
ID of the folder to delete.
2422+
2423+
Returns
2424+
-------
2425+
:class:`httpx.Response`
2426+
Response returned from twitter api.
2427+
"""
2428+
variables = {
2429+
'bookmark_collection_id': folder_id
2430+
}
2431+
data = {
2432+
'variables': variables,
2433+
'queryId': get_query_id(Endpoint.DELETE_BOOKMARK_FOLDER)
2434+
}
2435+
response = self.http.post(
2436+
Endpoint.DELETE_BOOKMARK_FOLDER,
2437+
json=data,
2438+
headers=self._base_headers
2439+
)
2440+
return response
2441+
2442+
def create_bookmark_folder(self, name: str) -> BookmarkFolder:
2443+
"""Creates a bookmark folder.
2444+
2445+
Parameters
2446+
----------
2447+
name : :class:`str`
2448+
Name of the folder.
2449+
2450+
Returns
2451+
-------
2452+
:class:`BookmarkFolder`
2453+
Newly created bookmark folder.
2454+
"""
2455+
variables = {
2456+
'name': name
2457+
}
2458+
data = {
2459+
'variables': variables,
2460+
'queryId': get_query_id(Endpoint.CREATE_BOOKMARK_FOLDER)
2461+
}
2462+
response = self.http.post(
2463+
Endpoint.CREATE_BOOKMARK_FOLDER,
2464+
json=data,
2465+
headers=self._base_headers
2466+
).json()
2467+
return BookmarkFolder(
2468+
self, response['data']['bookmark_collection_create']
2469+
)
2470+
23052471
def follow_user(self, user_id: str) -> Response:
23062472
"""
23072473
Follows a user.

twikit/errors.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
from __future__ import annotations
2+
3+
14
class TwitterException(Exception):
25
"""
36
Base class for Twitter API related exceptions.

twikit/tweet.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -147,15 +147,17 @@ def __init__(self, client: Client, data: dict, user: User = None) -> None:
147147
self.reply_count: int = legacy['reply_count']
148148
self.favorite_count: int = legacy['favorite_count']
149149
self.favorited: bool = legacy['favorited']
150-
self.view_count: int = data['views'].get('count') if 'views' in data else None
150+
self.view_count: int = (data['views'].get('count')
151+
if 'views' in data else None)
151152
self.retweet_count: int = legacy['retweet_count']
152153
self.editable_until_msecs: int = data['edit_control'].get(
153154
'editable_until_msecs')
154155
self.is_translatable: bool = data.get('is_translatable')
155156
self.is_edit_eligible: bool = data['edit_control'].get(
156157
'is_edit_eligible')
157158
self.edits_remaining: int = data['edit_control'].get('edits_remaining')
158-
self.state: str = data['views'].get('state') if 'views' in data else None
159+
self.state: str = (data['views'].get('state')
160+
if 'views' in data else None)
159161
self.has_community_notes: bool = data.get('has_birdwatch_notes')
160162

161163
if 'birdwatch_pivot' in data:

twikit/twikit_async/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
if os.name == 'nt':
55
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
66

7+
from .bookmark import BookmarkFolder
78
from ..errors import *
89
from ..utils import build_query
910
from .client import Client

0 commit comments

Comments
 (0)