├── .gitattributes
├── .gitignore
├── README.md
├── docs
├── Makefile
├── basic
│ ├── all-functions.rst
│ ├── events.rst
│ ├── exceptions.rst
│ ├── filter.rst
│ ├── installation.rst
│ ├── quick-start.rst
│ ├── singing-in.rst
│ ├── twDataTypes.rst
│ └── twitter-class.rst
├── conf.py
├── index.rst
├── make.bat
└── misc
│ └── changelog.rst
├── pyproject.toml
├── requirements.txt
├── setup.cfg
├── setup.py
└── src
└── tweety
├── LICENSE
├── __init__.py
├── auth.py
├── bot.py
├── builder.py
├── captcha
├── __init__.py
├── anticaptcha.py
├── base.py
├── capsolver.py
└── two_captcha.py
├── constants.py
├── events
├── __init__.py
├── base.py
├── newmessage.py
└── stream_event.py
├── exceptions.py
├── exceptions_.py
├── filters.py
├── http.py
├── session.py
├── transaction.py
├── types
├── __init__.py
├── base.py
├── bookmarks.py
├── community.py
├── follow.py
├── gifs.py
├── grok.py
├── inbox.py
├── likes.py
├── lists.py
├── mentions.py
├── n_types.py
├── notification.py
├── places.py
├── retweets.py
├── search.py
├── topic.py
├── twDataTypes.py
└── usertweet.py
├── updates.py
├── user.py
└── utils.py
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Auto detect text files and perform LF normalization
2 | * text=auto
3 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | build/
12 | develop-eggs/
13 | dist/
14 | downloads/
15 | eggs/
16 | .eggs/
17 | lib/
18 | lib64/
19 | parts/
20 | sdist/
21 | var/
22 | wheels/
23 | *.egg-info/
24 | .installed.cfg
25 | *.egg
26 | MANIFEST
27 |
28 | # PyInstaller
29 | # Usually these files are written by a python script from a template
30 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
31 | *.manifest
32 | *.spec
33 |
34 | # Installer logs
35 | pip-log.txt
36 | pip-delete-this-directory.txt
37 |
38 | # Unit test / coverage reports
39 | htmlcov/
40 | .tox/
41 | .nox/
42 | .coverage
43 | .coverage.*
44 | .cache
45 | nosetests.xml
46 | coverage.xml
47 | *.cover
48 | .hypothesis/
49 | .pytest_cache/
50 |
51 | # Translations
52 | *.mo
53 | *.pot
54 |
55 | # Django stuff:
56 | *.log
57 | local_settings.py
58 | db.sqlite3
59 |
60 | # Flask stuff:
61 | instance/
62 | .webassets-cache
63 |
64 | # Scrapy stuff:
65 | .scrapy
66 |
67 | # Sphinx documentation
68 | docs/_build/
69 |
70 | # PyBuilder
71 | target/
72 |
73 | # Jupyter Notebook
74 | .ipynb_checkpoints
75 |
76 | # IPython
77 | profile_default/
78 | ipython_config.py
79 |
80 | # pyenv
81 | .python-version
82 |
83 | # celery beat schedule file
84 | celerybeat-schedule
85 |
86 | # SageMath parsed files
87 | *.sage.py
88 |
89 | # Environments
90 | .env
91 | .venv
92 | env/
93 | venv/
94 | ENV/
95 | env.bak/
96 | venv.bak/
97 |
98 | # Spyder project settings
99 | .spyderproject
100 | .spyproject
101 |
102 | # Rope project settings
103 | .ropeproject
104 |
105 | # mkdocs documentation
106 | /site
107 |
108 | # mypy
109 | .mypy_cache/
110 | .dmypy.json
111 | dmypy.json
112 |
113 | # Pyre type checker
114 | .pyre/
115 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # tweety
2 | Reverse Engineered Twitter Frontend API.
3 |
4 | [](https://pepy.tech/project/tweety-ns) [](https://deepwiki.com/mahrtayyab/tweety)
5 |
6 | ## Installation:
7 | ```bash
8 | pip install tweety-ns
9 | ```
10 |
11 | ## Keep synced with latest fixes
12 |
13 | ##### **Pip might not be always updated , so to keep everything synced.**
14 |
15 | ```bash
16 | pip install https://github.com/mahrtayyab/tweety/archive/main.zip --upgrade
17 | ```
18 |
19 | ## A Quick Example:
20 | ```python
21 | from tweety import TwitterAsync
22 | import asyncio
23 |
24 | async def main():
25 |
26 | app = TwitterAsync("session")
27 | all_tweets = await app.get_tweets("elonmusk")
28 | for tweet in all_tweets:
29 | print(tweet)
30 |
31 | asyncio.run(main())
32 | ```
33 |
34 | > [!IMPORTANT]
35 | > Even Twitter Web Client has a lot of rate limits now, Abusing tweety can lead to `read_only` Twitter account.
36 |
37 | Do check [FAQs](https://github.com/mahrtayyab/tweety/wiki/FAQs)
38 |
39 | Full Documentation and Changelogs are [here](https://mahrtayyab.github.io/tweety_docs/)
40 |
--------------------------------------------------------------------------------
/docs/Makefile:
--------------------------------------------------------------------------------
1 | # Minimal makefile for Sphinx documentation
2 | #
3 |
4 | # You can set these variables from the command line.
5 | SPHINXOPTS =
6 | SPHINXBUILD = sphinx-build
7 | SPHINXPROJ = Tweety
8 | SOURCEDIR = .
9 | BUILDDIR = _build
10 |
11 | # Put it first so that "make" without argument is like "make help".
12 | help:
13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
14 |
15 | .PHONY: help Makefile
16 |
17 | # Catch-all target: route all unknown targets to Sphinx using the new
18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
19 | %: Makefile
20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
--------------------------------------------------------------------------------
/docs/basic/events.rst:
--------------------------------------------------------------------------------
1 | .. _events:
2 |
3 | ===============
4 | Events
5 | ===============
6 |
7 | Using Tweety you can also listen to the events and respond to them.
8 |
9 | Listen for new Message
10 | -----------------------
11 |
12 | .. py:class:: NewMessageUpdate
13 |
14 | :reference: `tweety.events.newmessage.NewMessageUpdate`
15 |
16 | You can listen to new messages using this method. Check the example below for reference
17 |
18 | .. code-block:: python
19 |
20 | from tweety import TwitterAsync
21 | from tweety import events
22 |
23 |
24 | cookies = cookies_value
25 | client = Twitter(cookies=cookies)
26 | @client.on(events.NewMessageUpdate)
27 | async def newMessage(event):
28 | await event.respond("OKAY")
29 |
30 | client.run_until_disconnected()
31 |
32 | The user will register a new event handler `NewMessageUpdate` which will start listening for new messages. When this event triggers
33 | the function `newMessage` will be called when one positional argument `event`. The class of this `event` is `NewMessage`
34 |
35 |
36 |
--------------------------------------------------------------------------------
/docs/basic/exceptions.rst:
--------------------------------------------------------------------------------
1 | .. _exceptions:
2 |
3 | =============
4 | Exceptions
5 | =============
6 |
7 | This page contains all the Exceptions raised by the different methods
8 |
9 | AuthenticationRequired
10 | ------------------------
11 |
12 | .. py:class:: AuthenticationRequired
13 |
14 | Bases : `Exception`
15 |
16 | :description: **This Exception is raised when you use a method which required the user to be authenticated.**
17 |
18 | :reference: `tweety.exceptions.AuthenticationRequired`
19 |
20 | .. py:data:: Attributes:
21 |
22 | .. py:attribute:: message
23 | :type: str
24 | :value: You need to be authenticated to make this request
25 |
26 | Main Exception Message
27 |
28 | .. py:attribute:: error_code
29 | :type: int
30 | :value: 200
31 |
32 | Exception Error Code
33 |
34 | .. py:attribute:: error_name
35 | :type: str
36 | :value: GenericForbidden
37 |
38 | Twitter Internal Error Name
39 |
40 | .. py:attribute:: response
41 | :type: httpx.Response
42 |
43 | Raw Response returned by the Twitter
44 |
45 | UserNotFound
46 | ---------------------
47 |
48 | .. py:class:: UserNotFound
49 |
50 | Bases : `Exception`
51 |
52 | :description: **This Exception is raised when you use a method which required the user to be authenticated.**
53 |
54 | :reference: `tweety.exceptions.UserNotFound`
55 |
56 | .. py:data:: Attributes:
57 |
58 | .. py:attribute:: message
59 | :type: str
60 | :value: The User Account wasn't Found
61 |
62 | Main Exception Message
63 |
64 | .. py:attribute:: error_code
65 | :type: int
66 | :value: 50
67 |
68 | Exception Error Code
69 |
70 | .. py:attribute:: error_name
71 | :type: str
72 | :value: GenericUserNotFound
73 |
74 | Twitter Internal Error Name
75 |
76 | .. py:attribute:: response
77 | :type: httpx.Response
78 |
79 | Raw Response returned by the Twitter
80 |
81 |
82 | InvalidTweetIdentifier
83 | ------------------------
84 |
85 | .. py:class:: InvalidTweetIdentifier
86 |
87 | Bases : `Exception`
88 |
89 | :description: **This Exception is raised when the tweet which is begin queried is not Found / Invalid**.
90 |
91 | :reference: `tweety.exceptions.InvalidTweetIdentifier`
92 |
93 | .. py:data:: Attributes:
94 |
95 | .. py:attribute:: message
96 | :type: str
97 | :value: The Tweet Identifier is Invalid
98 |
99 | Main Exception Message
100 |
101 | .. py:attribute:: error_code
102 | :type: int
103 | :value: 144
104 |
105 | Exception Error Code
106 |
107 | .. py:attribute:: error_name
108 | :type: str
109 | :value: StatusNotFound
110 |
111 | Twitter Internal Error Name
112 |
113 | .. py:attribute:: response
114 | :type: httpx.Response
115 |
116 | Raw Response returned by the Twitter
117 |
118 | GuestTokenNotFound
119 | ---------------------
120 |
121 | .. py:class:: GuestTokenNotFound
122 |
123 | Bases : `Exception`
124 |
125 | :description: **This Exception is raised when guest token couldn't be obtained.**
126 |
127 | :reference: `tweety.exceptions.GuestTokenNotFound`
128 |
129 | .. py:data:: Attributes:
130 |
131 | .. py:attribute:: message
132 | :type: str
133 | :value: The Guest Token couldn't be obtained
134 |
135 | Main Exception Message
136 |
137 | .. py:attribute:: error_code
138 | :type: None
139 | :value: None
140 |
141 | Exception Error Code
142 |
143 | .. py:attribute:: error_name
144 | :type: None
145 | :value: None
146 |
147 | Twitter Internal Error Name
148 |
149 | .. py:attribute:: response
150 | :type: httpx.Response
151 |
152 | Raw Response returned by the Twitter
153 |
154 | UserProtected
155 | ---------------------
156 |
157 | .. py:class:: UserProtected
158 |
159 | Bases : `Exception`
160 |
161 | :description: **This Exception is raised when the user which is begin queried has private profile.This can be fixed by authenticating the request using cookies**
162 |
163 | :reference: `tweety.exceptions.UserProtected`
164 |
165 | .. py:data:: Attributes:
166 |
167 | .. py:attribute:: message
168 | :type: str
169 | :value: The User is Protected , please make sure you are authenticated and authorized
170 |
171 | Main Exception Message
172 |
173 | .. py:attribute:: error_code
174 | :type: int
175 | :value: 403
176 |
177 | Exception Error Code
178 |
179 | .. py:attribute:: error_name
180 | :type: str
181 | :value: UserUnavailable
182 |
183 | Twitter Internal Error Name
184 |
185 | .. py:attribute:: response
186 | :type: httpx.Response
187 |
188 | Raw Response returned by the Twitter
189 |
190 | InvalidCredentials
191 | ---------------------
192 |
193 | .. py:class:: InvalidCredentials
194 |
195 | Bases : `Exception`
196 |
197 | :description: **This Exception is raised when the cookies provided for authentication are invalid**
198 |
199 | :reference: `tweety.exceptions.InvalidCredentials`
200 |
201 | .. py:data:: Attributes:
202 |
203 | .. py:attribute:: message
204 | :type: str
205 | :value: The Cookies are Invalid
206 |
207 | Main Exception Message
208 |
209 | .. py:attribute:: error_code
210 | :type: int
211 | :value: 403
212 |
213 | Exception Error Code
214 |
215 | .. py:attribute:: error_name
216 | :type: str
217 | :value: UserUnavailable
218 |
219 | Twitter Internal Error Name
220 |
221 | .. py:attribute:: response
222 | :type: httpx.Response
223 |
224 | Raw Response returned by the Twitter
225 |
226 | RateLimitReached
227 | ---------------------
228 |
229 | .. py:class:: RateLimitReached
230 |
231 | Bases : `Exception`
232 |
233 | :description: **This Exception is raised when you have exceeded the Twitter Rate Limit**
234 |
235 | :reference: `tweety.exceptions.RateLimitReached`
236 |
237 | .. py:data:: Attributes:
238 |
239 | .. py:attribute:: message
240 | :type: str
241 | :value: You have exceeded the Twitter Rate Limit
242 |
243 | Main Exception Message
244 |
245 | .. py:attribute:: error_code
246 | :type: int
247 | :value: 88
248 |
249 | Exception Error Code
250 |
251 | .. py:attribute:: error_name
252 | :type: str
253 | :value: RateLimitExceeded
254 |
255 | Twitter Internal Error Name
256 |
257 | .. py:attribute:: response
258 | :type: httpx.Response
259 |
260 | Raw Response returned by the Twitter
261 |
262 | DeniedLogin
263 | ---------------------
264 |
265 | .. py:class:: DeniedLogin
266 |
267 | Bases : `Exception`
268 |
269 | :description: **Exception Raised when the Twitter deny the login request , could be due to multiple login attempts (or failed attempts)**
270 |
271 | :reference: `tweety.exceptions.DeniedLogin`
272 |
273 | .. py:data:: Attributes:
274 |
275 | .. py:attribute:: message
276 | :type: str
277 |
278 | Main Exception Message
279 |
280 | .. py:attribute:: error_code
281 | :type: int
282 |
283 | Exception Error Code
284 |
285 | .. py:attribute:: error_name
286 | :type: str
287 |
288 | Twitter Internal Error Name
289 |
290 | .. py:attribute:: response
291 | :type: httpx.Response
292 |
293 | Raw Response returned by the Twitter
294 |
295 | UnknownError
296 | ---------------------
297 |
298 | .. py:class:: UnknownError
299 |
300 | Bases : `Exception`
301 |
302 | :description: **This Exception is raised when a error unknown to Tweety occurs**
303 | :reference: `tweety.exceptions.UnknownError`
304 |
305 | .. py:data:: Attributes:
306 |
307 | .. py:attribute:: message
308 | :type: str
309 |
310 | Main Exception Message
311 |
312 | .. py:attribute:: error_code
313 | :type: int
314 |
315 | Exception Error Code
316 |
317 | .. py:attribute:: error_name
318 | :type: str
319 |
320 | Twitter Internal Error Name
321 |
322 | .. py:attribute:: response
323 | :type: httpx.Response
324 |
325 | Raw Response returned by the Twitter
326 |
327 | ActionRequired
328 | ---------------------
329 |
330 | .. py:class:: ActionRequired
331 |
332 | Bases : `Exception`
333 |
334 | :description: **This Exception is raised when an additional step is required for Logging-in**
335 | :reference: `tweety.exceptions.ActionRequired`
336 |
337 | .. py:data:: Attributes:
338 |
339 | .. py:attribute:: message
340 | :type: str
341 |
342 | Main Exception Message / Description of the Action to be performed
343 |
344 | .. py:attribute:: error_code
345 | :type: int
346 |
347 | Exception Error Code
348 |
349 | .. py:attribute:: error_name
350 | :type: str
351 |
352 | Twitter Internal Error Name
353 |
354 | .. py:attribute:: response
355 | :type: httpx.Response
356 |
357 | Raw Response returned by the Twitter
358 |
359 | ListNotFound
360 | ---------------------
361 |
362 | .. py:class:: ListNotFound
363 |
364 | Bases : `Exception`
365 |
366 | :description: **This Exception is raised when List queried is not Found**
367 | :reference: `tweety.exceptions.ListNotFound`
368 |
369 | .. py:data:: Attributes:
370 |
371 | .. py:attribute:: message
372 | :type: str
373 |
374 | Main Exception Message / Description of the Action to be performed
375 |
376 | .. py:attribute:: error_code
377 | :type: int
378 |
379 | Exception Error Code
380 |
381 | .. py:attribute:: error_name
382 | :type: str
383 |
384 | Twitter Internal Error Name
385 |
386 | .. py:attribute:: response
387 | :type: httpx.Response
388 |
389 | Raw Response returned by the Twitter
390 |
391 | ProtectedTweet
392 | ---------------------
393 |
394 | .. py:class:: ProtectedTweet
395 |
396 | Bases : `Exception`
397 |
398 | :description: **This Exception is raised when queried Tweet is protected, and you need authorization to access it**
399 | :reference: `tweety.exceptions.ProtectedTweet`
400 |
401 | .. py:data:: Attributes:
402 |
403 | .. py:attribute:: message
404 | :type: str
405 | :value: "Tweet is private/protected"
406 |
407 | Main Exception Message / Description of the Action to be performed
408 |
409 | .. py:attribute:: error_code
410 | :type: int
411 |
412 | Exception Error Code
413 |
414 | .. py:attribute:: error_name
415 | :type: str
416 |
417 | Twitter Internal Error Name
418 |
419 | .. py:attribute:: response
420 | :type: httpx.Response
421 |
422 | Raw Response returned by the Twitter
423 |
424 | ConversationNotFound
425 | ---------------------
426 |
427 | .. py:class:: ConversationNotFound
428 |
429 | Bases : `Exception`
430 |
431 | :description: **This Exception is raised when queried conversation not Found**
432 | :reference: `tweety.exceptions.ConversationNotFound`
433 |
434 | .. py:data:: Attributes:
435 |
436 | .. py:attribute:: message
437 | :type: str
438 | :value: "Conversation Not Found"
439 |
440 | Main Exception Message / Description of the Action to be performed
441 |
442 | .. py:attribute:: error_code
443 | :type: int
444 |
445 | Exception Error Code
446 |
447 | .. py:attribute:: error_name
448 | :type: str
449 |
450 | Twitter Internal Error Name
451 |
452 | .. py:attribute:: response
453 | :type: httpx.Response
454 |
455 | Raw Response returned by the Twitter
456 |
457 | LockedAccount
458 | ---------------------
459 |
460 | .. py:class:: LockedAccount
461 |
462 | Bases : `Exception`
463 |
464 | :description: **This Exception is raised when logged in account is Locked and might require to pass a CAPTCHA check**
465 | :reference: `tweety.exceptions.LockedAccount`
466 |
467 | .. py:data:: Attributes:
468 |
469 | .. py:attribute:: message
470 | :type: str
471 | :value: "Your Account is Locked"
472 |
473 | Main Exception Message / Description of the Action to be performed
474 |
475 | .. py:attribute:: error_code
476 | :type: int
477 |
478 | Exception Error Code
479 |
480 | .. py:attribute:: error_name
481 | :type: str
482 |
483 | Twitter Internal Error Name
484 |
485 | .. py:attribute:: response
486 | :type: httpx.Response
487 |
488 | Raw Response returned by the Twitter
489 |
490 | SuspendedAccount
491 | ---------------------
492 |
493 | .. py:class:: SuspendedAccount
494 |
495 | Bases : `Exception`
496 |
497 | :description: **This Exception is raised when logged in account is suspended**
498 | :reference: `tweety.exceptions.SuspendedAccount`
499 |
500 | .. py:data:: Attributes:
501 |
502 | .. py:attribute:: message
503 | :type: str
504 | :value: "Your Account is Suspended"
505 |
506 | Main Exception Message / Description of the Action to be performed
507 |
508 | .. py:attribute:: error_code
509 | :type: int
510 |
511 | Exception Error Code
512 |
513 | .. py:attribute:: error_name
514 | :type: str
515 |
516 | Twitter Internal Error Name
517 |
518 | .. py:attribute:: response
519 | :type: httpx.Response
520 |
521 | Raw Response returned by the Twitter
522 |
523 | UploadFailed
524 | ---------------------
525 |
526 | .. py:class:: UploadFailed
527 |
528 | Bases : `Exception`
529 |
530 | :description: **This Exception is raised when media upload fails**
531 | :reference: `tweety.exceptions.UploadFailed`
532 |
533 | .. py:data:: Attributes:
534 |
535 | .. py:attribute:: message
536 | :type: str
537 | :value: "Unknown Error Occurred while uploading File"
538 |
539 | Main Exception Message / Description of the Action to be performed
540 |
541 | .. py:attribute:: error_code
542 | :type: int
543 |
544 | Exception Error Code
545 |
546 | .. py:attribute:: error_name
547 | :type: str
548 |
549 | Twitter Internal Error Name
550 |
551 | .. py:attribute:: response
552 | :type: httpx.Response
553 |
554 | Raw Response returned by the Twitter
555 |
556 | ProxyParseError
557 | ---------------------
558 |
559 | .. py:class:: ProxyParseError
560 |
561 | Bases : `Exception`
562 |
563 | :description: **This Exception is raised when Proxy Format is Irregular**
564 | :reference: `tweety.exceptions.ProxyParseError`
565 |
566 | .. py:data:: Attributes:
567 |
568 | .. py:attribute:: message
569 | :type: str
570 |
571 | Main Exception Message / Description of the Action to be performed
--------------------------------------------------------------------------------
/docs/basic/filter.rst:
--------------------------------------------------------------------------------
1 | .. _filter:
2 |
3 | ===============
4 | Search Filters
5 | ===============
6 |
7 | You can filter the `Search` function using these methods
8 |
9 |
10 | Filter Latest Tweets
11 | ---------------------
12 |
13 | .. py:class:: SearchFilters.Latest()
14 |
15 | :reference: `tweety.filters.SearchFilters.Latest`
16 |
17 | .. code-block:: python
18 |
19 | from tweety.filters import SearchFilters
20 |
21 | # Assuming `app` is Twitter Client Object
22 |
23 | await app.search("#pakistan", filter_=SearchFilters.Latest())
24 | from tweet in tweets:
25 | print(tweet)
26 |
27 | Filter Only Media Tweets
28 | ---------------------------
29 |
30 | .. py:class:: SearchFilters.Media()
31 |
32 | :reference: `tweety.filters.SearchFilters.Media`
33 |
34 | .. code-block:: python
35 |
36 | from tweety.filters import SearchFilters
37 |
38 | # Assuming `app` is Twitter Client Object
39 |
40 | await app.search("#pakistan", filter_=SearchFilters.Media())
41 | from tweet in tweets:
42 | print(tweet.media)
43 |
44 |
45 | Filter Only Users
46 | ---------------------
47 |
48 | .. py:class:: SearchFilters.Users()
49 |
50 | :reference: `tweety.filters.SearchFilters.Users`
51 |
52 | .. code-block:: python
53 |
54 | from tweety.filters import SearchFilters
55 |
56 | # Assuming `app` is Twitter Client Object
57 |
58 | await app.search("#pakistan", filter_=SearchFilters.Users())
59 | from user in users:
60 | print(user)
61 |
62 |
63 |
64 | ===============
65 | Tweet Audience Filter
66 | ===============
67 |
68 | You can filter the created Tweet Comment Audience function using these methods
69 |
70 |
71 | Filter Only People You Mention
72 | --------------------------------
73 |
74 | .. py:class:: TweetConversationFilters.PeopleYouMention()
75 |
76 | :reference: `tweety.filters.TweetConversationFilters.PeopleYouMention`
77 |
78 | .. code-block:: python
79 |
80 | from tweety.filters import TweetConversationFilters
81 |
82 | # Assuming `app` is Twitter Client Object
83 |
84 | await app.create_tweet("Hi", filter_=TweetConversationFilters.PeopleYouMention())
85 |
86 |
87 | Filter Only People You Follow
88 | --------------------------------
89 |
90 | .. py:class:: TweetConversationFilters.PeopleYouFollow()
91 |
92 | :reference: `tweety.filters.TweetConversationFilters.PeopleYouFollow`
93 |
94 | .. code-block:: python
95 |
96 | from tweety.filters import TweetConversationFilters
97 |
98 | # Assuming `app` is Twitter Client Object
99 |
100 | await app.create_tweet("Hi", filter_=TweetConversationFilters.PeopleYouFollow())
101 |
102 | ===============
103 | Filter Community Tweets
104 | ===============
105 |
106 | You can filter Community Tweet Timeline
107 |
108 | Filter Top Tweets
109 | --------------------------------
110 |
111 | .. py:class:: CommunityTweets.Top()
112 |
113 | :reference: `tweety.filters.CommunityTweets.Top`
114 |
115 | .. code-block:: python
116 |
117 | from tweety.filters import CommunityTweets
118 |
119 | # Assuming `app` is Twitter Client Object
120 |
121 | await app.get_community_tweets("1234", filter_=CommunityTweets.Top())
122 |
123 |
124 | ===============
125 | Filter Community Members
126 | ===============
127 |
128 | You can filter Community Members List
129 |
130 | Filter Moderators Members
131 | --------------------------------
132 |
133 | .. py:class:: CommunityMembers.Moderators()
134 |
135 | :reference: `tweety.filters.CommunityMembers.Moderators`
136 |
137 | .. code-block:: python
138 |
139 | from tweety.filters import CommunityMembers
140 |
141 | # Assuming `app` is Twitter Client Object
142 |
143 | await app.get_community_members("1234", filter_=CommunityMembers.Moderators())
144 |
145 | ===============
146 | Language
147 | ===============
148 |
149 | You can translate Tweet in different Language , get Language code from here.
150 |
151 | .. py:data:: Urdu
152 | :value: "ur"
153 |
154 | .. py:data:: URDU
155 | :value: "ur"
156 |
157 | .. py:data:: Russian
158 | :value: "ru"
159 |
160 | .. py:data:: RUSSIAN
161 | :value: "ru"
162 |
163 | .. py:data:: Danish
164 | :value: "da"
165 |
166 | .. py:data:: DANISH
167 | :value: "da"
168 |
169 | .. py:data:: Filipino
170 | :value: "fil"
171 |
172 | .. py:data:: FILIPINO
173 | :value: "fil"
174 |
175 | .. py:data:: Irish
176 | :value: "ga"
177 |
178 | .. py:data:: IRISH
179 | :value: "ga"
180 |
181 | .. py:data:: TraditionalChinese
182 | :value: "zh-tw"
183 |
184 | .. py:data:: TRADITIONAL_CHINESE
185 | :value: "zh-tw"
186 |
187 | .. py:data:: Hungarian
188 | :value: "hu"
189 |
190 | .. py:data:: HUNGARIAN
191 | :value: "hu"
192 |
193 | .. py:data:: Spanish
194 | :value: "es"
195 |
196 | .. py:data:: SPANISH
197 | :value: "es"
198 |
199 | .. py:data:: Arabic_Feminine
200 | :value: "ar-x-fm"
201 |
202 | .. py:data:: ARABIC_FEMININE
203 | :value: "ar-x-fm"
204 |
205 | .. py:data:: Croatian
206 | :value: "hr"
207 |
208 | .. py:data:: CROATIAN
209 | :value: "hr"
210 |
211 | .. py:data:: French
212 | :value: "fr"
213 |
214 | .. py:data:: FRENCH
215 | :value: "fr"
216 |
217 | .. py:data:: Kannada
218 | :value: "kn"
219 |
220 | .. py:data:: KANNADA
221 | :value: "kn"
222 |
223 | .. py:data:: Italian
224 | :value: "it"
225 |
226 | .. py:data:: ITALIAN
227 | :value: "it"
228 |
229 | .. py:data:: Marathi
230 | :value: "mr"
231 |
232 | .. py:data:: MARATHI
233 | :value: "mr"
234 |
235 | .. py:data:: Japanese
236 | :value: "ja"
237 |
238 | .. py:data:: JAPANESE
239 | :value: "ja"
240 |
241 | .. py:data:: Indonesian
242 | :value: "id"
243 |
244 | .. py:data:: INDONESIAN
245 | :value: "id"
246 |
247 | .. py:data:: Gujarati
248 | :value: "gu"
249 |
250 | .. py:data:: GUJARATI
251 | :value: "gu"
252 |
253 | .. py:data:: Romanian
254 | :value: "ro"
255 |
256 | .. py:data:: ROMANIAN
257 | :value: "ro"
258 |
259 | .. py:data:: Turkish
260 | :value: "tr"
261 |
262 | .. py:data:: TURKISH
263 | :value: "tr"
264 |
265 | .. py:data:: Basque
266 | :value: "eu"
267 |
268 | .. py:data:: BASQUE
269 | :value: "eu"
270 |
271 | .. py:data:: Swedish
272 | :value: "sv"
273 |
274 | .. py:data:: SWEDISH
275 | :value: "sv"
276 |
277 | .. py:data:: Tamil
278 | :value: "ta"
279 |
280 | .. py:data:: TAMIL
281 | :value: "ta"
282 |
283 | .. py:data:: Thai
284 | :value: "th"
285 |
286 | .. py:data:: THAI
287 | :value: "th"
288 |
289 | .. py:data:: Ukrainian
290 | :value: "uk"
291 |
292 | .. py:data:: UKRAINIAN
293 | :value: "uk"
294 |
295 | .. py:data:: Bangla
296 | :value: "bn"
297 |
298 | .. py:data:: BANGLA
299 | :value: "bn"
300 |
301 | .. py:data:: German
302 | :value: "de"
303 |
304 | .. py:data:: GERMAN
305 | :value: "de"
306 |
307 | .. py:data:: Vietnamese
308 | :value: "vi"
309 |
310 | .. py:data:: VIETNAMESE
311 | :value: "vi"
312 |
313 | .. py:data:: Catalan
314 | :value: "ca"
315 |
316 | .. py:data:: CATALAN
317 | :value: "ca"
318 |
319 | .. py:data:: Arabic
320 | :value: "ar"
321 |
322 | .. py:data:: ARABIC
323 | :value: "ar"
324 |
325 | .. py:data:: Dutch
326 | :value: "nl"
327 |
328 | .. py:data:: DUTCH
329 | :value: "nl"
330 |
331 | .. py:data:: SimplifiedChinese
332 | :value: "zh-cn"
333 |
334 | .. py:data:: SIMPLIFIED_CHINESE
335 | :value: "zh-cn"
336 |
337 | .. py:data:: Slovak
338 | :value: "sk"
339 |
340 | .. py:data:: SLOVAK
341 | :value: "sk"
342 |
343 | .. py:data:: Czech
344 | :value: "cs"
345 |
346 | .. py:data:: CZECH
347 | :value: "cs"
348 |
349 | .. py:data:: Greek
350 | :value: "el"
351 |
352 | .. py:data:: GREEK
353 | :value: "el"
354 |
355 | .. py:data:: Finnish
356 | :value: "fi"
357 |
358 | .. py:data:: FINNISH
359 | :value: "fi"
360 |
361 | .. py:data:: English
362 | :value: "en"
363 |
364 | .. py:data:: ENGLISH
365 | :value: "en"
366 |
367 | .. py:data:: Norwegian
368 | :value: "no"
369 |
370 | .. py:data:: NORWEGIAN
371 | :value: "no"
372 |
373 | .. py:data:: Polish
374 | :value: "pl"
375 |
376 | .. py:data:: POLISH
377 | :value: "pl"
378 |
379 | .. py:data:: Portuguese
380 | :value: "pt"
381 |
382 | .. py:data:: PORTUGUESE
383 | :value: "pt"
384 |
385 | .. py:data:: Persian
386 | :value: "fa"
387 |
388 | .. py:data:: PERSIAN
389 | :value: "fa"
390 |
391 | .. py:data:: Galician
392 | :value: "gl"
393 |
394 | .. py:data:: GALICIAN
395 | :value: "gl"
396 |
397 | .. py:data:: Korean
398 | :value: "ko"
399 |
400 | .. py:data:: KOREAN
401 | :value: "ko"
402 |
403 | .. py:data:: Serbian
404 | :value: "sr"
405 |
406 | .. py:data:: SERBIAN
407 | :value: "sr"
408 |
409 | .. py:data:: BritishEnglish
410 | :value: "en-gb"
411 |
412 | .. py:data:: BRITISH_ENGLISH
413 | :value: "en-gb"
414 |
415 | .. py:data:: Hindi
416 | :value: "hi"
417 |
418 | .. py:data:: HINDI
419 | :value: "hi"
420 |
421 | .. py:data:: Hebrew
422 | :value: "he"
423 |
424 | .. py:data:: HEBREW
425 | :value: "he"
426 |
427 | .. py:data:: Malay
428 | :value: "msa"
429 |
430 | .. py:data:: MALAY
431 | :value: "msa"
432 |
433 | .. py:data:: Bulgarian
434 | :value: "bg"
435 |
436 | .. py:data:: BULGARIAN
437 | :value: "bg"
438 |
439 | .. code-block:: python
440 |
441 | from tweety.filters import Language
442 |
443 | # Assuming `app` is Twitter Client Object
444 |
445 | await app.translate_tweet("1234", language=Language.English)
446 |
--------------------------------------------------------------------------------
/docs/basic/installation.rst:
--------------------------------------------------------------------------------
1 | .. _installation:
2 |
3 | ============
4 | Installation
5 | ============
6 |
7 | Tweety is a Python library, which means you need to download and install
8 | Python from https://www.python.org/downloads/ if you haven't already. Once
9 | you have Python installed, `upgrade pip`__ and run:
10 |
11 | .. code-block:: sh
12 |
13 | python3 -m pip install --upgrade pip
14 | python3 -m pip install --upgrade tweety-ns
15 |
16 | .. __: https://pythonspeed.com/articles/upgrade-pip/
17 |
18 | **Please** do check the full documentation before upgrading if upgrading to new version
19 |
20 | Dependencies
21 | =====================
22 |
23 | httpx_ : The library will be used to make the http/2 requests to Twitter
24 |
25 | dateutil_ : The library will be used to parse the dates in the http response
26 |
27 | openpyxl_ : The library will be used to save the responses as an Excel Sheet
28 |
29 |
30 | .. _httpx: https://github.com/encode/httpx
31 | .. _dateutil: https://github.com/dateutil/dateutil
32 | .. _openpyxl: https://github.com/theorchard/openpyxl
--------------------------------------------------------------------------------
/docs/basic/quick-start.rst:
--------------------------------------------------------------------------------
1 | ===========
2 | Quick-Start
3 | ===========
4 |
5 | Let's see a longer example to learn some of the methods that the library
6 | has to offer.
7 |
8 | .. code-block:: python
9 |
10 | from tweety import TwitterAsync
11 |
12 | async def main():
13 | app = TwitterAsync("session")
14 | await app.sign_in(username, password)
15 | target_username = "elonmusk"
16 |
17 | user = await app.get_user_info(target_username)
18 | all_tweets = await app.get_tweets(user)
19 |
20 | for tweet in all_tweets:
21 | print(tweet)
22 |
23 |
24 |
25 | Here, we show how to get the user information and tweets from ``elonmusk`` user profile on Twitter
26 | and then iterating over the Tweets
27 |
28 | - Method ``get_user_info`` returns the instance of `User` class
29 | - Method ``get_tweets`` returns the instance of `UserTweets` class
30 |
31 | - Reference to all Classes :ref:`twDataTypes`!
32 |
33 |
--------------------------------------------------------------------------------
/docs/basic/singing-in.rst:
--------------------------------------------------------------------------------
1 |
2 | .. _singing-in:
3 |
4 | =============
5 | Signing In
6 | =============
7 |
8 | The Twitter requires user to logged-in, in order to get any information. In this section you can check how can you sign to Twitter using tweety.
9 |
10 | Before Singing In , please make sure you already have account on Twitter
11 |
12 |
13 | Interactive Singing In
14 | ------------------------
15 | Most of the time , `start` is the only method you will interact with to login and create a session
16 | This Method will ask for `Username`, `Password` or any other information or action required for completing the log-in process.
17 |
18 | .. code-block:: python
19 |
20 | from tweety import TwitterAsync
21 |
22 | app = Twitter("session")
23 | await app.start(username, password, extra=extra)
24 | print(app.me)
25 |
26 | - Arguments `username` , `password` and `extra` are optional
27 |
28 | - Running the above code will first look for saved session, if found will resume that session
29 | - If not found , it will ask for `Username` if not provided
30 | - Then it will ask for `password` if not provided
31 | - Then it will start the login Flow
32 | - If any other information like 2-Factor Authentication Code , or Verification , it will ask for it
33 |
34 | .. code-block:: bash
35 |
36 | Action Required :> Please Enter the 2-Factor Authentication from the Authenticator App : 000000
37 |
38 |
39 | Singing In using Credentials
40 | ----------------------------
41 | You can login to Twitter on Tweety using your `username` and `password`
42 |
43 | .. code-block:: python
44 |
45 | from tweety import TwitterAsync
46 |
47 | app = Twitter("session")
48 | await app.sign_in(username, password)
49 | print(app.me)
50 |
51 |
52 | - By running this code ,
53 | - the Tweety will login to Twitter using the ``username`` and ``password`` provided ,
54 | - and if the request was successful , the authentication cookies obtained from response will be saved in ``session.tw_session`` (filename is subject to the name of session) file.
55 |
56 | - If any other information like 2-Factor Authenticated Code , or Verification id required , the `ActionRequired` will be raised
57 | - In case of `ActionRequired` , you can pass the required information to `extra` argument of `sign_in` method
58 |
59 | .. code-block:: python
60 |
61 | from tweety import TwitterAsync
62 |
63 | app = Twitter("session")
64 | try:
65 | await app.sign_in(username, password, extra=extra)
66 | except ActionRequired as e:
67 | action = input(f"Action Required :> {str(e.message)} : ")
68 | await app.sign_in(username, password, extra=action)
69 |
70 | Singing In using Cookies
71 | ----------------------------
72 | you can also log-in to Twitter on Tweety using ``Cookies``.
73 |
74 | .. code-block:: python
75 |
76 | from tweety import TwitterAsync
77 |
78 | cookies_value = """guest_id=guest_id_value; guest_id_marketing=guest_id_marketing; guest_id_ads=guest_id_ads; kdt=kdt_value; auth_token=auth_token_value; ct0=ct0_value; twid=twid_value; personalization_id="personalization_id_value" """
79 |
80 | # Cookies can be a str or a dict
81 |
82 | app = Twitter("session")
83 | await app.load_cookies(cookies_value)
84 | print(app.me)
85 |
86 |
87 | - By running this code ,if the request was successful , the authentication cookies obtained from response will be saved in ``session.tw_session`` (filename is subject to the name of session) file.
88 |
89 | Singing In using Auth Token
90 | ----------------------------
91 | you can also log-in to Twitter on Tweety using ``auth_token``.
92 |
93 | .. code-block:: python
94 |
95 | from tweety import TwitterAsync
96 |
97 | auth_token = """auth_token_value"""
98 |
99 | # Cookies can be a str or a dict
100 |
101 | app = Twitter("session")
102 | await app.load_auth_token(auth_token)
103 | print(app.me)
104 |
105 |
106 | - By running this code ,if the request was successful , the authentication cookies obtained from response will be saved in ``session.tw_session`` (filename is subject to the name of session) file.
107 |
108 |
109 |
110 | Singing In using previous session
111 | ----------------------------------
112 |
113 | Signing in using previous session requires a session file in the current directory of the script. Either you run `sign_in` or `load_cookies` , it will save the session in the session file named as the `session` argument provided to `Twitter` class.
114 |
115 | If the 'session' was passed as an argument of `session` to `Twitter` , your session will be save in `session.tw_session` file , if it is 'kharltayyab' , session will be saved in `kharltayyab.tw_session`
116 |
117 | Now using the same session name ,you can load the previous session from file
118 |
119 | .. attention:: If the session file is in different directory , make sure to provide the relative path.
120 |
121 | .. code-block:: python
122 |
123 | from tweety import TwitterAsync
124 |
125 | app = Twitter("session")
126 | await app.connect()
127 | # as 'session.tw_session' is already a authenticated session file , the session can be loaded using `connect` method
128 |
129 | print(app.me)
130 |
131 |
132 |
--------------------------------------------------------------------------------
/docs/basic/twitter-class.rst:
--------------------------------------------------------------------------------
1 |
2 | .. _twitter-class:
3 |
4 | =============
5 | Twitter Class
6 | =============
7 |
8 | The Twitter aggregates several mixin classes to provide all the common functionality in a nice, Pythonic interface. Each mixin has its own methods, which you all can use.
9 |
10 | .. py:class:: TwitterAsync(session_name: Union[str, Session], proxy: Union[str, Proxy] = None)
11 |
12 |
13 | Bases : `UpdateMethods` , `BotMethods`, `AuthMethods`, `UserMethods`
14 |
15 | .. py:data:: Arguments
16 |
17 | .. py:data:: session_name
18 | :type: str | Session
19 |
20 | This is the name of the session which will be saved and can be loaded later
21 |
22 | .. py:data:: proxy (optional)
23 | :type: str | Proxy
24 | :value: None
25 |
26 | Proxy you want to use
27 |
28 | .. py:data:: Methods:
29 |
30 | All Methods are Here :ref:`all-functions`!
31 |
32 | .. py:class:: Twitter(session_name: Union[str, Session], proxy: Union[str, Proxy] = None)
33 |
34 | This is just Synced Version of `TwitterAsync`.
35 |
36 | .. attention:: It is just a little hack , some functions might not work always prefer using `TwitterAsync`.
37 |
38 |
39 |
40 | =======================
41 | BaseGeneratorClass
42 | =======================
43 | .. py:class:: BaseGeneratorClass
44 |
45 | Bases : `dict`
46 |
47 | :reference: `tweety.types.base.BaseGeneratorClass`
48 |
49 | .. note:: **This Object is JSON Serializable and Iterable**
50 |
51 | This is the Base Class for All Generator Classes
52 |
53 | .. py:data:: Attributes:
54 |
55 | .. py:attribute:: cursor
56 | :type: str
57 |
58 | Cursor for next page
59 |
60 | .. py:attribute:: is_next_page
61 | :type: bool
62 |
63 | Is next page of tweets available
64 |
65 | .. py:attribute:: cursor_top
66 | :type: str
67 |
68 | Cursor for previous page
69 |
70 | .. py:data:: Methods:
71 |
72 | .. py:method:: get_page(cursor: str)
73 | :async:
74 |
75 | Get a Page of tweets
76 |
77 | .. py:data:: Arguments:
78 |
79 | .. py:data:: cursor
80 | :type: str
81 |
82 | Cursor of that specific Page
83 |
84 | .. py:data:: Return
85 | :type: tuple[Union[list[Tweet | SelfThread | User | ConversationThread | TwList]], str, str]
86 |
87 | .. py:method:: get_next_page()
88 | :async:
89 |
90 | Get next page of tweets if available using the saved cursor
91 |
92 | .. py:data:: Return
93 | :type: list[Tweet | SelfThread | User | ConversationThread | TwList]
94 |
95 |
96 |
97 |
98 |
--------------------------------------------------------------------------------
/docs/conf.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # -*- coding: utf-8 -*-
3 | #
4 | # Telethon documentation build configuration file, created by
5 | # sphinx-quickstart on Fri Nov 17 15:36:11 2017.
6 | #
7 | # This file is execfile()d with the current directory set to its
8 | # containing dir.
9 | #
10 | # Note that not all possible configuration values are present in this
11 | # autogenerated file.
12 | #
13 | # All configuration values have a default; values that are commented out
14 | # serve to show the default.
15 |
16 | # If extensions (or modules to document with autodoc) are in another directory,
17 | # add these directories to sys.path here. If the directory is relative to the
18 | # documentation root, use os.path.abspath to make it absolute, like shown here.
19 | #
20 | import re
21 | import os
22 | import sys
23 |
24 | sys.path.insert(0, os.path.abspath(os.curdir))
25 | sys.path.insert(0, os.path.abspath(os.pardir))
26 |
27 | root = os.path.abspath(os.path.join(__file__, os.path.pardir, os.path.pardir))
28 |
29 | # -- General configuration ------------------------------------------------
30 |
31 | # If your documentation needs a minimal Sphinx version, state it here.
32 | #
33 | # needs_sphinx = '1.0'
34 |
35 | # Add any Sphinx extension module names here, as strings. They can be
36 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
37 | # ones.
38 | extensions = [
39 | 'sphinx.ext.autodoc',
40 | 'sphinx.ext.autosummary',
41 | 'sphinx.ext.intersphinx',
42 | 'sphinx.ext.autosectionlabel',
43 | "sphinx_rtd_dark_mode"
44 | ]
45 | default_dark_mode = False
46 | autosectionlabel_prefix_document = True
47 | autosectionlabel_maxdepth = 3
48 | intersphinx_mapping = {
49 | 'python': ('https://docs.python.org/3', None)
50 | }
51 |
52 | # Change the default role so we can avoid prefixing everything with :obj:
53 | default_role = "py:obj"
54 |
55 | # Add any paths that contain templates here, relative to this directory.
56 | templates_path = ['_templates']
57 |
58 | # The suffix(es) of source filenames.
59 | # You can specify multiple suffix as a list of string:
60 | #
61 | # source_suffix = ['.rst', '.md']
62 | source_suffix = '.rst'
63 |
64 | # The master toctree document.
65 | master_doc = 'index'
66 |
67 | # General information about the project.
68 | project = 'Tweety'
69 | copyright = '2022 - 2024, mahrtayyab'
70 | author = 'mahrtayyab'
71 |
72 | # The version info for the project you're documenting, acts as replacement for
73 | # |version| and |release|, also used in various other places throughout the
74 | # built documents.
75 | #
76 | # The short X.Y version.
77 | with open(os.path.join(root, 'src', 'tweety', '__init__.py'), 'r') as f:
78 | version = re.search('__version__ = "(.*?)"', f.read(), flags=re.IGNORECASE).group(1)
79 |
80 | # The full version, including alpha/beta/rc tags.
81 | release = version
82 |
83 | # The language for content autogenerated by Sphinx. Refer to documentation
84 | # for a list of supported languages.
85 | #
86 | # This is also used if you do content translation via gettext catalogs.
87 | # Usually you set "language" from the command line for these cases.
88 | language = 'en'
89 |
90 | # List of patterns, relative to source directory, that match files and
91 | # directories to ignore when looking for source files.
92 | # This patterns also effect to html_static_path and html_extra_path
93 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
94 |
95 | # The name of the Pygments (syntax highlighting) style to use.
96 | pygments_style = 'friendly'
97 |
98 | # If true, `todo` and `todoList` produce output, else they produce nothing.
99 | todo_include_todos = False
100 |
101 |
102 | def skip(app, what, name, obj, would_skip, options):
103 | if name.endswith('__'):
104 | # We want to show special methods names, except some which add clutter
105 | return name in {
106 | '__init__',
107 | '__abstractmethods__',
108 | '__module__',
109 | '__doc__',
110 | '__dict__'
111 | }
112 |
113 | return would_skip
114 |
115 |
116 | def setup(app):
117 | app.connect("autodoc-skip-member", skip)
118 | app.add_object_type('confval', 'confval',
119 | objname='configuration value',
120 | indextemplate='pair: %s; configuration value')
121 |
122 |
123 | # -- Options for HTML output ----------------------------------------------
124 |
125 | # The theme to use for HTML and HTML Help pages. See the documentation for
126 | # a list of builtin themes.
127 | #
128 | html_theme = 'sphinx_rtd_theme'
129 | # html_theme = 'furo'
130 | # html_theme = 'insegel'
131 | # html_theme = 'sphinx_material'
132 |
133 | # Theme options are theme-specific and customize the look and feel of a theme
134 | # further. For a list of options available for each theme, see the
135 | # documentation.
136 | #
137 | html_theme_options = {
138 | 'collapse_navigation': True,
139 | 'display_version': True,
140 | 'navigation_depth': 2,
141 | }
142 |
143 | # Add any paths that contain custom static files (such as style sheets) here,
144 | # relative to this directory. They are copied after the builtin static files,
145 | # so a file named "default.css" will overwrite the builtin "default.css".
146 | # html_static_path = ['_static']
147 |
148 | # Custom sidebar templates, must be a dictionary that maps document names
149 | # to template names.
150 | #
151 | # This is required for the alabaster theme
152 | # refs: http://alabaster.readthedocs.io/en/latest/installation.html#sidebars
153 | html_sidebars = {
154 | '**': [
155 | 'globaltoc.html',
156 | 'relations.html', # needs 'show_related': True theme option to display
157 | 'searchbox.html',
158 | ]
159 | }
160 |
161 | # -- Options for HTMLHelp output ------------------------------------------
162 |
163 | # Output file base name for HTML help builder.
164 | htmlhelp_basename = 'Tweetydoc'
165 |
166 | # -- Options for LaTeX output ---------------------------------------------
167 |
168 | latex_elements = {
169 | # The paper size ('letterpaper' or 'a4paper').
170 | #
171 | # 'papersize': 'letterpaper',
172 |
173 | # The font size ('10pt', '11pt' or '12pt').
174 | #
175 | # 'pointsize': '10pt',
176 |
177 | # Additional stuff for the LaTeX preamble.
178 | #
179 | # 'preamble': '',
180 |
181 | # Latex figure (float) alignment
182 | #
183 | # 'figure_align': 'htbp',
184 | }
185 |
186 | # Grouping the document tree into LaTeX files. List of tuples
187 | # (source start file, target name, title,
188 | # author, documentclass [howto, manual, or own class]).
189 | latex_documents = [
190 | (master_doc, 'Tweety.tex', 'Tweety Documentation',
191 | author, 'manual'),
192 | ]
193 |
194 | # -- Options for manual page output ---------------------------------------
195 |
196 | # One entry per manual page. List of tuples
197 | # (source start file, name, description, authors, manual section).
198 | man_pages = [
199 | (master_doc, 'tweety', 'Tweety Documentation',
200 | [author], 1)
201 | ]
202 |
203 | # -- Options for Texinfo output -------------------------------------------
204 |
205 | # Grouping the document tree into Texinfo files. List of tuples
206 | # (source start file, target name, title, author,
207 | # dir menu entry, description, category)
208 | texinfo_documents = [
209 | (master_doc, 'Tweety', 'Tweety Documentation',
210 | author, 'Tweety', 'One line description of project.',
211 | 'Miscellaneous'),
212 | ]
213 |
--------------------------------------------------------------------------------
/docs/index.rst:
--------------------------------------------------------------------------------
1 | ========================
2 | Tweety Documentation
3 | ========================
4 |
5 | .. code-block:: python
6 |
7 | from tweety import TwitterAsync
8 |
9 | async def main():
10 | app = TwitterAsync("session")
11 | await app.sign_in(username, password)
12 |
13 | all_tweets = await app.get_tweets("elonmusk")
14 | for tweet in all_tweets:
15 | print(tweet)
16 |
17 |
18 | * Are you new here? Jump straight into :ref:`installation`!
19 | * Looking for All Available Functions? See :ref:`all-functions`.
20 | * Did you upgrade the library? Please read :ref:`changelog`.
21 |
22 |
23 | What is this?
24 | -------------
25 |
26 | Twitter is a popular social media platform used by millions of people
27 | even the Governments too. This library is meant to scrape the Tweets,
28 | Users , Trends and Search Results from Twitter.
29 |
30 | How should I use the documentation?
31 | -----------------------------------
32 |
33 | If you are getting started with the library, you should follow the
34 | documentation in order by pressing the "Next" button at the bottom-right
35 | of every page.
36 |
37 | You can also use the menu on the left to quickly skip over sections.
38 |
39 | .. toctree::
40 | :hidden:
41 | :caption: Get Started
42 |
43 | basic/installation
44 | basic/quick-start
45 |
46 | .. toctree::
47 | :hidden:
48 | :caption: Base Class
49 |
50 | basic/twitter-class
51 |
52 |
53 | .. toctree::
54 | :hidden:
55 | :caption: References
56 |
57 | basic/singing-in
58 | basic/all-functions
59 | basic/twDataTypes
60 | basic/exceptions
61 | basic/events
62 |
63 | .. toctree::
64 | :hidden:
65 | :caption: Filters
66 |
67 | basic/filter
68 |
69 | .. toctree::
70 | :hidden:
71 | :caption: Miscellaneous
72 |
73 | misc/changelog
74 |
--------------------------------------------------------------------------------
/docs/make.bat:
--------------------------------------------------------------------------------
1 | @ECHO OFF
2 |
3 | pushd %~dp0
4 |
5 | REM Command file for Sphinx documentation
6 |
7 | if "%SPHINXBUILD%" == "" (
8 | set SPHINXBUILD=sphinx-build
9 | )
10 | set SOURCEDIR=.
11 | set BUILDDIR=_build
12 |
13 | %SPHINXBUILD% >NUL 2>NUL
14 | if errorlevel 9009 (
15 | echo.
16 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
17 | echo.installed, then set the SPHINXBUILD environment variable to point
18 | echo.to the full path of the 'sphinx-build' executable. Alternatively you
19 | echo.may add the Sphinx directory to PATH.
20 | echo.
21 | echo.If you don't have Sphinx installed, grab it from
22 | echo.https://www.sphinx-doc.org/
23 | exit /b 1
24 | )
25 |
26 | if "%1" == "" goto help
27 |
28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
29 | goto end
30 |
31 | :help
32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
33 |
34 | :end
35 | popd
36 |
--------------------------------------------------------------------------------
/docs/misc/changelog.rst:
--------------------------------------------------------------------------------
1 | .. _changelog:
2 |
3 | =============
4 | Changelogs
5 | =============
6 |
7 | Update 0.8:
8 | -------------------
9 |
10 | * Module version on `PYPI Repository `_ is bumped to 0.4
11 | * More clean Code
12 | * Added ``UserProtected`` exception to identify if the user is private
13 | * Added ``place`` attribute to the `Tweet`
14 | * Added ``card`` attribute to the `Tweet`
15 |
16 | Update 1.0:
17 | -------------------
18 |
19 | * Module version on `PYPI Repository `_ is bumped to 0.5
20 | * All Data Objects are JSON Serializable now (mostly)
21 | * UserTweets and Search has been reworked alot , more details :ref:`all-functions`
22 | * Now you can pass an additional ``cursor`` parameter to get_tweets and search functions
23 | * Whole directory structure has been reworked , please do check documentation before upgrading
24 |
25 | Update 1.0.1:
26 | -------------------
27 |
28 | * Module version on `PYPI Repository `_ is bumped to 0.5.2
29 | * Fixed the ``sheet not Found`` error in ``to_xlsx()`` method
30 | * Fixed ``NoneType`` error when Card has no choices
31 | * Added ``is_quoted`` attribute to `Tweet`
32 | * Added ``quoted_tweet`` attribute to `Tweet`
33 | * Added ``quote_counts`` attribute to `Tweet`
34 | * Added ``vibe`` attribute to `Tweet`
35 | * Added ``is_possibly_sensitive`` attribute to `Tweet`
36 | * Added ``username`` attribute to `User`
37 | * Added ``possibly_sensitive`` attribute to `User`
38 | * Added ``pinned_tweets`` attribute to `User`
39 | * Early Adaptation to Twitter 2.0
40 |
41 | Update 1.1:
42 | -------------------
43 |
44 | * Module version on `PYPI Repository `_ is bumped to 0.6.0
45 | * Moved from `requests` to `httpx`
46 | * Lot of Bug Fixes
47 |
48 | Update 1.2:
49 | -------------------
50 |
51 | * Module version on `PYPI Repository `_ is bumped to 0.6.1
52 | * ``quoted_tweet`` has been fixed
53 | * New Attribute ``bookmark_count`` has been added
54 | * Bug Fixes
55 |
56 | Update 1.3:
57 | -------------------
58 |
59 | * Module version on `PYPI Repository `_ is bumped to 0.7
60 | * Early Cookies implementations
61 | * Now `get_tweets` accept the individual usernames, check `get_user_info `_
62 | * As search now requires user to be logged in , it will not work without cookies
63 | * Extended All Exceptions with some additional attributes , check `exceptions `_
64 | * Removed ``wget`` , and added a custom implementation for download
65 | * Added ``tqdm`` for download progress
66 | * Removed `to_dict` in most methods , and all will be removed in future
67 | * **Please** do check the full documentation before upgrading
68 |
69 | Update 1.4:
70 | -------------------
71 |
72 | * Module version on `PYPI Repository `_ is bumped to 0.7.1
73 | * Fixed 'TypeError: Union[arg, ...]'
74 | * Completely Removed ``wget``
75 | * Completely removed `to_dict` method
76 | * Added ``bio`` ``description`` ``entities`` ``date`` attributes to `User`
77 | * Added ``date`` attribute to `Tweet`
78 | * Added New ``iter_tweets`` methods to `Twitter` , check `iter_tweets `_
79 | * Added New ``iter_search`` methods to `Twitter` , check `iter_search `_
80 | * **Please** do check the full documentation before upgrading
81 |
82 | Update 1.5:
83 | -------------------
84 |
85 | * Module version on `PYPI Repository `_ is bumped to 0.8
86 | * Many bug fixes
87 | * Added new ``get_mentions`` , ``get_inbox``, ``send_message`` methods
88 | * Cookies are now necessary
89 | * Added ``can_dm`` attributes to `User`
90 | * Added `RateLimitReached` new Exception
91 | * Added New `Inbox`, `Conversation`, `Message`, `Mention`
92 | * Added Event Listener
93 | * **Please do check the full documentation before upgrading**
94 |
95 | Update 1.6:
96 | -------------------
97 |
98 | * Module version on `PYPI Repository `_ is bumped to 0.9
99 | * Many bug fixes
100 | * Updated the `Twitter` Class
101 | * Added `sign_in` method with session support
102 | * Added `load_cookies` method
103 | * **Please do check the full documentation before upgrading**
104 |
105 |
106 | Update 1.7:
107 | -------------------
108 |
109 | * Module version on `PYPI Repository `_ is bumped to 0.9.5
110 | * Fixed sign in issues
111 | * Session file can only be saved and loaded from different directory
112 | * Fixed the Proxy support , it would now work as expected.
113 | * `send_message` can now send files too (only images)
114 | * Added `DeniedLogin` Exception
115 | * Added `create_tweet` method
116 | * Added `get_bookmarks` , `iter_bookmarks` method
117 | * Added ``MozillaCookieJar`` support to `load_cookies` method
118 | * Removed tqdm totally , you can now pass your own ``progress_callback`` function
119 | * **Please do check the full documentation before upgrading**
120 |
121 | Update 1.7.1:
122 | -------------------
123 |
124 | * Module version on `PYPI Repository `_ is bumped to 0.9.6
125 | * Fixed sign in issues once again
126 | * **Please do check the full documentation before upgrading**
127 |
128 | Update 1.8:
129 | -------------------
130 |
131 | * Module version on `PYPI Repository `_ is bumped to 0.9.9
132 | * Added interactive version of `sign_in` called `start`
133 | * Added New Exception `ActionRequired`
134 | * Reworked `tweet_detail` , it will be fixed now
135 | * Added New `get_home_timeline`, `iter_home_timeline`, `get_tweet_likes`, `iter_tweet_likes`, `get_tweet_retweets`, `iter_tweet_retweets`, `like_tweet`, `retweet_tweet`, `follow_user`, `unfollow_user` Methods
136 | * ``TweetThreads`` has been renamed to `SelfThreads`
137 | * Added new `ConversationThread`, `TweetRetweets`, `TweetLikes` Types
138 | * `comments` in `Tweet` now returns list of `ConversationThread` Object
139 | * Session file format renamed to ``tw_session``
140 | * ``photos``, ``videos`` SearchFilter has been merged and renamed to `Media`
141 | * Added `reply_to` argument to `create_tweet`
142 | * **Please do check the full documentation before upgrading**
143 |
144 | Update 1.8.1:
145 | -------------------
146 |
147 | * Module version on `PYPI Repository `_ is bumped to 0.9.9.1
148 | * Fixed the Import Errors
149 | * Fixed the annotation Errors
150 | * **Please do check the full documentation before upgrading**
151 |
152 | Update 1.9:
153 | -------------------
154 |
155 | * Module version on `PYPI Repository `_ is bumped to 0.9.9.5
156 | * Fixed tweet comments pagination
157 | * Added Video Upload Support
158 | * Fixed the `create_tweet` issue
159 | * Fixed the tweet`text` length issue
160 | * Added `alt_text` to `Media`
161 | * Added `create_pool`
162 | * Added `Pool`
163 | * Reworked `Choice`
164 | * `wait_time` now accepts iterable
165 | * Added `RichText` , `RichTag`
166 | * Added `rich_text` attribute to `Tweet`
167 | * Fixed `SelfThread` not able to parse the tweet
168 | * Added `load_auth_token` method to AuthMethods
169 | * Added `get_community`, `get_community_tweets`, `iter_community_tweet`, `get_community_members`, `iter_community_members` method to BotMethods
170 | * Added `get_tweet_notifications`, `iter_tweet_notifications`, `enable_user_notification`, `disable_user_notification`, method to UserMethods
171 | * Added `Community`, `CommunityTweets`, `CommunityMembers`, `TweetNotifications` Data Types
172 | * **Please do check the full documentation before upgrading**
173 |
174 | Update 2.0:
175 | ------------
176 |
177 | * Module version on `PYPI Repository `_ is bumped to 1.0
178 | * Added many new methods to Base `Twitter` Class, do check full documentation
179 | * Added `best_stream` method to `Media` Class
180 | * Added video upload
181 | * Many bug fixes
182 | * **Please do check the full documentation before upgrading**
183 |
184 | Update 2.1:
185 | ------------
186 |
187 | * Module version on `PYPI Repository `_ is bumped to 1.0.2
188 | * Lot of bug fixes and code improvements
189 |
190 | ...
191 |
192 | Update 3.1
193 | --------------
194 | - Module version on `PYPI Repository `_ is bumped to 1.0.9.6
195 | - Alot of Bug Fixes
196 | - Added `ProtectedTweet` Exception
197 | - Added `URL`, `Hashtag` Type
198 | - Added "tweet_edit_history" method
199 | - Fixed `Inbox` Class
200 | - Added `EditControl` Type
201 | - Added `Symbol` Type
202 | - Added `get_user_media` method
203 | - Added `iter_user_media` method
204 | - Added `is_liked`, `is_retweeted` attributes to `Tweet` class
205 | - Added `get_next_page` and `get_next_page` method to `Conversation` class
206 | - Added user id cache
207 | - Optimized the `BaseGeneratorClass`
208 | - SelfThread not being parsed is fixed
209 | - Fixed issue when no user returned in inbox requests
210 | - Added `get_topic` method
211 | - Added `get_topic_tweets` method
212 | - Create Tweet now supports quoting a Tweet
213 | - Added `get_user_id` method
214 | - Added `user_mentions`, `urls`, `hashtags`, `symbols` in `Message` object
215 | - Added `get_mutual_followers` method
216 | - Added `get_blocked_users` method
217 | - Added `get_tweet_analytics` method
218 | - Added `unlike_tweet` method
219 | - Added `translate_tweet` method
220 |
221 | Pypi release 2.0
222 | -------------------
223 | - Module version on `PYPI Repository `_ is bumped to 2.0
224 | - **Added Full Async Support**
225 | - `TwitterAsync` class should be used from now on
226 | * **Please do check the full documentation before upgrading**
227 |
228 |
229 | Pypi release 2.1
230 | -----------------
231 | - Module version on `PYPI Repository `_ is bumped to 2.1
232 | - Added Grok related methods
233 | - Bug Fixes
234 | - Improvements and Optimizations
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [build-system]
2 | requires = [
3 | "setuptools>=42",
4 | "wheel"
5 | ]
6 | build-backend = "setuptools.build_meta"
7 |
8 | [tool.poetry.extras]
9 | windows = ["python-magic-bin"]
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | beautifulsoup4[lxml]~=4.12
2 | httpx[http2]
3 | openpyxl
4 | dateutils
5 | anticaptchaofficial
6 | capsolver
7 | 2captcha-python
8 | python-magic
9 | python-magic-bin; os_name == 'nt'
10 |
--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
1 | [metadata]
2 | name = tweety-ns
3 | version = 2.3.3
4 | author = Tayyab Kharl
5 | author_email = tayyabmahr@gmail.com
6 | description = An easy Twitter Scraper
7 | long_description = file: README.md
8 | long_description_content_type = text/markdown
9 | url = https://github.com/mahrtayyab/tweety
10 | project_urls =
11 | Bug Tracker = https://github.com/mahrtayyab/tweety/issues
12 | Documentation = https://github.com/mahrtayyab/tweety
13 | classifiers =
14 | Programming Language :: Python :: 4
15 | Operating System :: OS Independent
16 |
17 | [options]
18 | package_dir =
19 | = src
20 | packages = find:
21 | python_requires = >=3.9
22 |
23 | [options.packages.find]
24 | where = src
25 |
26 | [project]
27 | license = "MIT AND (Apache-2.0 OR BSD-2-Clause)"
28 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | from distutils.core import setup
2 |
3 | install_requires = [
4 | "beautifulsoup4[lxml]~=4.12",
5 | "openpyxl",
6 | "httpx[http2]",
7 | "dateutils",
8 | "anticaptchaofficial",
9 | "capsolver",
10 | "2captcha-python",
11 | "python-magic",
12 | "python-magic-bin; platform_system == 'Windows'"
13 | ]
14 |
15 | setup(
16 | name='tweety-ns',
17 | packages=['tweety', 'tweety.types', 'tweety.events', 'tweety.captcha'],
18 | version='2.3.3',
19 | license='MIT',
20 | description='An easy Twitter Scraper',
21 | author='Tayyab Kharl',
22 | author_email='tayyabmahr@gmail.com',
23 | url='https://github.com/mahrtayyab/tweety',
24 | keywords=['TWITTER', 'TWITTER SCRAPE', 'SCRAPE TWEETS'],
25 | install_requires=install_requires,
26 | classifiers=[
27 | 'Development Status :: 4 - Beta',
28 | 'Intended Audience :: Developers',
29 | 'Topic :: Software Development :: Build Tools',
30 | 'Programming Language :: Python :: 3'
31 | ],
32 | )
33 |
--------------------------------------------------------------------------------
/src/tweety/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Tayyab Kharl
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/src/tweety/__init__.py:
--------------------------------------------------------------------------------
1 | __version__ = "2.3.2"
2 | __author__ = "mahrtayyab"
3 |
4 |
5 | import inspect
6 | from .bot import BotMethods
7 | from .updates import UpdateMethods
8 | from .auth import AuthMethods
9 | from .user import UserMethods
10 | from .utils import get_running_loop
11 |
12 |
13 | def SyncWrap(cls):
14 | def method_wrapper_decorator(method_):
15 | def wrapper(self, *args, **kwargs):
16 | coro = method_(self, *args, **kwargs)
17 | loop = get_running_loop()
18 | if loop.is_running():
19 | return coro
20 | else:
21 | return loop.run_until_complete(coro)
22 |
23 | return wrapper
24 |
25 | if inspect.isclass(cls):
26 | for name in dir(cls):
27 | if not name.startswith('_') or name != '__init__':
28 | if inspect.iscoroutinefunction(getattr(cls, name)):
29 | setattr(cls, name, method_wrapper_decorator(getattr(cls, name)))
30 |
31 | return cls
32 | return method_wrapper_decorator(cls)
33 |
34 |
35 | class TwitterAsync(
36 | UserMethods, BotMethods, UpdateMethods, AuthMethods
37 | ):
38 | pass
39 |
40 |
41 | @SyncWrap
42 | class Twitter(TwitterAsync):
43 | pass
44 |
45 |
46 |
--------------------------------------------------------------------------------
/src/tweety/auth.py:
--------------------------------------------------------------------------------
1 | import getpass
2 | from http.cookiejar import MozillaCookieJar
3 | from typing import Union
4 | from .exceptions import InvalidCredentials, DeniedLogin, ActionRequired, ArkoseLoginRequired
5 | from .builder import FlowData
6 | from .types.n_types import Cookies
7 | from .utils import find_objects, get_url_parts
8 | from . import constants
9 |
10 |
11 | class AuthMethods:
12 |
13 | async def connect(self):
14 | """
15 | This method will be used to connect to already saved session in disk
16 | """
17 |
18 | if not self.session.logged_in:
19 | return
20 |
21 | self.request.cookies = self.session.cookies_dict()
22 | self.user = await self.request.verify_cookies()
23 | await self.session.save_session(self.cookies, self.user)
24 | self.is_user_authorized = True
25 | return self.user
26 |
27 | async def start(
28 | self,
29 | username=None,
30 | password=None,
31 | *,
32 | extra=None
33 | ):
34 | """
35 | Interactive Version of `sign_in` which will ask user for inputs
36 | Most of the time , this would be the only method you will be working with,
37 | it will check for existing sessions and login to it if available
38 |
39 | :param username: (`str`) Username of the user
40 | :param password: (`str`) Password of the user
41 | :param extra: (`str`) If you have 2-Factor authentication enabled and already have a code ,
42 | or any other action required for completing the login process
43 | it will be passed to this parameter
44 | :return: .types.twDataTypes.User (the user which is authenticated)
45 | """
46 |
47 | username = input('Please enter the Username: ') if not username else username
48 | password = getpass.getpass('Please enter your password: ') if not password else password
49 |
50 | _extra = extra
51 | _extra_once = False
52 | while not self.logged_in:
53 | try:
54 | return await self.sign_in(username, password, extra=_extra)
55 | except ActionRequired as e:
56 | _extra = input(f"\rAction Required :> {str(e.message)} : ")
57 | _extra_once = True
58 | except InvalidCredentials as ask_info:
59 | if _extra_once:
60 | _extra = input(f"\rAction Required :> {str(ask_info.message)} : ")
61 | else:
62 | raise ask_info
63 |
64 | async def sign_in(
65 | self,
66 | username,
67 | password,
68 | *,
69 | extra=None
70 | ):
71 | """
72 | - This method can be used to sign in to Twitter using username and password
73 | - It will also check for the saved session for the username in the disk
74 |
75 | :param username: (`str`) Username of the user
76 | :param password: (`str`) Password of the user
77 | :param extra: (`str`) If you have 2-Factor authentication enabled and already have a code ,
78 | or any other action required for completing the login process
79 | it will be passed to this parameter
80 | :return: .types.twDataTypes.User (the user which is authenticated)
81 | """
82 |
83 | if self.session.logged_in and self.session.user['username'].lower() == username.lower():
84 | try:
85 | return await self.connect()
86 | except InvalidCredentials:
87 | self.request.cookies = None
88 | pass
89 |
90 | self._username = username
91 | self._password = password
92 | self._extra = extra
93 | self._captcha_token = None
94 |
95 | if not self._login_flow:
96 | self._login_flow = FlowData()
97 |
98 | if not self._login_flow_state:
99 | self._login_flow_state = self._login_flow.initial_state
100 |
101 | return await self._login()
102 |
103 | async def load_cookies(
104 | self,
105 | cookies: Union[str, dict, MozillaCookieJar]
106 | ):
107 | """
108 | This method can be used to load the already authenticated cookies from Twitter
109 |
110 | :param cookies: (`str`, `dict`, `MozillaCookieJar`) The Cookies to load
111 | :return: .types.twDataTypes.User (the user which is authenticated)
112 | """
113 | self.cookies = Cookies(cookies)
114 | await self.session.save_session(self.cookies, None)
115 | return await self.connect()
116 |
117 | async def load_auth_token(self, auth_token):
118 | URL = "https://business.x.com/en"
119 | temp_cookie = {"auth_token": auth_token}
120 | temp_headers = {'authorization': constants.DEFAULT_BEARER_TOKEN}
121 | res = await self.request.session.get(URL, cookies=temp_cookie)
122 | ct0 = res.cookies.get('ct0')
123 |
124 | if not ct0:
125 | res = await self.request.session.get(URL, cookies=temp_cookie, headers=temp_headers)
126 | ct0 = res.cookies.get('ct0')
127 |
128 | if not ct0:
129 | raise DeniedLogin(response=res, message="Auth Token isn't Valid")
130 |
131 | temp_cookie.update(dict(res.cookies))
132 | return await self.load_cookies(temp_cookie)
133 |
134 | @staticmethod
135 | def _get_action_text(response):
136 | primary_message = find_objects(response, 'primary_text', None, none_value={})
137 | secondary_message = find_objects(response, 'secondary_text', None, none_value={})
138 | if primary_message:
139 | if isinstance(primary_message, list):
140 | primary_message = primary_message[0]
141 |
142 | primary_message = primary_message.get('text', '')
143 |
144 | if secondary_message:
145 | if isinstance(secondary_message, list):
146 | secondary_message = secondary_message[0]
147 | secondary_message = secondary_message.get('text', '')
148 | return f"{primary_message}. {secondary_message}"
149 |
150 | async def _login(self):
151 |
152 | while not self.logged_in:
153 | _login_payload = self._login_flow.get(
154 | self._login_flow_state,
155 | json_=self._last_json,
156 | username=self._username,
157 | password=self._password,
158 | extra=self._extra,
159 | captcha_token=self._captcha_token
160 | )
161 |
162 | # Twitter now often asks for multiple verifications
163 | if self._login_flow_state in constants.AUTH_ACTION_REQUIRED_KEYS:
164 | self._extra = None
165 |
166 | response = await self.request.login(self._login_url, _payload=_login_payload)
167 |
168 | self._last_json = response.json()
169 |
170 | if response.cookies.get("att"):
171 | self.request.headers = {"att": response.cookies.get("att")}
172 |
173 | if self._last_json.get('status') != "success":
174 | raise DeniedLogin(response=response, message=response.text)
175 |
176 | subtask = self._last_json["subtasks"][0].get("subtask_id")
177 | self._login_url = self._login_url.split("?")[0]
178 | self._login_flow_state = subtask
179 |
180 | if subtask in constants.AUTH_ACTION_REQUIRED_KEYS and not self._extra:
181 | message = self._get_action_text(self._last_json)
182 | raise ActionRequired(0, "ActionRequired", response, message)
183 |
184 | if subtask == "ArkoseLogin":
185 | # if self._captcha_solver is None:
186 | raise ArkoseLoginRequired(response=response)
187 |
188 | # token = await self.request.solve_captcha(websiteUrl="https://iframe.arkoselabs.com")
189 | # token = self.request.solve_captcha(websiteUrl="https://twitter.com/i/flow/login", blob_data=data[0])
190 | # self._captcha_token = token
191 |
192 | if subtask == "DenyLoginSubtask":
193 | reason = self._get_action_text(self._last_json)
194 | raise DeniedLogin(response=response, message=reason)
195 |
196 | if subtask == "LoginSuccessSubtask":
197 | self.request.remove_header("att")
198 | self.cookies = Cookies(dict(response.cookies))
199 | await self.session.save_session(self.cookies, None)
200 | return await self.connect()
201 |
202 | raise DeniedLogin(response=response, message="Unknown Error Occurred")
203 |
204 |
205 |
206 |
--------------------------------------------------------------------------------
/src/tweety/captcha/__init__.py:
--------------------------------------------------------------------------------
1 | from .capsolver import CapSolver
2 | from .anticaptcha import AntiCaptcha
3 | from .two_captcha import TwoCaptcha
--------------------------------------------------------------------------------
/src/tweety/captcha/anticaptcha.py:
--------------------------------------------------------------------------------
1 | from anticaptchaofficial.funcaptchaproxyless import funcaptchaProxyless
2 | from anticaptchaofficial.funcaptchaproxyon import funcaptchaProxyon
3 | from .base import BaseCaptchaSolver
4 | from ..utils import unpack_proxy
5 | from ..constants import REQUEST_USER_AGENT
6 | from ..exceptions import CaptchaSolverFailed
7 |
8 |
9 | class AntiCaptcha(BaseCaptchaSolver):
10 | def __init__(self, api_key, proxy=None, verbose=False):
11 | self._api_key = api_key
12 | self._verbose = verbose
13 | super().__init__(self._api_key, proxy)
14 |
15 | def __call__(self, twitter_client):
16 | super().init(cookies=twitter_client.session.cookies_dict, proxy=twitter_client._proxy)
17 | return self
18 |
19 | def get_solved_token(
20 | self,
21 | websitePublicKey,
22 | websiteUrl,
23 | blob_data=None
24 | ) -> str:
25 | if self._proxy is None:
26 | solver = funcaptchaProxyless()
27 | else:
28 | solver = funcaptchaProxyon()
29 | proxy_dict = unpack_proxy(self._proxy)
30 | solver.set_user_agent(REQUEST_USER_AGENT)
31 | solver.set_proxy_type(proxy_dict.get("type"))
32 | solver.set_proxy_address(proxy_dict.get("host"))
33 | solver.set_proxy_port(proxy_dict.get("port"))
34 |
35 | if proxy_dict.get("username"):
36 | solver.set_proxy_login(proxy_dict.get("username"))
37 | solver.set_proxy_password(proxy_dict.get("password"))
38 |
39 | solver.set_verbose(1 if self._verbose is True else 0)
40 | solver.set_key(self._api_key)
41 | solver.set_website_url(websiteUrl)
42 | solver.set_website_key(websitePublicKey)
43 | solver.set_js_api_domain("client-api.arkoselabs.com")
44 |
45 | if blob_data:
46 | solver.set_data_blob(blob_data)
47 |
48 | token = solver.solve_and_return_solution()
49 |
50 | if token != 0:
51 | return token
52 |
53 | raise CaptchaSolverFailed(message=f"Unable to Solve Captcha using 'AntiCaptcha' : [{solver.error_code}] {solver.err_string}")
54 |
55 |
--------------------------------------------------------------------------------
/src/tweety/captcha/base.py:
--------------------------------------------------------------------------------
1 | import warnings
2 | import httpx
3 | import re
4 | from ..types.n_types import Proxy
5 | from ..constants import LOGIN_SITE_KEY
6 | from ..exceptions import CaptchaSolverFailed
7 |
8 | """
9 | Modified Code from : https://github.com/ZakariaMQ/twitter-account-unlocker
10 | """
11 |
12 |
13 | class BaseCaptchaSolver:
14 | def __init__(self, api_key, proxy) -> None:
15 | self._cookies = None
16 | self._proxy = proxy
17 | self._api_key = api_key
18 | self.url = "https://x.com/account/access"
19 | self.client = None
20 | self.headers = None
21 | self._tokens = {}
22 |
23 | def init(self, cookies, proxy):
24 | self._cookies = cookies
25 | proxy_from_client = proxy
26 |
27 | if isinstance(self._proxy, bool) and self._proxy is True:
28 | if proxy_from_client is None:
29 | warnings.warn("No Proxy was found in Client , Captcha will be solved without Proxy")
30 | proxy = None
31 | else:
32 | proxy = proxy_from_client
33 | elif isinstance(self._proxy, (dict, Proxy)):
34 | proxy = self._proxy
35 | else:
36 | proxy = None
37 |
38 | if isinstance(proxy, str):
39 | self._proxy = dict.fromkeys(["http://", "https://"], proxy)
40 | elif isinstance(proxy, Proxy):
41 | self._proxy = proxy.get_dict()
42 | else:
43 | self._proxy = proxy
44 |
45 | self.client = httpx.Client(timeout=30, follow_redirects=True, proxies=self._proxy)
46 | self.headers = {
47 | "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:123.0) Gecko/20100101 Firefox/123.0",
48 | "Accept": "*/*",
49 | "Accept-Language": "en-GB,en;q=0.5",
50 | "Accept-Encoding": "gzip, deflate, br",
51 | "DNT": "1",
52 | "Sec-GPC": "1",
53 | "Connection": "keep-alive",
54 | "Sec-Fetch-Dest": "script",
55 | "Sec-Fetch-Mode": "no-cors",
56 | "Sec-Fetch-Site": "same-origin",
57 | "Pragma": "no-cache",
58 | "Cache-Control": "no-cache",
59 | "TE": "trailers"
60 | }
61 |
62 | def __extract_tokens_from_access_html_page(self, html: str):
63 | pattern = re.compile(r'name="authenticity_token" value="([^"]+)"|name="assignment_token" value="([^"]+)"')
64 | matches = pattern.findall(html)
65 | if matches:
66 | authenticity_token = matches[0][0] if matches[0][0] else matches[0][1]
67 | assignment_token = matches[1][0] if matches[1][0] else matches[1][1]
68 | self._tokens = {"authenticity_token": authenticity_token,
69 | "assignment_token": assignment_token}
70 |
71 | def __js_inst(self):
72 | js_script = self.client.get(
73 | url="https://x.com/i/js_inst?c_name=ui_metrics"
74 | ).text
75 | pattern = re.compile(r'return\s*({.*?});', re.DOTALL)
76 | js_instr = pattern.search(js_script)
77 |
78 | return js_instr.group(1)
79 |
80 | def __get_access_page(self):
81 | res = self.client.get(self.url, headers=self.headers, cookies=self._cookies)
82 | self._cookies.update(dict(res.cookies))
83 | self.__extract_tokens_from_access_html_page(html=res.text)
84 |
85 | def __post_to_access_page(self, data: dict):
86 | headers = self.headers
87 | headers["Host"] = "x.com"
88 | headers["Origin"] = "https://x.com"
89 | headers["Referer"] = "https://x.com/account/access"
90 |
91 | res = self.client.post(f"{self.url}?lang=en", data=data, headers=headers, cookies=self._cookies)
92 | self._cookies.update(dict(res.cookies))
93 | self.__extract_tokens_from_access_html_page(html=res.text)
94 |
95 | def __data_with_js_inst(self, tokens: dict) -> dict:
96 | return {
97 | "authenticity_token": tokens["authenticity_token"],
98 | "assignment_token": tokens["assignment_token"],
99 | "lang": "en",
100 | "flow": "",
101 | "ui_metrics": self.__js_inst()
102 | }
103 |
104 | @staticmethod
105 | def __data_with_funcaptcha(tokens: dict, fun_captcha_token: str) -> dict:
106 | return {
107 | "authenticity_token": tokens["authenticity_token"],
108 | "assignment_token": tokens["assignment_token"],
109 | 'lang': 'en',
110 | 'flow': '',
111 | 'verification_string': fun_captcha_token,
112 | 'language_code': 'en'
113 | }
114 |
115 | def __post_data_with_token(self, fun_captcha_token: str):
116 | data = self.__data_with_funcaptcha(tokens=self._tokens, fun_captcha_token=fun_captcha_token)
117 | self.__post_to_access_page(data=data)
118 |
119 | def __post_data_with_js_inst(self):
120 | data = self.__data_with_js_inst(tokens=self._tokens)
121 | self.__post_to_access_page(data=data)
122 |
123 | def unlock(self, websitePublicKey="0152B4EB-D2DC-460A-89A1-629838B529C9", websiteUrl="https://twitter.com/", blob_data=None):
124 | try:
125 | if not blob_data and websitePublicKey != LOGIN_SITE_KEY:
126 | self.__get_access_page()
127 |
128 | self.__post_data_with_js_inst()
129 |
130 | fun_captcha_token = self.get_solved_token(websitePublicKey=websitePublicKey, websiteUrl=websiteUrl)
131 |
132 | self.__post_data_with_token(fun_captcha_token=fun_captcha_token)
133 |
134 | fun_captcha_token = self.get_solved_token(websitePublicKey=websitePublicKey, websiteUrl=websiteUrl)
135 |
136 | self.__post_data_with_token(fun_captcha_token=fun_captcha_token)
137 |
138 | self.__post_data_with_js_inst()
139 | return fun_captcha_token
140 | else:
141 | fun_captcha_token = self.get_solved_token(websitePublicKey=websitePublicKey, websiteUrl=websiteUrl, blob_data=blob_data)
142 | return fun_captcha_token
143 | except Exception as e:
144 | raise CaptchaSolverFailed(message=str(e))
145 |
--------------------------------------------------------------------------------
/src/tweety/captcha/capsolver.py:
--------------------------------------------------------------------------------
1 | from .base import BaseCaptchaSolver
2 | import capsolver
3 | from ..utils import unpack_proxy
4 |
5 |
6 | class CapSolver(BaseCaptchaSolver):
7 | def __init__(self, api_key, proxy=None):
8 | self._api_key = api_key
9 | super().__init__(self._api_key, proxy)
10 |
11 | def __call__(self, twitter_client):
12 | super().init(cookies=twitter_client.session.cookies_dict, proxy=twitter_client._proxy)
13 | return self
14 |
15 | def get_solved_token(
16 | self,
17 | websitePublicKey,
18 | websiteUrl,
19 | blob_data=None
20 | ) -> str:
21 | capsolver.api_key = self._api_key
22 |
23 | request = {
24 | "type": "FunCaptchaTaskProxyLess",
25 | "websitePublicKey": websitePublicKey,
26 | "websiteURL": websiteUrl,
27 | }
28 | if self._proxy is not None:
29 | proxy_dict = unpack_proxy(self._proxy)
30 | request.update({
31 | "type": "FunCaptchaTask",
32 | "proxyType": proxy_dict.get("type"),
33 | "proxyAddress": proxy_dict.get("host"),
34 | "proxyPort": proxy_dict.get("port"),
35 | "proxyLogin": proxy_dict.get("username"),
36 | "proxyPassword": proxy_dict.get("password")
37 | })
38 |
39 | if blob_data:
40 | request["data"] = "{{\"blob\": \"{}\"}}".format(blob_data)
41 |
42 | return capsolver.solve(request)["token"]
43 |
44 |
--------------------------------------------------------------------------------
/src/tweety/captcha/two_captcha.py:
--------------------------------------------------------------------------------
1 | from twocaptcha import TwoCaptcha as TwoCaptchaSolver
2 | from .base import BaseCaptchaSolver
3 | from ..utils import unpack_proxy
4 |
5 |
6 | class TwoCaptcha(BaseCaptchaSolver):
7 | def __init__(self, api_key, proxy=None):
8 | self._api_key = api_key
9 | super().__init__(self._api_key, proxy)
10 |
11 | def __call__(self, twitter_client):
12 | super().init(cookies=twitter_client.session.cookies_dict, proxy=twitter_client._proxy)
13 | return self
14 |
15 | def get_solved_token(
16 | self,
17 | websitePublicKey,
18 | websiteUrl,
19 | blob_data=None
20 | ) -> str:
21 | solver = TwoCaptchaSolver(self._api_key)
22 | request = dict(
23 | sitekey=websitePublicKey,
24 | url=websiteUrl,
25 | data=blob_data
26 | )
27 |
28 | if self._proxy is not None:
29 | this_proxy = unpack_proxy(self._proxy)
30 | proxy_uri = f"{this_proxy['host']}:{this_proxy['port']}"
31 |
32 | if this_proxy.get("username"):
33 | proxy_uri = f"{this_proxy['username']}:{this_proxy['password']}@{proxy_uri}"
34 |
35 | request["proxy"] = {"type": this_proxy["type"].upper(), "uri": proxy_uri}
36 |
37 | token = solver.funcaptcha(**request)
38 |
39 | return token.get("code")
--------------------------------------------------------------------------------
/src/tweety/constants.py:
--------------------------------------------------------------------------------
1 | PROXY_TYPE_SOCKS4 = SOCKS4 = 1
2 | PROXY_TYPE_SOCKS5 = SOCKS5 = 2
3 | PROXY_TYPE_HTTP = HTTP = 3
4 | HOME_TIMELINE_TYPE_FOR_YOU = "HomeTimeline"
5 | HOME_TIMELINE_TYPE_FOLLOWING = "HomeLatestTimeline"
6 | INBOX_PAGE_TYPE_TRUSTED = "trusted"
7 | INBOX_PAGE_TYPE_UNTRUSTED = "untrusted"
8 | INBOX_PAGE_TYPES = (INBOX_PAGE_TYPE_TRUSTED, INBOX_PAGE_TYPE_UNTRUSTED)
9 | MEDIA_TYPE_VIDEO = "video"
10 | MEDIA_TYPE_GIF = "animated_gif"
11 | MEDIA_TYPE_IMAGE = MEDIA_TYPE_PHOTO = "photo"
12 | REQUEST_USER_AGENT = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36'
13 | REQUEST_USER_AGENT_CH = '"Chromium";v="136", "Google Chrome";v="136", "Not.A/Brand";v="99"'
14 | REQUEST_PLATFORMS = ['Linux']
15 | DEFAULT_BEARER_TOKEN = 'Bearer AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA'
16 | AUTH_ACTION_REQUIRED_KEYS = ("LoginTwoFactorAuthChallenge", "LoginAcid", "LoginEnterAlternateIdentifierSubtask")
17 | LIKES_ARE_PRIVATE_NOW_WARNING = "User Likes are now private , you can only see the Likes of authenticated User"
18 |
19 | LOGIN_SITE_KEY = "2F4F0B28-BC94-4271-8AD7-A51662E3C91C"
20 | GENERAL_SITE_KEY = "0152B4EB-D2DC-460A-89A1-629838B529C9"
21 |
22 | ITERABLE_TYPES = (list, tuple)
23 | UPLOAD_TYPE_TWEET_IMAGE = "tweet_image"
24 | # GENERAL_SITE_KEY = "50706BFE-942C-4EEC-B9AD-03F7CD268FB1"
25 |
26 |
27 | # JA3_FINGERPRINTS = "771,4865-4866-4867-49195-49199-49196-49200-52393-52392-49171-49172-156-157-47-53,51-18-10-5-35-0-45-65037-65281-23-43-11-17513-13-27-16,25497-29-23-24,0"
28 | JA3_FINGERPRINTS = "771,4865-4866-4867-49195-49199-49196-49200-52393-52392-49171-49172-156-157-47-53,13-23-18-27-51-0-45-5-17513-65281-65037-43-10-16-35-11,25497-29-23-24,0"
29 | AKAMAI_FINGERPRINTS = "1:65536;2:0;4:6291456;6:262144|15663105|0|m,a,s,p"
30 | HTTP2_SETTINGS = "1:65536;2:0;4:6291456;6:262144"
31 | TLS_OPTIONS = {
32 | "tls_signature_algorithms": [
33 | "ecdsa_secp256r1_sha256",
34 | "rsa_pss_rsae_sha256",
35 | "rsa_pkcs1_sha256",
36 | "ecdsa_secp384r1_sha384",
37 | "rsa_pss_rsae_sha384",
38 | "rsa_pkcs1_sha384",
39 | "rsa_pss_rsae_sha512",
40 | "rsa_pkcs1_sha512"
41 | ],
42 | "tls_cert_compression": "brotli",
43 | "tls_grease": True,
44 | }
--------------------------------------------------------------------------------
/src/tweety/events/__init__.py:
--------------------------------------------------------------------------------
1 | from .newmessage import NewMessageUpdate
2 |
3 |
--------------------------------------------------------------------------------
/src/tweety/events/base.py:
--------------------------------------------------------------------------------
1 | class BaseUpdateMethod:
2 | pass
3 |
4 |
--------------------------------------------------------------------------------
/src/tweety/events/newmessage.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 | from typing import Union, List
3 |
4 | from ..types.inbox import Inbox, Message
5 | from ..utils import get_running_loop
6 | from .base import BaseUpdateMethod
7 |
8 | class NewMessageUpdate(BaseUpdateMethod):
9 | def __init__(
10 | self,
11 | from_users: Union[str, List[str]] = None,
12 | blacklist_users: Union[str, List[str]] = None,
13 | func = None
14 | ):
15 | self.from_users = from_users
16 | self.blacklist_users = blacklist_users
17 | self.func = func
18 | self.inbox = None
19 | self.cursor = None
20 | self.client = None
21 | self.callback_func = None
22 |
23 | def __call__(self, client, callback):
24 | self.client = client
25 | self.callback_func = callback
26 | self.inbox = None
27 | self.cursor = None
28 | return self
29 |
30 | async def start(self):
31 | self.inbox = await self.client.get_inbox(pages=1)
32 | self.cursor = self.inbox.cursor
33 | await self.wait_for_message()
34 |
35 | class NewMessage:
36 | def __init__(self, conversation, message):
37 | self.conversation = conversation
38 | self.message = message
39 | self.conversation.messages = [message]
40 | self.participants = self.conversation.participants
41 | self.sender = self.message.sender
42 | self.receiver = self.message.receiver
43 | self.text = self.message.text if hasattr(self.message, "text") else None
44 | self.time = self.message.time
45 | self.id = self.message.id
46 | self.media = self.message.media if hasattr(self.message, "media") else None
47 |
48 | async def respond(self, text, file=None, reply_to_message_id=None, audio_only=False, quote_tweet_id=None):
49 | return await self.conversation.send_message(text, file, reply_to_message_id, audio_only, quote_tweet_id)
50 |
51 | def __repr__(self):
52 | return "NewMessage(id={}, sender={}, receiver={}, time={}, text={})".format(
53 | self.id, self.sender, self.receiver, self.time, self.text
54 | )
55 |
56 | async def parse_filters(self):
57 | if self.blacklist_users:
58 | if not isinstance(self.blacklist_users, list):
59 | self.blacklist_users = [self.blacklist_users]
60 |
61 | if self.from_users:
62 | if not isinstance(self.from_users, list):
63 | self.from_users = [self.from_users]
64 |
65 | async def wait_for_message(self):
66 | while True:
67 | new_chats = await self.inbox.get_new_messages()
68 | if new_chats:
69 | for conv in new_chats:
70 | for message in conv.messages:
71 | event = None
72 |
73 | if isinstance(message, Message):
74 | if not message.sender or str(message.sender.id) != str(self.client.user.id):
75 | event = self.NewMessage(conv, message)
76 | else:
77 | event = message
78 |
79 | # TODO: Filtering User Messages
80 |
81 | if event:
82 | get_running_loop().create_task(self.callback_func(event))
83 |
84 | await asyncio.sleep(5)
85 |
--------------------------------------------------------------------------------
/src/tweety/events/stream_event.py:
--------------------------------------------------------------------------------
1 | class StreamEvent:
2 | pass
--------------------------------------------------------------------------------
/src/tweety/exceptions.py:
--------------------------------------------------------------------------------
1 |
2 | TWITTER_ERRORS = {0: 'DefaultApiError', 3: 'InvalidCoordinates', 4: 'InvalidGranularity', 5: 'InvalidAccuracy', 6: 'NoDataForPoint', 7: 'NoDataForPointRadius', 8: 'InvalidId', 9: 'InvalidMaxResults', 10: 'RockdoveError', 11: 'InvalidIp', 12: 'MustProvideCoordinatesIpQueryOrAttributes', 13: 'NoLocationForIp', 14: 'OverlimitAddressBookApi', 15: 'AddressBookDarkmoded', 16: 'AddressBookPermissionsError', 17: 'AddressBookLookupNotFound', 18: 'TooManyTerms', 19: 'RetweetDarkmoded', 20: 'NoScreenNameProvided', 21: 'ContributorsNotEnabled', 22: 'NotAuthorizedToViewUser', 23: 'BulkLookupDarkmoded', 24: 'UnsupportedProfileImageSize', 25: 'MissingQuery', 26: 'AutocompleteMustBeTrueOrFalse', 27: 'AccountLocked', 28: 'GenericDarkmode', 29: 'TimeOut', 30: 'WoeidDataUnavailable', 31: 'InvalidTimescale', 32: 'InvalidCredentials', 33: 'OverLimit', 34: 'GenericNotFound', 35: 'TrendDataUnavailable', 36: 'CantReportYourselfAsSpam', 37: 'GenericAccessDenied', 38: 'MissingParameter', 39: 'InvalidCreationToken', 41: 'RockdoveInvalidArgumentError', 42: 'InvalidAttribute', 43: 'AttributeAccessDenied', 44: 'InvalidParameter', 46: 'InvalidPlaceJson', 47: 'InvalidRequestUrl', 48: 'TimeoutRequestRainbird', 49: 'NoFollowRequest', 50: 'GenericUserNotFound', 51: 'PromotedContentOfflineError', 52: 'PromotedSearchNoQuery', 53: 'BasicAuthDisabled', 54: 'CassowaryError', 55: 'ResourceNotFound', 56: 'InvalidEmailAddress', 57: 'PasswordResetPermissionsError', 58: 'PasswordResetExpiredToken', 59: 'PasswordResetInvalidHash', 60: 'PasswordResetMismatchedEntries', 61: 'ClientNotPermitted', 62: 'CustomSaveErrors', 63: 'OtherUserSuspended', 64: 'CurrentUserSuspended', 65: 'StrictMustBeTrueOrFalse', 66: 'RequireActivityMustBeTrueOrFalse', 67: 'BackendServiceUnavailable', 68: 'EndpointDeprecated', 69: 'TalonUrlMalware', 70: 'InvalidPromotedContentLogEvent', 71: 'EmailDeliveryError', 72: 'ApplicationNotFound', 73: 'ApplicationNotDeleted', 74: 'ApplicationDomainNotRevoked', 75: 'ApplicationKeysNotReset', 76: 'ApplicationImageNotProcessed', 77: 'ApplicationNoManageRight', 78: 'ApplicationNoAdminRight', 79: 'InvalidTrimPlace', 80: 'CurationDarkmoded', 81: 'ContributorsAccessLevelNotValid', 82: 'ContributorsTargetUserNotSpecified', 83: 'ContributorsTargetUserNotValid', 84: 'TalonUrlUnrenderable', 85: 'ValidationFailure', 86: 'WrongHttpMethod', 87: 'ClientNotPrivileged', 88: 'RateLimitExceeded', 89: 'BadOauthToken', 90: 'ContributionNotPermitted', 91: 'InvalidUtf8', 92: 'SslRequired', 93: 'DmAccessRequired', 94: 'PageIsForbidden', 95: 'InvalidLanguage', 96: 'InvalidIds', 97: 'EndpointFeatureDeprecated', 98: 'FlagPossiblySensitiveScribeError', 99: 'AuthenticityTokenError', 100: 'GenericThriftException', 101: 'InvalidReverseAuthCredentials', 102: 'DarkmodedFeature', 103: 'TrendsAvailableTransientException', 104: 'ListAdminRightsError', 105: 'MaximumMembersExceeded', 106: 'AddBlockedUserError', 107: 'NoTargetUser', 108: 'TargetUserNotFound', 109: 'TargetUserNotRelatedToList', 110: 'ListNotAMemberError', 111: 'TargetUserSuspended', 112: 'InsufficientListParameters', 113: 'InsufficientTargetUserParameters', 114: 'InvalidCurrentPassword', 115: 'ListUnauthorizedSubscriptionError', 116: 'PasswordSmsResetPwSeedNotExist', 117: 'PasswordSmsResetOptOut', 118: 'ArgumentTooLarge', 119: 'NarrowcastNotSupported', 120: 'AccountUpdateFailure', 121: 'InvalidHexColor', 122: 'UpdateProfileColorsError', 123: 'ImageUpdateError', 124: 'AttributeUpdateError', 125: 'GeolocationError', 126: 'LoggedOut', 127: 'ArchiveDeprecated', 128: 'LocationUpdateFailure', 129: 'EmailRateLimitExceeded', 130: 'OverCapacity', 131: 'InternalError', 132: 'UnusedBackgroundUploadError', 133: 'NoSelectedBackgroundError', 134: 'TooManyDevices', 135: 'OauthTimestampException', 136: 'BlockedUserError', 137: 'PushForbidden', 138: 'FollowingInformationUnavailable', 139: 'DuplicateFavorite', 140: 'FollowingStatusUnauthorized', 141: 'InactiveUser', 142: 'ProtectedStatusFavoriteError', 143: 'FavoriteRateLimitExceeded', 144: 'StatusNotFound', 145: 'RecordInvalid', 146: 'OtherUserNotBlocked', 147: 'SelfBlockError', 148: 'UnsupportedDevice', 149: 'InvalidEnabledFor', 150: 'DirectMessageOtherUserNotFollowing', 151: 'MessageSendError', 152: 'DirectMessageDestroyPermissionsError', 153: 'DirectMessageDeleteError', 154: 'DirectMessageNotFound', 155: 'MessageSendUnknownError', 156: 'DowntimeAlert', 157: 'VerifiedDeviceNotFound', 158: 'SelfFollowError', 159: 'GenericSuspended', 160: 'DuplicateFollowRequest', 161: 'FollowRateLimitExceeded', 162: 'FollowBlockedUserError', 163: 'IndeterminateSource', 164: 'TargetUserNotSpecified', 165: 'MultipleMissingParameters', 166: 'MultipleUserNotFound', 167: 'FollowError', 168: 'StatusNotFoundForbidden', 169: 'StatusRelatedResultsForbidden', 170: 'ForbiddenMissingParameter', 171: 'SearchDeletionError', 172: 'SearchCreationError', 173: 'ConfirmEmailExpiredCode', 174: 'ConfirmEmailInvalidCode', 175: 'ConfirmEmailInvalidStateChange', 176: 'ConfirmEmailAlreadyConfirmed', 177: 'ConfirmEmailSuccessChanged', 178: 'ConfirmEmailSuccessNew', 179: 'StatusViewForbidden', 180: 'GenericEndpointOffline', 181: 'TimeParameterOrderError', 182: 'ParameterDeprecated', 183: 'StatusActionPermissionError', 184: 'StatusUpdateError', 185: 'OverStatusUpdateLimit', 186: 'StatusTooLongError', 187: 'DuplicateStatusError', 188: 'StatusMalwareError', 189: 'StatusCreationError', 190: 'UnknownInterpreterError', 191: 'OverPhotoLimit', 192: 'OverMediaEntitiesPerUpdateLimit', 193: 'MediaTooLarge', 194: 'StatusUpdateForbidden', 195: 'InvalidRequestUrlForbidden', 196: 'TimelineAuthorizationRequired', 197: 'CategoryNotFound', 198: 'ContactLoadError', 199: 'IdsOfContactsError', 200: 'GenericForbidden', 201: 'GetRequired', 202: 'InternalApplicationAuthenticationDenied', 203: 'DeviceError', 204: 'DestinationError', 205: 'SpamRateLimitExceeded', 206: 'InvalidDeviceRelationship', 207: 'AlreadyActivated', 208: 'FormatNotSupported', 209: 'DirectMessageMustFollowFirst', 210: 'TokenLimitExceeded', 211: 'InvalidBrandBanner', 212: 'ProfileBannerUploadsDisabled', 213: 'ProcessingInProgress', 214: 'GenericBadRequest', 215: 'BadAuthenticationData', 216: 'ShareViaEmailRateLimitExceeded', 217: 'ProtectedStatusShareViaEmailError', 218: 'RestrictedAccessShareViaEmailError', 219: 'ShareViaEmailIpRateLimitExceeded', 220: 'RestrictedAuthToken', 221: 'CursorInvalid', 222: 'TieredActionSignupSpammer', 223: 'EmailTweetSendingError', 224: 'MissingEmailAddress', 225: 'TieredActionFollowSpammer', 226: 'TieredActionTweetSpammer', 227: 'TieredActionFollowCreeper', 228: 'TieredActionTweetCreeper', 229: 'AmbiguousCredentials', 230: 'UserSleeping', 231: 'RequiresLoginVerification', 232: 'CannotEnableLoginVerificationPhone', 233: 'CannotEnableLoginVerificationAlreadyEnabled', 234: 'CannotEnableLoginVerificationUnconfirmedEmail', 235: 'ExpiredLoginVerificationRequest', 236: 'IncorrectChallengeResponse', 237: 'MissingLoginVerificationRequest', 238: 'NewPasswordWeak', 239: 'BadGuestToken', 240: 'TieredActionSignupSpammerPhoneVerify', 241: 'RejectedLoginVerificationRequest', 242: 'DeactivatedUser', 243: 'OverLimitLogin', 244: 'ForcePasswordReset', 245: 'OverLimitLoginVerificationStart', 246: 'OverLimitLoginVerificationAttempt', 247: 'CannotEnableLoginVerificationPush', 248: 'LoginVerificationAlreadyEnabled', 249: 'CloudIpRestricted', 250: 'UserMustBeAlcoholAgeScreened', 251: 'EndpointRetired', 252: 'DmSpamTimeout', 253: 'NotYetApprovedLoginVerification', 254: 'OfflineCodeSync', 255: 'RequiresTemporaryPassword', 256: 'CannotFollowFromCountry', 257: 'BadDeviceToken', 258: 'AppsCreateRequiresConfirmedEmail', 259: 'AppsCreateRequiresVerifiedPhone', 260: 'AppsCreateRejectedForAbuse', 261: 'AppInReadOnlyMode', 262: 'CurrentUserNeedsPhoneVerification', 263: 'TieredActionChallengeCaptcha', 264: 'TargetUserNotFollowing', 265: 'TargetUserNotFavoriteFollowing', 266: 'FailureSendingLoginVerificationRequest', 267: 'InvalidCredentialsOneFactorEligible', 268: 'MissingOneFactorLoginVerificationParams', 269: 'UserIsNotSdkUser', 270: 'AppsUpdateSettingsRequiresVerifiedPhone', 271: 'SelfMuteError', 272: 'NotMutingTargetUser', 273: 'ScheduledInPast', 274: 'ScheduledTooFarInFuture', 275: 'TooLateToEdit', 276: 'ScheduleInvalid', 277: 'DirectMessageRecipientDoesNotFollowSenderWithUnverifiedPhoneNumber', 278: 'DirectMessageUserNotInConversation', 279: 'DirectMessageConversationNotFound', 280: 'DirectMessageTooManyParticipants', 281: 'DirectMessageTooFewParticipants', 282: 'DirectMessageRecipientBlocksSender', 283: 'TieredActionFavoriteSpammer', 284: 'DeviceRegistrationGeneralError', 285: 'DeviceAlreadyRegistered', 286: 'DeviceOperatorUnsupported', 287: 'UserAlreadyHasVerifiedPhone', 288: 'CannotReuseCurrentPassword', 289: 'DevicePinInvalid', 290: 'DevicePinRequired', 291: 'UnexpectedDeviceProvided', 292: 'TieredActionConversationSpammer', 293: 'SmsVerifyGeneralError', 294: 'SmsVerifyInvalidPin', 295: 'SmsVerifyRateLimitExceeded', 296: 'DtabOverrideDarkmoded', 297: 'DirectMessageCannotHaveBothTweetAndMedia', 298: 'DirectMessageTweetNotFound', 299: 'DeviceRegistrationRateExceeded', 300: 'DeviceRegistrationInvalidInput', 301: 'DeviceRegistrationPending', 302: 'DeviceRegistrationOperationFailed', 303: 'DeviceRegistrationPhoneNormalizationFailed', 304: 'DeviceRegistrationPhoneCountryDetectionFailed', 305: 'CannotIdentifyByEmail', 306: 'TieredActionAccessTokenGrantSpam', 307: 'TieredActionAccessTokenRevokeSpam', 308: 'NoSmsVerifyExists', 309: 'DeviceNotVerified', 310: 'ExpiredPin', 311: 'DirectMessageDuplicate', 312: 'LocationNameMustBeSpecified', 313: 'EULANotAccepted', 314: 'VideoTranscodingError', 315: 'ClientCaptchaRequired', 316: 'CannotContributeToYourself', 317: 'AccountHasTooManyContributors', 318: 'AccountHasTooManyContributees', 319: 'CannotChangePassword', 320: 'ContributorsAccessLevelInsufficient', 321: 'DirectMessageConversationNameTooLong', 322: 'DirectMessageGenericUserCouldNotBeAdded', 323: 'AnimatedGifMultipleImages', 324: 'InvalidMediaId', 325: 'MediaNotFound', 326: 'AccessDeniedByBouncer', 327: 'AlreadyRetweeted', 328: 'InvalidRetweetForStatus', 329: 'NonsupportingClientRequiresLoginVerification', 330: 'ContributorsGenericUserCouldNotBeAdded', 331: 'MobileSettingsUserNotFound', 332: 'MobileSettingsTemplateNotFound', 333: 'MobileSettingsFileNotFound', 334: 'MobileSettingsUnsupportedTransport', 335: 'MobileSettingsSettingNotFound', 336: 'MobileSettingsInvalidValueFound', 337: 'MobileSettingsSettingObjectNotFound', 338: 'MobileSettingsEnabledForMissing', 339: 'MobileSettingsNoDevicesFound', 340: 'MobileSettingsNoIncomingPushSettings', 341: 'MobileSettingsNoIncomingSmsSettings', 342: 'MobileSettingsIncorrectApplicationId', 343: 'MobileSettingsNoIncomingSettings', 344: 'UserActionRateLimitExceeded', 345: 'OneFactorMethodIsNotSupported', 346: 'UserIsNotOneFactorEligible', 347: 'InvalidRequestToken', 348: 'ClientApplicationNotPermitted', 349: 'DirectMessageCannotDmOtherUser', 350: 'OauthException', 351: 'MobileSettingsCouldNotUpdateSleep', 352: 'ParameterLimitExceeded', 353: 'DeniedByApiCsrfProtection', 354: 'DirectMessageTooLongError', 355: 'GenericConflict', 356: 'GenericValidationFailure', 357: 'RequiredFieldMissing', 358: 'JsonProcessingError', 359: 'ValueTooLarge', 360: 'ValueTooSmall', 361: 'ValueCannotBeEmpty', 362: 'TimeNotFuture', 363: 'InvalidCountryCodes', 364: 'InvalidTimeGranularity', 365: 'InvalidUUID', 366: 'InvalidValues', 367: 'SizeOutOfRange', 368: 'TimeNotPast', 369: 'InvalidJsonSyntax', 370: 'DigitsCannotReuseCurrentEmail', 371: 'MentionLimitInTweetExceeded', 372: 'UrlLimitInTweetExceeded', 373: 'HashtagLimitInTweetExceeded', 374: 'ExpiredQrCode', 375: 'InvalidQrCode', 376: 'MissingCredentials', 377: 'TokenRetrievalException', 378: 'TokenMissing', 379: 'DataminrUserNotLinked', 380: 'ABLiveSyncIsDisabled', 381: 'SoftUserCreationSpamDenied', 382: 'SoftUserActionSpamDenied', 383: 'CashtagLimitInTweetExceeded', 384: 'HashtagLengthLimitInTweetExceeded', 385: 'InReplyToTweetNotFound', 386: 'AttachmentTypesLimitInTweetExceeded', 387: 'NotEnoughFollowers', 388: 'FeatureAccessLimited', 389: 'DirectMessagesSenderBlocksRecipient', 390: 'SearchRecordingNotFound', 391: 'MaximumSearchRecordingsExceeded', 392: 'SessionNotFound', 393: 'SessionModificationNotAuthorized', 394: 'SessionModificationFailed', 395: 'VoiceVerifyRateLimitExceeded', 396: 'BlockUserFailed', 397: 'InvalidMetricsJson', 398: 'OnboardingFlowFailure', 399: 'OnboardingFlowRetriableFailure', 400: 'NoTwoFactorAuthMethodFound', 401: 'MomentCapsuleAccessError', 402: 'CannotEnrollLoginVerificationNotYetEnabled', 403: 'IneligibleFor2faAfterModification', 404: 'CookiesRequired', 405: 'DuplicateBookmark', 406: 'ProtectedTweetBookmarkError', 407: 'DirectMessageInactiveDevice', 408: 'InvalidUrl', 409: 'BirthdateRequired', 410: 'PasswordVerificationRequired', 411: 'DirectMessageSenderInSecretDmsDisabledCountry', 412: 'DirectMessageRecipientInSecretDmsDisabledCountry', 413: 'DirectMessageSenderDeviceIsNotActiveForSecretDms', 414: 'DirectMessageRecipientDeviceIsNotActiveForSecretDms', 415: 'CallbackUrlLocked', 416: 'InvalidOrSuspendedApp', 417: 'InvalidDesktopCallback', 418: 'DirectMessageSenderIsNotRegisteredForSecretDms', 419: 'DirectMessageRecipientIsNotRegisteredForSecretDms', 420: 'ReservedErrorCode', 421: 'TweetIsBounced', 422: 'TweetIsBounceDeleted', 423: 'InvalidHeaders', 424: 'MomentUnavailableForNewsCamera', 425: 'TweetEngagementsLimited', 426: 'InvalidRequestIpv6Token', 427: 'IpResolverNotAvailable', 428: 'ValidIpv6TokenRequired', 429: 'HarmfulLink', 430: 'ConversationControlNotAllowed', 431: 'ConversationControlNotSupported', 432: 'ConversationControlNotAuthorized', 433: 'ConversationControlReplyRestricted', 434: 'NotMutingTargetList', 435: 'ConversationControlInvalidParameter', 436: 'PassswordRequiredForEmailUpdate', 437: 'NewPasswordShort', 438: 'NewPasswordLong', 439: 'NudgeReceived', 440: 'CommunityUserNotAuthorized', 441: 'CommunityNotFound', 442: 'CommunityRetweetNotAllowed', 443: 'CommunityInvalidParams', 444: 'CommunityReplyTweetNotAllowed', 445: 'RestrictedSession', 446: 'TokenSecurityLevelAgreementPolicyFailure', 447: 'SuperFollowsCreateNotAuthorized', 448: 'SuperFollowsInvalidParams', 449: 'TOOMomentsList', 450: 'CommunityProtectedUserCannotTweet', 451: 'ExclusiveTweetEngagementNotAllowed', 452: 'SteamCreationException', 453: 'V11Restricted', 454: 'SteamGetException', 455: 'TrustedFriendsInvalidParams', 456: 'TrustedFriendsRetweetNotAllowed', 457: 'TrustedFriendsEngagementNotAllowed', 458: 'CollabTweetInvalidParams', 459: 'TrustedFriendsCreateNotAllowed', 460: 'TrustedFriendsQuoteTweetNotAllowed', 461: 'StaleTweetEngagementNotAllowed', 462: 'StaleTweetQuoteTweetNotAllowed', 463: 'RetweetIdBookmarkError', 464: 'FieldEditNotAllowed', 465: 'StaleTweetRetweetNotAllowed', 466: 'NotEligibleForEdit', 467: 'V11TierRestricted', 468: 'DirectMessageUnregisteredDevice', 469: 'DirectMessageSenderIsNotBlueVerifiedForSecretDms', 470: 'DirectMessageRecipientIsNotBlueVerifiedForSecretDms', 471: 'ConversationKeysNotProvidedForNewSecretConversation', 472: 'ConversationKeysProvidedForExistingSecretConvo', 473: 'InvalidConversationKeysProvidedForNewSecretConvo', 474: 'DirectMessageReplyNotInConversation', 475: 'DirectMessageConversationMetadataNotFound', 476: 'DirectMessageSenderIsNotVerifiedForMessageRequests', 477: 'DirectMessageSenderIsNotVerifiedRateLimited'}
3 |
4 |
5 | class TwitterError(Exception):
6 | """
7 | Base Exception raised when error Occurs.
8 |
9 | Attributes:
10 | message -- explanation of the error
11 | """
12 |
13 | def __init__(
14 | self,
15 | error_code,
16 | error_name,
17 | response,
18 | message
19 | ):
20 | self.message = message
21 | self.error_code = error_code
22 | self.error_name = error_name
23 | self.response = response
24 |
25 | if isinstance(self.response, str):
26 | self.message = f"[{self.error_code}] {self.response}"
27 | elif self.response is not None and not isinstance(self.response, dict) and not self.response.json() and self.response.text:
28 | self.message = f"[{self.error_code}] {self.response.text}"
29 | elif str(self.error_code) == "404":
30 | self.message = "Page not Found. Most likely you need elevated authorization to access this resource"
31 |
32 | super().__init__(self.message)
33 |
34 |
35 | class ConversationNotFound(TwitterError):
36 | """
37 | Exception raised when conversation is not Found.
38 |
39 | Attributes:
40 | message -- explanation of the error
41 | """
42 |
43 | def __init__(self, error_code=404, error_name="ConversationNotFound", response=None, message="Conversation Not Found", **kw):
44 | super().__init__(error_code, error_name, response, message)
45 |
46 |
47 | class UserNotFound(TwitterError):
48 | """Exception raised when user isn't found.
49 |
50 | Attributes:
51 | message -- explanation of the error
52 | """
53 |
54 | def __init__(self, error_code=50, error_name="GenericUserNotFound", response=None, message="The User Account wasn't Found or is Protected", **kw):
55 | super().__init__(error_code, error_name, response, message)
56 |
57 |
58 | class GuestTokenNotFound(TwitterError):
59 | """
60 | Exception Raised when the guest token wasn't found after specific number of retires
61 |
62 | Attributes:
63 | message -- explanation of the error
64 | """
65 |
66 | def __init__(self, error_code=404, error_name="GuestTokenNotFound", response=None, message="The Guest Token couldn't be obtained", **kw):
67 | super().__init__(error_code, error_name, response, message)
68 |
69 |
70 | class InvalidTweetIdentifier(TwitterError):
71 | """
72 | Exception Raised when the tweet identifier is invalid
73 |
74 | Attributes:
75 | message -- explanation of the error
76 | """
77 |
78 | def __init__(self, error_code=144, error_name="StatusNotFound", response=None, message="The Tweet Identifier is Invalid", **kw):
79 | super().__init__(error_code, error_name, response, message)
80 |
81 |
82 | class RateLimitReached(TwitterError):
83 | """
84 | Exception Raised when the tweet identifier is invalid
85 |
86 | Attributes:
87 | message -- explanation of the error
88 | """
89 |
90 | def __init__(self, error_code, error_name, response, message="You have exceeded the Twitter Rate Limit", **kw):
91 | super().__init__(error_code, error_name, response, message)
92 | self.retry_after = kw.get('retry_after') # Number of seconds required for rate limit to be reset
93 |
94 |
95 | class ProxyParseError(TwitterError):
96 | """
97 | Exception Raised when an error occurs while parsing the provided proxy
98 |
99 | Attributes:
100 | message -- explanation of the error
101 | """
102 |
103 | def __init__(self, message="Error while parsing the Proxy, please make sure you are passing the right formatted proxy", **kw):
104 | super().__init__(0, "ProxyParseError", None, message)
105 |
106 |
107 | class UserProtected(TwitterError):
108 | """
109 | Exception Raised when an error occurs when the queried User isn't available / Protected
110 |
111 | Attributes:
112 | message -- explanation of the error
113 | """
114 |
115 | def __init__(self, error_code=403, error_name="UserUnavailable", response=None, message=None, **kw):
116 |
117 | if not message:
118 | message = "The User is Protected OR Unavailable, please make sure you are authenticated and authorized"
119 |
120 | super().__init__(error_code, error_name, response, message)
121 |
122 |
123 | class DeniedLogin(TwitterError):
124 | """
125 | Exception Raised when the Twitter deny the login request ,
126 | could be due to multiple login attempts (or failed attempts)
127 |
128 | Attributes:
129 | message -- explanation of the error
130 | """
131 |
132 | def __init__(self, error_code=37, error_name="GenericAccessDenied", response=None, message=None, **kw):
133 | super().__init__(error_code, error_name, response, message)
134 |
135 |
136 | class ActionRequired(TwitterError):
137 | """
138 | Exception Raised when the Twitter Login Request require an additional step from the user
139 |
140 | Attributes:
141 | message -- explanation of the error
142 | """
143 |
144 | def __init__(self, error_code, error_name, response, message, **kw):
145 | super().__init__(error_code, error_name, response, message)
146 |
147 |
148 | class InvalidCredentials(TwitterError):
149 | """
150 | Exception Raised when cookies credentials are invalid
151 |
152 | Attributes:
153 | message -- explanation of the error
154 | """
155 |
156 | def __init__(self, error_code, error_name, response, message="The Cookies are Invalid", **kw):
157 | super().__init__(error_code, error_name, response, message)
158 |
159 |
160 | class InvalidBroadcast(TwitterError):
161 | """
162 | Exception Raised when cookies are required for making a specific request
163 |
164 | Attributes:
165 | message -- explanation of the error
166 | """
167 |
168 | def __init__(self, error_code, error_name, response, message="The Broadcast doesn't exists", **kw):
169 | super().__init__(error_code, error_name, response, message)
170 |
171 |
172 | class AuthenticationRequired(TwitterError):
173 | """
174 | Exception Raised when cookies are required for making a specific request
175 |
176 | Attributes:
177 | message -- explanation of the error
178 | """
179 |
180 | def __init__(self, error_code, error_name, response, message="You need to be authenticated and connected to make this request", **kw):
181 | super().__init__(error_code, error_name, response, message)
182 |
183 |
184 | class ListNotFound(TwitterError):
185 | """
186 | Exception Raised when queried list wasn't found
187 |
188 | Attributes:
189 | message -- explanation of the error
190 | """
191 |
192 | def __init__(self, error_code, error_name, response, message="List not Found", **kw):
193 | super().__init__(error_code, error_name, response, message)
194 |
195 |
196 | class AudioSpaceNotFound(TwitterError):
197 | """
198 | Exception Raised when queried Audio Space isn't found
199 |
200 | Attributes:
201 | message -- explanation of the error
202 | """
203 |
204 | def __init__(self, error_code, error_name, response, message="Audio Space not found", **kw):
205 | super().__init__(error_code, error_name, response, message)
206 |
207 |
208 | class ProtectedTweet(TwitterError):
209 | """
210 | Exception Raised when queried Tweet is protected, and you need authorization to access it
211 |
212 | Attributes:
213 | message -- explanation of the error
214 | """
215 |
216 | def __init__(self, error_code, error_name, response, message="Tweet is private/protected", **kw):
217 | super().__init__(error_code, error_name, response, message)
218 |
219 |
220 | class LockedAccount(TwitterError):
221 | """
222 | Exception Raised when Twitter Account is locked and most likely requires captcha test to unlock
223 |
224 | Attributes:
225 | message -- explanation of the error
226 | """
227 |
228 | def __init__(self, error_code, error_name, response, message="Your Account is Locked", **kw):
229 | super().__init__(error_code, error_name, response, message)
230 |
231 |
232 | class SuspendedAccount(TwitterError):
233 | """
234 | Exception Raised when Twitter Account is Suspended
235 |
236 | Attributes:
237 | message -- explanation of the error
238 | """
239 |
240 | def __init__(self, error_code, error_name, response, message="Your Account is Suspended", **kw):
241 | super().__init__(error_code, error_name, response, message)
242 |
243 |
244 | class ArkoseLoginRequired(TwitterError):
245 | """
246 | Exception Raised when you need to solve a captcha in Login Flow
247 |
248 | Attributes:
249 | message -- explanation of the error
250 | """
251 |
252 | def __init__(self, error_code=403, error_name="ArkoseLoginRequired", response=None, message="ArkoseLogin(Captcha) Detected while logging-in, please do restart the process with 'captcha_solver'", **kw):
253 | super().__init__(error_code, error_name, response, message)
254 |
255 | class CaptchaSolverFailed(TwitterError):
256 | """
257 | Exception Raised when you need to solve a captcha in Login Flow
258 |
259 | Attributes:
260 | message -- explanation of the error
261 | """
262 |
263 | def __init__(self, error_code=0, error_name="CaptchaSolverFailed", response=None, message="ArkoseLogin(Captcha) Detected while logging-in, please do restart the process with 'captcha_solver'",**kw):
264 | super().__init__(error_code, error_name, response, message)
265 |
266 |
267 | class UploadFailed(TwitterError):
268 | """
269 | Exception Raised when file upload failed
270 |
271 | Attributes:
272 | message -- explanation of the error
273 | """
274 |
275 | def __init__(self, error_code=400, error_name="UploadFailed", response=None, message="Unknown Error Occurred while uploading File",**kw):
276 | super().__init__(error_code, error_name, response, message)
277 |
278 |
279 | # For Backward Compatibility
280 | UnknownError = TwitterError
--------------------------------------------------------------------------------
/src/tweety/exceptions_.py:
--------------------------------------------------------------------------------
1 | """
2 | This module is an alias for `exceptions` for backward compatibility.
3 | """
4 |
5 | from .exceptions import *
--------------------------------------------------------------------------------
/src/tweety/filters.py:
--------------------------------------------------------------------------------
1 |
2 | class _CallableString(str):
3 | """
4 |
5 | For backward compatibility where user can still call the attributes as method
6 |
7 | """
8 | def __init__(self, __value__):
9 | super().__init__()
10 |
11 | def __call__(self, *args, **kwargs):
12 | return self
13 |
14 |
15 | class SearchFilters:
16 | """
17 | This class can be used to filter the search results
18 | """
19 | Users = _CallableString("People")
20 | Latest = _CallableString("Latest")
21 | Media = _CallableString("Media")
22 | Lists = _CallableString("Lists")
23 |
24 | class TweetCommentFilters:
25 | Likes = _CallableString("Likes")
26 | Latest = _CallableString("Recency")
27 | Relevant = Relevancy = _CallableString("Relevance")
28 |
29 | class TweetConversationFilters:
30 | """
31 | This class can be used to filter the audience of posted Tweet
32 | """
33 |
34 | PeopleYouMention = _CallableString("ByInvitation")
35 | PeopleYouFollow = _CallableString("Community")
36 | Subscribers = _CallableString("Subscribers")
37 | VerifiedUsers = _CallableString("Verified")
38 |
39 |
40 | class CommunityTweets:
41 | Top = _CallableString("Top")
42 |
43 |
44 | class CommunityMembers:
45 | Moderators = _CallableString("Mods")
46 |
47 |
48 | class Language:
49 | Urdu = URDU = _CallableString("ur")
50 | Russian = RUSSIAN = _CallableString("ru")
51 | Danish = DANISH = _CallableString("da")
52 | Filipino = FILIPINO = _CallableString("fil")
53 | Irish = IRISH = _CallableString("ga")
54 | TraditionalChinese = TRADITIONAL_CHINESE = _CallableString("zh-tw")
55 | Hungarian = HUNGARIAN = _CallableString("hu")
56 | Spanish = SPANISH = _CallableString("es")
57 | Arabic_Feminine = ARABIC_FEMININE = _CallableString("ar-x-fm")
58 | Croatian = CROATIAN = _CallableString("hr")
59 | French = FRENCH = _CallableString("fr")
60 | Kannada = KANNADA = _CallableString("kn")
61 | Italian = ITALIAN = _CallableString("it")
62 | Marathi = MARATHI = _CallableString("mr")
63 | Japanese = JAPANESE = _CallableString("ja")
64 | Indonesian = INDONESIAN = _CallableString("id")
65 | Gujarati = GUJARATI = _CallableString("gu")
66 | Romanian = ROMANIAN = _CallableString("ro")
67 | Turkish = TURKISH = _CallableString("tr")
68 | Basque = BASQUE = _CallableString("eu")
69 | Swedish = SWEDISH = _CallableString("sv")
70 | Tamil = TAMIL = _CallableString("ta")
71 | Thai = THAI = _CallableString("th")
72 | Ukrainian = UKRAINIAN = _CallableString("uk")
73 | Bangla = BANGLA = _CallableString("bn")
74 | German = GERMAN = _CallableString("de")
75 | Vietnamese = VIETNAMESE = _CallableString("vi")
76 | Catalan = CATALAN = _CallableString("ca")
77 | Arabic = ARABIC = _CallableString("ar")
78 | Dutch = DUTCH = _CallableString("nl")
79 | SimplifiedChinese = SIMPLIFIED_CHINESE = _CallableString("zh-cn")
80 | Slovak = SLOVAK = _CallableString("sk")
81 | Czech = CZECH = _CallableString("cs")
82 | Greek = GREEK = _CallableString("el")
83 | Finnish = FINNISH = _CallableString("fi")
84 | English = ENGLISH = _CallableString("en")
85 | Norwegian = NORWEGIAN = _CallableString("no")
86 | Polish = POLISH = _CallableString("pl")
87 | Portuguese = PORTUGUESE = _CallableString("pt")
88 | Persian = PERSIAN = _CallableString("fa")
89 | Galician = GALICIAN = _CallableString("gl")
90 | Korean = KOREAN = _CallableString("ko")
91 | Serbian = SERBIAN = _CallableString("sr")
92 | BritishEnglish = BRITISH_ENGLISH = _CallableString("en-gb")
93 | Hindi = HINDI = _CallableString("hi")
94 | Hebrew = HEBREW = _CallableString("he")
95 | Malay = MALAY = _CallableString("msa")
96 | Bulgarian = BULGARIAN = _CallableString("bg")
--------------------------------------------------------------------------------
/src/tweety/session.py:
--------------------------------------------------------------------------------
1 | import json
2 | import os.path
3 | from .utils import dict_to_string
4 |
5 | class Session:
6 | def __init__(self, client):
7 | self._client = client
8 | self.user = None
9 | self.logged_in = False
10 | self.cookies = {}
11 |
12 | def cookies_dict(self):
13 |
14 | if isinstance(self.cookies, dict):
15 | return self.cookies
16 |
17 | result = {}
18 | split = str(self.cookies).split(";")
19 | for i in split:
20 | try:
21 | key, value = i.split("=")
22 | result[key] = value
23 | except:
24 | pass
25 |
26 | return result
27 |
28 | async def save_session(self, cookies, user):
29 | self.logged_in = True
30 |
31 | if hasattr(cookies, "to_dict"):
32 | cookies = cookies.to_dict()
33 |
34 | self.cookies = cookies or self.cookies
35 | self.user = user or self.user
36 |
37 | def __str__(self):
38 | if isinstance(self.cookies, dict):
39 | return dict_to_string(self.cookies)
40 | return str(self.cookies)
41 |
42 |
43 | class MemorySession(Session):
44 |
45 | def __init__(self):
46 | super().__init__(None)
47 |
48 | def __call__(self, client):
49 | self._client = client
50 | return self
51 |
52 | async def save_session(self, cookies, user):
53 | self.logged_in = True
54 |
55 | if hasattr(cookies, "to_dict"):
56 | cookies = cookies.to_dict()
57 |
58 | self.cookies = cookies or self.cookies
59 | self.user = user or self.user
60 |
61 |
62 | class FileSession(Session):
63 | def __init__(self, client, session_name):
64 | super().__init__(client)
65 | self.session_name = os.path.basename(session_name)
66 | self.session_file_path = self._get_session_file_path(session_name, self.session_name)
67 | self._load_session()
68 |
69 | @staticmethod
70 | def _get_session_file_path(session_path, session_name):
71 | _session = session_name.replace(".tw_session", "")
72 | directory = os.path.dirname(session_path) or os.getcwd()
73 | return os.path.abspath(os.path.join(directory, f"{_session}.tw_session"))
74 |
75 | async def save_session(self, cookies, user):
76 | await super().save_session(cookies, user)
77 | session_data = {"cookies": self.cookies, "user": self.user}
78 |
79 | with open(self.session_file_path, "w") as f:
80 | json.dump(session_data, f, default=str)
81 |
82 | def _load_session(self):
83 | if os.path.exists(self.session_file_path):
84 | with open(self.session_file_path, "r") as f:
85 | session_data = json.load(f)
86 | self.cookies = session_data['cookies']
87 | self.user = session_data.get('user', {})
88 |
89 | self.logged_in = True
90 |
91 |
--------------------------------------------------------------------------------
/src/tweety/transaction.py:
--------------------------------------------------------------------------------
1 | """
2 | Credit : https://github.com/iSarabjitDhiman/TweeterPy/tree/master/tweeterpy/tid
3 | """
4 |
5 | import re
6 | import bs4
7 | import math
8 | import time
9 | import random
10 | import base64
11 | import hashlib
12 | import httpx
13 | from functools import reduce
14 | from typing import Union, List
15 | from .utils import float_to_hex, is_odd, base64_encode
16 |
17 | ON_DEMAND_FILE_REGEX = re.compile(
18 | r"""['|\"]{1}ondemand\.s['|\"]{1}:\s*['|\"]{1}([\w]*)['|\"]{1}""", flags=(re.VERBOSE | re.MULTILINE))
19 | INDICES_REGEX = re.compile(
20 | r"""(\(\w{1}\[(\d{1,2})\],\s*16\))+""", flags=(re.VERBOSE | re.MULTILINE))
21 |
22 |
23 | def interpolate(from_list: List[Union[float, int]], to_list: List[Union[float, int]], f: Union[float, int]):
24 | if len(from_list) != len(to_list):
25 | raise Exception(f"Mismatched interpolation arguments {from_list}: {to_list}")
26 | out = []
27 | for i in range(len(from_list)):
28 | out.append(interpolate_num(from_list[i], to_list[i], f))
29 | return out
30 |
31 |
32 | def interpolate_num(from_val: List[Union[float, int]], to_val: List[Union[float, int]], f: Union[float, int]):
33 | if all([isinstance(number, (int, float)) for number in [from_val, to_val]]):
34 | return from_val * (1 - f) + to_val * f
35 |
36 | if all([isinstance(number, bool) for number in [from_val, to_val]]):
37 | return from_val if f < 0.5 else to_val
38 |
39 |
40 | def convert_rotation_to_matrix(rotation: Union[float, int]):
41 | rad = math.radians(rotation)
42 | return [math.cos(rad), -math.sin(rad), math.sin(rad), math.cos(rad)]
43 |
44 |
45 | def convertRotationToMatrix(degrees: Union[float, int]):
46 | radians = degrees * math.pi / 180
47 | """
48 | [cos(r), -sin(r), 0]
49 | [sin(r), cos(r), 0]
50 |
51 | in this order:
52 | [cos(r), sin(r), -sin(r), cos(r), 0, 0]
53 | """
54 | cos = math.cos(radians)
55 | sin = math.sin(radians)
56 | return [cos, sin, -sin, cos, 0, 0]
57 |
58 |
59 | class Cubic:
60 | def __init__(self, curves: List[Union[float, int]]):
61 | self.curves = curves
62 |
63 | def get_value(self, _time: Union[float, int]):
64 | start_gradient = end_gradient = start = mid = 0.0
65 | end = 1.0
66 |
67 | if _time <= 0.0:
68 | if self.curves[0] > 0.0:
69 | start_gradient = self.curves[1] / self.curves[0]
70 | elif self.curves[1] == 0.0 and self.curves[2] > 0.0:
71 | start_gradient = self.curves[3] / self.curves[2]
72 | return start_gradient * _time
73 |
74 | if _time >= 1.0:
75 | if self.curves[2] < 1.0:
76 | end_gradient = (self.curves[3] - 1.0) / (self.curves[2] - 1.0)
77 | elif self.curves[2] == 1.0 and self.curves[0] < 1.0:
78 | end_gradient = (self.curves[1] - 1.0) / (self.curves[0] - 1.0)
79 | return 1.0 + end_gradient * (_time - 1.0)
80 |
81 | while start < end:
82 | mid = (start + end) / 2
83 | x_est = self.calculate(self.curves[0], self.curves[2], mid)
84 | if abs(_time - x_est) < 0.00001:
85 | return self.calculate(self.curves[1], self.curves[3], mid)
86 | if x_est < _time:
87 | start = mid
88 | else:
89 | end = mid
90 | return self.calculate(self.curves[1], self.curves[3], mid)
91 |
92 | @staticmethod
93 | def calculate(a, b, m):
94 | return 3.0 * a * (1 - m) * (1 - m) * m + 3.0 * b * (1 - m) * m * m + m * m * m
95 |
96 |
97 | class TransactionGenerator:
98 | DEFAULT_KEYWORD = "obfiowerehiring"
99 | ADDITIONAL_RANDOM_NUMBER = 3
100 | DEFAULT_ROW_INDEX = None
101 | DEFAULT_KEY_BYTES_INDICES = None
102 |
103 | def __init__(self, home_page_html: Union[bs4.BeautifulSoup, httpx.Response]):
104 |
105 | self.home_page_html = self.validate_response(home_page_html)
106 | self.DEFAULT_ROW_INDEX, self.DEFAULT_KEY_BYTES_INDICES = self.get_indices(self.home_page_html)
107 | self.key = self.get_key(response=self.home_page_html)
108 | self.key_bytes = self.get_key_bytes(key=self.key)
109 | self.animation_key = self.get_animation_key(key_bytes=self.key_bytes, response=self.home_page_html)
110 |
111 | def get_indices(self, home_page_html=None):
112 | key_byte_indices = []
113 | response = self.validate_response(home_page_html) or self.home_page_html
114 | on_demand_file = ON_DEMAND_FILE_REGEX.search(str(response))
115 | if on_demand_file:
116 | on_demand_file_url = f"https://abs.twimg.com/responsive-web/client-web/ondemand.s.{on_demand_file.group(1)}a.js"
117 | on_demand_file_response = httpx.get(on_demand_file_url)
118 | key_byte_indices_match = INDICES_REGEX.finditer(
119 | str(on_demand_file_response.text))
120 | for item in key_byte_indices_match:
121 | key_byte_indices.append(item.group(2))
122 | if not key_byte_indices:
123 | raise Exception("Couldn't get animation key indices")
124 | key_byte_indices = list(map(int, key_byte_indices))
125 | return key_byte_indices[0], key_byte_indices[1:]
126 |
127 | def validate_response(self, response: Union[bs4.BeautifulSoup, httpx.Response]):
128 | if not isinstance(response, (bs4.BeautifulSoup, httpx.Response)):
129 | raise Exception("Unable to get Twitter Home Page")
130 | return response if isinstance(response, bs4.BeautifulSoup) else bs4.BeautifulSoup(response.content, 'lxml')
131 |
132 | def get_key(self, response=None):
133 | response = self.validate_response(response) or self.home_page_html
134 | element = response.select_one("[name='twitter-site-verification']")
135 | if not element:
136 | raise Exception("Couldn't get twitter site verification code")
137 | return element.get("content")
138 |
139 | def get_key_bytes(self, key: str):
140 | return list(base64.b64decode(bytes(key, 'utf-8')))
141 |
142 | def get_frames(self, response=None):
143 | response = self.validate_response(response) or self.home_page_html
144 | return response.select("[id^='loading-x-anim']")
145 |
146 | def get_2d_array(self, key_bytes: List[Union[float, int]], response, frames: bs4.ResultSet = None):
147 | if not frames:
148 | frames = self.get_frames(response)
149 | return [[int(x) for x in re.sub(r"[^\d]+", " ", item).strip().split()] for item in list(list(frames[key_bytes[5] % 4].children)[0].children)[1].get("d")[9:].split("C")]
150 |
151 | def solve(self, value, min_val, max_val, rounding: bool):
152 | result = value * (max_val-min_val) / 255 + min_val
153 | return math.floor(result) if rounding else round(result, 2)
154 |
155 | def animate(self, frames, target_time):
156 | from_color = [float(item) for item in [*frames[:3], 1]]
157 | to_color = [float(item) for item in [*frames[3:6], 1]]
158 | from_rotation = [0.0]
159 | to_rotation = [self.solve(float(frames[6]), 60.0, 360.0, True)]
160 | frames = frames[7:]
161 | curves = [self.solve(float(item), is_odd(counter), 1.0, False)
162 | for counter, item in enumerate(frames)]
163 | cubic = Cubic(curves)
164 | val = cubic.get_value(target_time)
165 | color = interpolate(from_color, to_color, val)
166 | color = [value if value > 0 else 0 for value in color]
167 | rotation = interpolate(from_rotation, to_rotation, val)
168 | matrix = convert_rotation_to_matrix(rotation[0])
169 | str_arr = [format(round(value), 'x') for value in color[:-1]]
170 | for value in matrix:
171 | rounded = round(value, 2)
172 | if rounded < 0:
173 | rounded = -rounded
174 | hex_value = float_to_hex(rounded)
175 | str_arr.append(f"0{hex_value}".lower() if hex_value.startswith(
176 | ".") else hex_value if hex_value else '0')
177 | str_arr.extend(["0", "0"])
178 | animation_key = re.sub(r"[.-]", "", "".join(str_arr))
179 | return animation_key
180 |
181 | def get_animation_key(self, key_bytes, response):
182 | total_time = 4096
183 | row_index = key_bytes[self.DEFAULT_ROW_INDEX] % 16
184 | frame_time = reduce(lambda num1, num2: num1*num2,
185 | [key_bytes[index] % 16 for index in self.DEFAULT_KEY_BYTES_INDICES])
186 | arr = self.get_2d_array(key_bytes, response)
187 | frame_row = arr[row_index]
188 |
189 | target_time = float(frame_time) / total_time
190 | animation_key = self.animate(frame_row, target_time)
191 | return animation_key
192 |
193 | def generate_transaction_id(self, method: str, path: str, response=None, key=None, animation_key=None, time_now=None):
194 | try:
195 | time_now = time_now or math.floor(
196 | (time.time() * 1000 - 1682924400 * 1000) / 1000)
197 | time_now_bytes = [(time_now >> (i * 8)) & 0xFF for i in range(4)]
198 | key = key or self.key or self.get_key(response)
199 | key_bytes = self.get_key_bytes(key)
200 | animation_key = animation_key or self.animation_key or self.get_animation_key(
201 | key_bytes, response)
202 | hash_val = hashlib.sha256(
203 | f"{method}!{path}!{time_now}{self.DEFAULT_KEYWORD}{animation_key}".encode()).digest()
204 | hash_bytes = list(hash_val)
205 | random_num = random.randint(0, 255)
206 | bytes_arr = [*key_bytes, *time_now_bytes, *
207 | hash_bytes[:16], self.ADDITIONAL_RANDOM_NUMBER]
208 | out = bytearray(
209 | [random_num, *[item ^ random_num for item in bytes_arr]])
210 | return base64_encode(out).strip("=")
211 | except Exception as error:
212 | raise Exception(f"Couldn't generate transaction ID.\n{error}")
213 |
214 |
215 | if __name__ == "__main__":
216 | pass
217 |
--------------------------------------------------------------------------------
/src/tweety/types/__init__.py:
--------------------------------------------------------------------------------
1 |
2 | from .twDataTypes import (
3 | User,
4 | Excel,
5 | Tweet,
6 | Media,
7 | Stream,
8 | Trends,
9 | RichText,
10 | RichTag,
11 | SelfThread,
12 | Poll,
13 | Choice,
14 | Community,
15 | List,
16 | Symbol,
17 | URL,
18 | EditControl,
19 | Hashtag,
20 | ConversationThread,
21 | Coordinates,
22 | ShortUser,
23 | MediaSize,
24 | Broadcast,
25 | AudioSpace,
26 | Gif,
27 | Topic,
28 | TweetTranslate,
29 | TweetAnalytics,
30 | Place,
31 | ScheduledTweet,
32 | GrokMessage,
33 | GrokShare,
34 | GrokShareMessage,
35 | LiveStreamPayload
36 | )
37 | from .n_types import (
38 | UploadedMedia,
39 | Proxy
40 | )
41 | from .search import Search, TypeHeadSearch
42 | from .usertweet import UserTweets, SelfTimeline, TweetComments, TweetHistory, UserMedia, UserHighlights, UserLikes, ScheduledTweets
43 | from .mentions import Mention
44 | from .inbox import Inbox, SendMessage, Media, Conversation
45 | from .bookmarks import Bookmarks
46 | from .likes import TweetLikes
47 | from .retweets import TweetRetweets
48 | from .community import CommunityTweets, CommunityMembers, UserCommunities
49 | from .notification import TweetNotifications
50 | from .lists import Lists, ListMembers, ListTweets, ListFollowers
51 | from .follow import UserFollowers, UserFollowings, MutualFollowers, BlockedUsers, UserSubscribers
52 | from .gifs import GifSearch
53 | from .topic import TopicTweets
54 | from .places import Places
55 | from ..constants import (
56 | PROXY_TYPE_SOCKS4,
57 | PROXY_TYPE_SOCKS5,
58 | PROXY_TYPE_HTTP,
59 | HOME_TIMELINE_TYPE_FOLLOWING,
60 | HOME_TIMELINE_TYPE_FOR_YOU,
61 | INBOX_PAGE_TYPES,
62 | INBOX_PAGE_TYPE_TRUSTED,
63 | INBOX_PAGE_TYPE_UNTRUSTED,
64 | MEDIA_TYPE_GIF,
65 | MEDIA_TYPE_IMAGE,
66 | MEDIA_TYPE_VIDEO
67 | )
68 |
69 |
70 |
71 |
--------------------------------------------------------------------------------
/src/tweety/types/base.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 | from tweety.types import ShortUser
3 | from .twDataTypes import User, Tweet
4 | from ..utils import find_objects, parse_wait_time
5 |
6 |
7 | class BaseGeneratorClass(dict):
8 |
9 | @staticmethod
10 | def _get_cursor_(response, cursor_key="Bottom"):
11 | cursor = find_objects(response, "cursorType", cursor_key, recursive=False, none_value={})
12 | return cursor.get("value", None)
13 |
14 | def _has_next_page(self, new_cursor):
15 | if new_cursor == self.cursor or new_cursor is None or not new_cursor:
16 | return False
17 |
18 | self.cursor = new_cursor
19 | return True
20 |
21 | @staticmethod
22 | def _get_entries(response, key_value="TimelineAddEntries"):
23 | entry = find_objects(response, "type", key_value)
24 |
25 | if not entry:
26 | return []
27 |
28 | return entry.get('entries', [])
29 |
30 | async def get_next_page(self, cursor=0):
31 | if cursor == 0 and not self.is_next_page:
32 | return []
33 |
34 | cursor = cursor if cursor != 0 else self.cursor
35 |
36 | results, cursor, cursor_top = await self.get_page(cursor)
37 | self.is_next_page = self._has_next_page(cursor)
38 | self.cursor, self.cursor_top = cursor, cursor_top
39 | _result_attr = self._RESULT_ATTR
40 | getattr(self, _result_attr).extend(results)
41 | self[_result_attr] = getattr(self, _result_attr)
42 | self['cursor'], self['cursor_top'], self['is_next_page'] = self.cursor, self.cursor_top, self.is_next_page
43 |
44 | for result in results:
45 | if isinstance(result, (User, ShortUser)):
46 | self.client._cached_users[str(result.username).lower()] = result.id
47 | elif isinstance(result, Tweet):
48 | self.client._cached_users[str(result.author.username).lower()] = result.author.id
49 |
50 | for user in result.user_mentions:
51 | self.client._cached_users[str(user.username).lower()] = user.id
52 |
53 | if result.is_retweet and result.retweeted_tweet:
54 | self.client._cached_users[str(result.retweeted_tweet.author.username).lower()] = result.retweeted_tweet.author.id
55 |
56 | return results
57 |
58 | async def generator(self):
59 | this_page = 0
60 | while this_page != int(self.pages):
61 | try:
62 | results = await self.get_next_page()
63 |
64 | if len(results) == 0:
65 | break
66 |
67 | yield self, results
68 |
69 | if not self.is_next_page:
70 | break
71 |
72 | this_page += 1
73 |
74 | if this_page != self.pages:
75 | this_wait_time = parse_wait_time(self.wait_time)
76 | await asyncio.sleep(this_wait_time)
77 | except asyncio.CancelledError:
78 | break
79 |
80 | def __repr__(self):
81 | class_name = self.__class__.__name__
82 | return "{}(user_id={}, count={})".format(
83 | class_name, self.user_id, self.__len__()
84 | )
85 |
86 | def __getitem__(self, index):
87 | if isinstance(index, str):
88 | return getattr(self, index)
89 |
90 | return getattr(self, self._RESULT_ATTR)[index]
91 |
92 | def __iter__(self):
93 | for i in getattr(self, self._RESULT_ATTR):
94 | yield i
95 |
96 | def __len__(self):
97 | return len(getattr(self, self._RESULT_ATTR))
--------------------------------------------------------------------------------
/src/tweety/types/bookmarks.py:
--------------------------------------------------------------------------------
1 | from .twDataTypes import Tweet, Excel
2 | from .base import BaseGeneratorClass
3 |
4 |
5 | class Bookmarks(BaseGeneratorClass):
6 | _RESULT_ATTR = "tweets"
7 |
8 | def __init__(self, user_id, client, pages=1, wait_time=2, cursor=None):
9 | super().__init__()
10 | self.tweets = []
11 | self.cursor = cursor
12 | self.cursor_top = cursor
13 | self.is_next_page = True
14 | self.client = client
15 | self.user_id = user_id
16 | self.pages = pages
17 | self.wait_time = wait_time
18 |
19 | async def get_page(self, cursor):
20 | _tweets = []
21 | response = await self.client.http.get_bookmarks(cursor=cursor)
22 |
23 | entries = self._get_entries(response)
24 | for entry in entries:
25 | try:
26 | parsed = Tweet(self.client, entry, response)
27 | if parsed:
28 | _tweets.append(parsed)
29 | except:
30 | pass
31 |
32 | cursor = self._get_cursor_(response)
33 | cursor_top = self._get_cursor_(response, "Top")
34 | return _tweets, cursor, cursor_top
35 |
36 | def to_xlsx(self, filename=None):
37 | return Excel(self, filename)
38 |
39 |
--------------------------------------------------------------------------------
/src/tweety/types/community.py:
--------------------------------------------------------------------------------
1 | from .twDataTypes import SelfThread, Tweet, Excel, User, Community
2 | from .base import BaseGeneratorClass
3 | from ..utils import find_objects
4 |
5 |
6 | class UserCommunities(BaseGeneratorClass):
7 | OBJECTS_TYPES = {
8 | "community": Community
9 | }
10 | _RESULT_ATTR = "communities"
11 |
12 | def __init__(self, client, user_id):
13 | super().__init__()
14 | self.communities = []
15 | self.cursor = None
16 | self.is_next_page = True
17 | self.client = client
18 | self.user_id = user_id
19 | self.pages = 1
20 | self.wait_time = None
21 |
22 | @staticmethod
23 | def _get_users(response):
24 | all_users = find_objects(response, "__typename", "User", none_value=[])
25 | return all_users
26 |
27 | async def get_page(self, cursor=None):
28 | _communities = []
29 | response = await self.client.http.get_user_communities(self.user_id)
30 | entries = self._get_entries(response)
31 |
32 | for entry in entries:
33 | try:
34 | parsed = Community(self.client, entry, None)
35 | if parsed:
36 | _communities.append(parsed)
37 | except:
38 | pass
39 |
40 | cursor = find_objects(response, "next_cursor", value=None)
41 | cursor_top = self._get_cursor_(response, "Top")
42 |
43 | return _communities, cursor, cursor_top
44 |
45 | def __repr__(self):
46 | return "UserCommunities(user_id={}, count={})".format(
47 | self.user_id, self.__len__()
48 | )
49 |
50 |
51 | class CommunityTweets(BaseGeneratorClass):
52 | OBJECTS_TYPES = {
53 | "tweet": Tweet,
54 | "homeConversation": SelfThread,
55 | "profile": SelfThread
56 | }
57 | _RESULT_ATTR = "tweets"
58 |
59 | def __init__(self, community_id, client, pages=1, filter_=None, wait_time=2, cursor=None):
60 | super().__init__()
61 | self.tweets = []
62 | self.cursor = cursor
63 | self.cursor_top = cursor
64 | self.is_next_page = True
65 | self.filter = filter_
66 | self.client = client
67 | self.community_id = community_id
68 | self.pages = pages
69 | self.wait_time = wait_time
70 |
71 | def _get_target_object(self, tweet):
72 | entry_type = str(tweet['entryId']).split("-")[0]
73 | return self.OBJECTS_TYPES.get(entry_type)
74 |
75 | async def get_page(self, cursor):
76 | _tweets = []
77 | response = await self.client.http.get_community_tweets(self.community_id, self.filter, cursor=cursor)
78 |
79 | entries = self._get_entries(response)
80 |
81 | for entry in entries:
82 | object_type = self._get_target_object(entry)
83 |
84 | try:
85 | if object_type is None:
86 | continue
87 |
88 | parsed = object_type(self.client, entry, None)
89 | if parsed:
90 | _tweets.append(parsed)
91 | except:
92 | pass
93 |
94 | cursor = self._get_cursor_(response)
95 | cursor_top = self._get_cursor_(response, "Top")
96 |
97 | return _tweets, cursor, cursor_top
98 |
99 | def to_xlsx(self, filename=None):
100 | return Excel(self, filename)
101 |
102 | def __repr__(self):
103 | return "CommunityTweets(id={}, count={})".format(
104 | self.community_id, self.__len__()
105 | )
106 |
107 |
108 | class CommunityMembers(BaseGeneratorClass):
109 | _RESULT_ATTR = "users"
110 |
111 | def __init__(self, community_id, client, pages=1, filter_=None, wait_time=2, cursor=None):
112 | super().__init__()
113 | self.users = []
114 | self.cursor = cursor
115 | self.is_next_page = True
116 | self.filter = filter_
117 | self.client = client
118 | self.community_id = community_id
119 | self.pages = pages
120 | self.wait_time = wait_time
121 |
122 | @staticmethod
123 | def _get_users(response):
124 | all_users = find_objects(response, "__typename", "User", none_value=[])
125 | return all_users
126 |
127 | async def get_page(self, cursor):
128 | _users = []
129 | response = await self.client.http.get_community_members(self.community_id, self.filter, cursor=cursor)
130 |
131 | response_users = self._get_users(response)
132 |
133 | for response_user in response_users:
134 | try:
135 | parsed = User(self.client, response_user, None)
136 | if parsed:
137 | _users.append(parsed)
138 | except:
139 | pass
140 |
141 | cursor = find_objects(response, "next_cursor", value=None)
142 | cursor_top = self._get_cursor_(response, "Top")
143 |
144 | return _users, cursor, cursor_top
145 |
146 | def __repr__(self):
147 | return "CommunityMembers(id={}, count={})".format(
148 | self.community_id, self.__len__()
149 | )
150 |
--------------------------------------------------------------------------------
/src/tweety/types/follow.py:
--------------------------------------------------------------------------------
1 | from .twDataTypes import User
2 | from .base import BaseGeneratorClass
3 |
4 |
5 | class UserFollowers(BaseGeneratorClass):
6 | _RESULT_ATTR = "users"
7 |
8 | def __init__(self, user_id, client, pages=1, wait_time=2, cursor=None):
9 | super().__init__()
10 | self.users = []
11 | self.cursor = cursor
12 | self.cursor_top = cursor
13 | self.is_next_page = True
14 | self.client = client
15 | self.user_id = user_id
16 | self.pages = pages
17 | self.wait_time = wait_time
18 |
19 | async def get_page(self, cursor):
20 | _users = []
21 | response = await self.client.http.get_user_followers(self.user_id, cursor=cursor)
22 |
23 | entries = self._get_entries(response)
24 |
25 | for entry in entries:
26 | try:
27 |
28 | parsed = User(self.client, entry, None)
29 | if parsed:
30 | _users.append(parsed)
31 | except:
32 | pass
33 |
34 | cursor = self._get_cursor_(response)
35 | cursor_top = self._get_cursor_(response, "Top")
36 |
37 | return _users, cursor, cursor_top
38 |
39 |
40 | class UserFollowings(BaseGeneratorClass):
41 | _RESULT_ATTR = "users"
42 |
43 | def __init__(self, user_id, client, pages=1, wait_time=2, cursor=None):
44 | super().__init__()
45 | self.users = []
46 | self.cursor = cursor
47 | self.cursor_top = cursor
48 | self.is_next_page = True
49 | self.client = client
50 | self.user_id = user_id
51 | self.pages = pages
52 | self.wait_time = wait_time
53 |
54 | async def get_page(self, cursor):
55 | _users = []
56 | response = await self.client.http.get_user_followings(self.user_id, cursor=cursor)
57 |
58 | entries = self._get_entries(response)
59 |
60 | for entry in entries:
61 | try:
62 |
63 | parsed = User(self.client, entry, None)
64 | if parsed:
65 | _users.append(parsed)
66 | except:
67 | pass
68 |
69 | cursor = self._get_cursor_(response)
70 | cursor_top = self._get_cursor_(response, "Top")
71 |
72 | return _users, cursor, cursor_top
73 |
74 |
75 | class UserSubscribers(BaseGeneratorClass):
76 | _RESULT_ATTR = "users"
77 |
78 | def __init__(self, user_id, client, pages=1, wait_time=2, cursor=None):
79 | super().__init__()
80 | self.users = []
81 | self.cursor = cursor
82 | self.cursor_top = cursor
83 | self.is_next_page = True
84 | self.client = client
85 | self.user_id = user_id
86 | self.pages = pages
87 | self.wait_time = wait_time
88 |
89 | async def get_page(self, cursor):
90 | _users = []
91 | response = await self.client.http.get_user_subscribers(self.user_id, cursor=cursor)
92 |
93 | entries = self._get_entries(response)
94 |
95 | for entry in entries:
96 | try:
97 |
98 | parsed = User(self.client, entry, None)
99 | if parsed:
100 | _users.append(parsed)
101 | except:
102 | pass
103 |
104 | cursor = self._get_cursor_(response)
105 | cursor_top = self._get_cursor_(response, "Top")
106 |
107 | return _users, cursor, cursor_top
108 |
109 |
110 | class MutualFollowers(BaseGeneratorClass):
111 | _RESULT_ATTR = "users"
112 |
113 | def __init__(self, user_id, client, pages=1, wait_time=2, cursor=None):
114 | super().__init__()
115 | self.users = []
116 | self.cursor = cursor
117 | self.cursor_top = cursor
118 | self.is_next_page = True
119 | self.client = client
120 | self.user_id = user_id
121 | self.pages = pages
122 | self.wait_time = wait_time
123 |
124 | async def get_page(self, cursor):
125 | _users = []
126 | response = await self.client.http.get_mutual_friends(self.user_id, cursor=cursor)
127 |
128 | entries = self._get_entries(response)
129 |
130 | for entry in entries:
131 | try:
132 |
133 | parsed = User(self.client, entry, None)
134 | if parsed:
135 | _users.append(parsed)
136 | except:
137 | pass
138 |
139 | cursor = self._get_cursor_(response)
140 | cursor_top = self._get_cursor_(response, "Top")
141 |
142 | return _users, cursor, cursor_top
143 |
144 |
145 | class BlockedUsers(BaseGeneratorClass):
146 | _RESULT_ATTR = "users"
147 |
148 | def __init__(self, client, pages=1, wait_time=2, cursor=None):
149 | super().__init__()
150 | self.users = []
151 | self.cursor = cursor
152 | self.cursor_top = cursor
153 | self.is_next_page = True
154 | self.client = client
155 | self.pages = pages
156 | self.user_id = self.client.me.id
157 | self.wait_time = wait_time
158 |
159 | async def get_page(self, cursor):
160 | _users = []
161 | response = await self.client.http.get_blocked_users(cursor=cursor)
162 |
163 | entries = self._get_entries(response)
164 |
165 | for entry in entries:
166 | try:
167 |
168 | parsed = User(self.client, entry, None)
169 | if parsed:
170 | _users.append(parsed)
171 | except:
172 | pass
173 |
174 | cursor = self._get_cursor_(response)
175 | cursor_top = self._get_cursor_(response, "Top")
176 |
177 | return _users, cursor, cursor_top
178 |
--------------------------------------------------------------------------------
/src/tweety/types/gifs.py:
--------------------------------------------------------------------------------
1 | from .twDataTypes import Gif
2 | from .base import BaseGeneratorClass
3 |
4 |
5 | class GifSearch(BaseGeneratorClass):
6 | _RESULT_ATTR = "gifs"
7 |
8 | def __init__(self, search_term, client, pages=1, cursor=None, wait_time=2):
9 | super().__init__()
10 | self.term = search_term
11 | self.client = client
12 | self.pages = pages
13 | self.cursor = cursor
14 | self.wait_time = wait_time
15 | self.is_next_page = True
16 | self.gifs = []
17 |
18 | async def get_page(self, cursor):
19 | _gifs = []
20 | response = await self.client.http.gif_search(self.term, cursor)
21 |
22 | if not response.get('data'):
23 | return _gifs
24 |
25 | items = response.get('data', {}).get('items', [])
26 | for item in items:
27 | _gifs.append(Gif(self.client, item))
28 |
29 | cursor = response.get('cursor', {}).get('next')
30 | cursor_top = self._get_cursor_(response, "Top")
31 |
32 | return _gifs, cursor, cursor_top
33 |
34 | def __repr__(self):
35 | return f"GifSearch(count={self.__len__()})"
36 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/src/tweety/types/grok.py:
--------------------------------------------------------------------------------
1 | import datetime
2 | import json
3 |
4 | from .base import BaseGeneratorClass
5 | from . import GrokMessage
6 | from ..utils import find_objects
7 |
8 |
9 | class GrokConversation(BaseGeneratorClass):
10 | _RESULT_ATTR = "messages"
11 |
12 | def __init__(self, conversation_id, client, pages=1, wait_time=2, cursor=None):
13 | super().__init__()
14 | self.messages = []
15 | self.cursor = cursor
16 | self.cursor_top = cursor
17 | self.is_next_page = True
18 | self.client = client
19 | self.conversation_id = self.id = conversation_id
20 | self.pages = pages
21 | self.wait_time = wait_time
22 |
23 | async def get_page(self, cursor):
24 | this_items = []
25 | response = await self.client.http.get_grok_conversation_by_id(self.conversation_id, cursor)
26 |
27 | items = find_objects(response, "items", None, recursive=False)
28 | for item in items:
29 | this_items.append(GrokMessage(self.client, item))
30 |
31 | cursor = find_objects(response, "cursor", None, recursive=False, none_value=None)
32 |
33 | return this_items, cursor, None
34 |
35 | def __repr__(self):
36 | return "GrokConversation(id={}, messages={})".format(
37 | self.id, len(self.messages)
38 | )
39 |
40 | async def get_new_response(self, prompt_text):
41 | responses = []
42 | for i in self.messages:
43 | this_response = {
44 | "message": i.text,
45 | "sender": 2 if i.is_grok_response() else 1,
46 | }
47 | # if not i.is_grok_response():
48 | # this_response["fileAttachments"] = i.attachments
49 |
50 | responses.append(this_response)
51 |
52 | responses.append({
53 | "message": prompt_text,
54 | "sender": 1
55 | })
56 |
57 | response = await self.client.http.get_new_grok_response(self.id, responses)
58 |
59 | grok_message_object = {
60 | "grok_mode": "Normal",
61 | "sender_type": "Agent",
62 | "file_attachments": []
63 | }
64 |
65 | message = ""
66 | lines = [i for i in response.content.split(b"\n") if i]
67 | for line in lines:
68 | json_data = json.loads(line)
69 | if json_data.get("result", {}).get("message"):
70 | message += json_data.get("result", {}).get("message", "")
71 | elif json_data.get("userChatItemId"):
72 | grok_message_object["chat_item_id"] = json_data["userChatItemId"]
73 | elif json_data.get("result", {}).get("webResults"):
74 | grok_message_object["cited_web_results"] = json_data["result"]["cited_web_results"]
75 | elif json_data.get("result", {}).get("xPostIds"):
76 | grok_message_object["tweet_ids"] = json_data["result"]["xPostIds"]
77 | elif json_data.get("result", {}).get("imageAttachment"):
78 | image = json_data["result"]["imageAttachment"]
79 | grok_message_object["file_attachments"].append(image)
80 |
81 | grok_message_object["message"] = message
82 | grok_message_object["created_at_ms"] = datetime.datetime.now(datetime.UTC)
83 | grok_message_object_parsed = GrokMessage(self.client, grok_message_object)
84 | self.messages.append(grok_message_object_parsed)
85 | return grok_message_object_parsed
86 |
87 |
88 |
89 |
90 |
--------------------------------------------------------------------------------
/src/tweety/types/likes.py:
--------------------------------------------------------------------------------
1 | from .base import BaseGeneratorClass
2 | from .twDataTypes import User
3 |
4 |
5 | class TweetLikes(BaseGeneratorClass):
6 | _RESULT_ATTR = "users"
7 |
8 | def __init__(self, tweet_id, client, pages=1, wait_time=2, cursor=None):
9 | super().__init__()
10 | self.client = client
11 | self.users = []
12 | self.cursor = cursor
13 | self.cursor_top = cursor
14 | self.is_next_page = True
15 | self.tweet_id = tweet_id
16 | self.pages = pages
17 | self.wait_time = wait_time
18 |
19 | def __repr__(self):
20 | return "TweetLikes(tweet_id={}, count={})".format(
21 | self.tweet_id, len(self.users)
22 | )
23 |
24 | @staticmethod
25 | def _get_tweet_content_key(response):
26 | if str(response['entryId']).split("-")[0] == "user":
27 | return [response['content']['itemContent']['user_results']['result']]
28 |
29 | return []
30 |
31 | async def get_page(self, cursor):
32 | _users = []
33 | response = await self.client.http.get_tweet_likes(tweet_id=self.tweet_id, cursor=cursor)
34 |
35 | entries = self._get_entries(response)
36 |
37 | for entry in entries:
38 | try:
39 | parsed = User(self.client, entry)
40 | if parsed:
41 | _users.append(parsed)
42 | except:
43 | pass
44 |
45 | cursor = self._get_cursor_(response)
46 | cursor_top = self._get_cursor_(response, "Top")
47 |
48 | return _users, cursor, cursor_top
49 |
--------------------------------------------------------------------------------
/src/tweety/types/lists.py:
--------------------------------------------------------------------------------
1 | from .twDataTypes import User, List, Tweet, SelfThread
2 | from .base import BaseGeneratorClass
3 | from ..utils import find_objects
4 |
5 |
6 | class Lists(BaseGeneratorClass):
7 | _RESULT_ATTR = "lists"
8 |
9 | def __init__(self, user_id, client, pages=1, wait_time=2, cursor=None):
10 | super().__init__()
11 | self.lists = []
12 | self.cursor = cursor
13 | self.cursor_top = cursor
14 | self.is_next_page = True
15 | self.client = client
16 | self.user_id = user_id
17 | self.pages = pages
18 | self.wait_time = wait_time
19 |
20 | @staticmethod
21 | def _get_user_owned_lists(entries):
22 | for entry in entries:
23 | entry_type = str(entry['entryId']).split("-")[0]
24 | if entry_type == "owned":
25 | return entry
26 |
27 | return {}
28 |
29 | async def get_page(self, cursor):
30 | _lists = []
31 | response = await self.client.http.get_lists(cursor=cursor)
32 | entries = self._get_entries(response)
33 | item = self._get_user_owned_lists(entries)
34 | lists = find_objects(item, "__typename", "TimelineTwitterList", none_value=[])
35 |
36 | for item in lists:
37 | try:
38 | parsed = List(self.client, item)
39 | if parsed:
40 | _lists.append(parsed)
41 | except:
42 | pass
43 |
44 | cursor = self._get_cursor_(response)
45 | cursor_top = self._get_cursor_(response, "Top")
46 |
47 | return _lists, cursor, cursor_top
48 |
49 |
50 | class ListTweets(BaseGeneratorClass):
51 | OBJECTS_TYPES = {
52 | "tweet": Tweet,
53 | "homeConversation": SelfThread,
54 | "profile": SelfThread,
55 | "list": SelfThread,
56 | }
57 | _RESULT_ATTR = "tweets"
58 |
59 | def __init__(self, list_id, client, pages=1, wait_time=2, cursor=None):
60 | super().__init__()
61 | self.tweets = []
62 | self.cursor = cursor
63 | self.cursor_top = cursor
64 | self.is_next_page = True
65 | self.client = client
66 | self.list_id = list_id
67 | self.pages = pages
68 | self.wait_time = wait_time
69 |
70 | def _get_target_object(self, tweet):
71 | entry_type = str(tweet['entryId']).split("-")[0]
72 | return self.OBJECTS_TYPES.get(entry_type)
73 |
74 | async def get_page(self, cursor):
75 | _tweets = []
76 | response = await self.client.http.get_list_tweets(self.list_id, cursor=cursor)
77 | entries = self._get_entries(response)
78 |
79 | for entry in entries:
80 | object_type = self._get_target_object(entry)
81 |
82 | try:
83 | if object_type is None:
84 | continue
85 |
86 | parsed = object_type(self.client, entry, None)
87 | _tweets.append(parsed)
88 | except:
89 | pass
90 |
91 | cursor = self._get_cursor_(response)
92 | cursor_top = self._get_cursor_(response, "Top")
93 |
94 | return _tweets, cursor, cursor_top
95 |
96 | def __repr__(self):
97 | return "ListTweets(id={}, count={})".format(
98 | self.list_id, self.__len__()
99 | )
100 |
101 |
102 | class ListMembers(BaseGeneratorClass):
103 | _RESULT_ATTR = "users"
104 |
105 | def __init__(self, list_id, client, pages=1, wait_time=2, cursor=None):
106 | super().__init__()
107 | self.users = []
108 | self.cursor = cursor
109 | self.cursor_top = cursor
110 | self.is_next_page = True
111 | self.client = client
112 | self.list_id = list_id
113 | self.pages = pages
114 | self.wait_time = wait_time
115 |
116 | async def get_page(self, cursor):
117 | _users = []
118 | response = await self.client.http.get_list_members(self.list_id, cursor=cursor)
119 |
120 | entries = self._get_entries(response)
121 |
122 | for entry in entries:
123 | try:
124 | parsed = User(self.client, entry, None)
125 | if parsed:
126 | _users.append(parsed)
127 | except:
128 | pass
129 |
130 | cursor = self._get_cursor_(response)
131 | cursor_top = self._get_cursor_(response, "Top")
132 |
133 | return _users, cursor, cursor_top
134 |
135 | def __repr__(self):
136 | return "ListMembers(id={}, count={})".format(
137 | self.list_id, self.__len__()
138 | )
139 |
140 | class ListFollowers(BaseGeneratorClass):
141 | _RESULT_ATTR = "users"
142 |
143 | def __init__(self, list_id, client, pages=1, wait_time=2, cursor=None):
144 | super().__init__()
145 | self.users = []
146 | self.cursor = cursor
147 | self.cursor_top = cursor
148 | self.is_next_page = True
149 | self.client = client
150 | self.list_id = list_id
151 | self.pages = pages
152 | self.wait_time = wait_time
153 |
154 | @staticmethod
155 | def _get_users(response):
156 | all_users = find_objects(response, "__typename", "User", none_value=[])
157 | return [all_users] if not isinstance(all_users, list) else all_users
158 |
159 | async def get_page(self, cursor):
160 | _users = []
161 | response = await self.client.http.get_list_followers(self.list_id, cursor=cursor)
162 |
163 | response_users = self._get_users(response)
164 |
165 | for response_user in response_users:
166 | try:
167 | parsed = User(self.client, response_user, None)
168 | if parsed:
169 | _users.append(parsed)
170 | except:
171 | pass
172 |
173 | cursor = self._get_cursor_(response)
174 | cursor_top = self._get_cursor_(response, "Top")
175 |
176 | return _users, cursor, cursor_top
177 |
178 | def __repr__(self):
179 | return "ListFollowers(id={}, count={})".format(
180 | self.list_id, self.__len__()
181 | )
182 |
--------------------------------------------------------------------------------
/src/tweety/types/mentions.py:
--------------------------------------------------------------------------------
1 | from .twDataTypes import Tweet
2 | from .base import BaseGeneratorClass
3 |
4 |
5 | class Mention(BaseGeneratorClass):
6 | _RESULT_ATTR = "tweets"
7 |
8 | def __init__(self, user_id, client, pages=1, wait_time=2, cursor=None):
9 | super().__init__()
10 | self.tweets = []
11 | self.cursor = cursor
12 | self.cursor_top = cursor
13 | self.is_next_page = True
14 | self.client = client
15 | self.user_id = user_id
16 | self.pages = pages
17 | self.wait_time = wait_time
18 |
19 | async def get_page(self, cursor):
20 | _tweets = []
21 | response = await self.client.http.get_mentions(self.user_id, cursor=cursor)
22 |
23 | users = response.get('globalObjects', {}).get('users', {})
24 | tweets = response.get('globalObjects', {}).get('tweets', {})
25 |
26 | for tweet_id, tweet in tweets.items():
27 | user = users.get(str(tweet['user_id']))
28 | user['__typename'] = "User"
29 | tweet['author'], tweet['rest_id'], tweet['__typename'] = user, tweet_id, "Tweet"
30 |
31 | try:
32 | parsed = Tweet(self.client, tweet, response)
33 | if parsed:
34 | _tweets.append(parsed)
35 | except:
36 | pass
37 |
38 | cursor = self._get_cursor_(response)
39 | cursor_top = self._get_cursor_(response, "Top")
40 |
41 | return _tweets, cursor, cursor_top
42 |
43 | def __repr__(self):
44 | return f"Mentions(user_id={self.user_id}, count={self.__len__()})"
45 |
46 |
--------------------------------------------------------------------------------
/src/tweety/types/n_types.py:
--------------------------------------------------------------------------------
1 | import datetime
2 | import os
3 | import time
4 | from http.cookiejar import MozillaCookieJar
5 | from httpx._content import encode_multipart_data
6 | from io import BytesIO
7 | from .. import constants
8 | from . import Gif
9 | from ..utils import calculate_md5, get_random_string, check_if_file_is_supported
10 | from ..exceptions import *
11 |
12 |
13 | class Proxy:
14 | PROTOCOLS = {
15 | constants.PROXY_TYPE_HTTP: "http://",
16 | constants.PROXY_TYPE_SOCKS4: "socks4://",
17 | constants.PROXY_TYPE_SOCKS5: "socks5://",
18 | }
19 |
20 | def __init__(self, host: str, port: int, proxy_type: int, username: str = None, password: str = None):
21 | self.host = host
22 | self.password = password
23 | self.proxy_type = proxy_type
24 | self.username = username
25 | self.port = port
26 |
27 | def __proxy_url__(self):
28 | if self.proxy_type not in list(self.PROTOCOLS.keys()):
29 | raise ProxyParseError()
30 |
31 | if self.username and self.password:
32 | this_url = "{}:{}@{}:{}".format(
33 | self.username, self.password, self.host, self.port
34 | )
35 | else:
36 | this_url = "{}:{}".format(self.host, self.port)
37 |
38 | return "{}{}".format(self.PROTOCOLS[self.proxy_type], this_url)
39 |
40 | def __str__(self):
41 | return self.__proxy_url__()
42 |
43 |
44 |
45 | class GenericError:
46 | EXCEPTIONS = {
47 | 32: InvalidCredentials,
48 | # 37: SuspendedAccount,
49 | 64: SuspendedAccount,
50 | 88: RateLimitReached,
51 | 141: SuspendedAccount,
52 | 144: InvalidTweetIdentifier,
53 | 214: InvalidBroadcast,
54 | 220: InvalidCredentials,
55 | 326: LockedAccount,
56 | 366: InvalidTweetIdentifier,
57 | 399: InvalidCredentials,
58 | 477: RateLimitReached
59 | }
60 |
61 | def __init__(self, response, error_code, message=None):
62 | self.response = response
63 | self.error_code = error_code
64 | self.message = message
65 | self.retry_after = self._get_retry_after()
66 | self._raise_exception()
67 |
68 | def _get_retry_after(self):
69 | if all(key in self.response.headers for key in ['x-rate-limit-reset', 'x-rate-limit-remaining']):
70 | epochLimitTime = int(self.response.headers['x-rate-limit-reset'])
71 | epochCurrentTime = int(datetime.datetime.now().timestamp())
72 | return epochLimitTime - epochCurrentTime
73 |
74 | return 0
75 |
76 | def _raise_exception(self):
77 | if self.EXCEPTIONS.get(self.error_code):
78 | raise self.EXCEPTIONS[self.error_code](
79 | error_code=self.error_code,
80 | error_name=TWITTER_ERRORS[self.error_code],
81 | response=self.response,
82 | message=self.message,
83 | retry_after=self.retry_after
84 | )
85 |
86 | raise TwitterError(
87 | error_code=self.error_code,
88 | error_name=TWITTER_ERRORS.get(self.error_code, 0),
89 | response=self.response,
90 | message="[{}] {}".format(self.error_code, self.message)
91 | )
92 |
93 |
94 | class Cookies:
95 | def __init__(self, cookies):
96 | self._raw_cookies = cookies
97 | self.parse_cookies()
98 |
99 | def parse_cookies(self):
100 | if isinstance(self._raw_cookies, MozillaCookieJar):
101 | for i in self._raw_cookies:
102 | setattr(self, i.name, i.value)
103 | else:
104 | true_cookies = dict()
105 | if isinstance(self._raw_cookies, str):
106 | cookie_list = self._raw_cookies.split(";")
107 | for cookie in cookie_list:
108 | split_cookie = cookie.strip().split("=", 1)
109 |
110 | if len(split_cookie) >= 2:
111 | cookie_key = split_cookie[0]
112 | cookie_value = split_cookie[1]
113 | true_cookies[cookie_key] = cookie_value
114 | elif isinstance(self._raw_cookies, dict):
115 | true_cookies = self._raw_cookies
116 | else:
117 | raise TypeError("cookies should be of class 'str', 'dict' or 'MozillaCookieJar' not {}".format(self._raw_cookies.__class__))
118 |
119 | for key, value in true_cookies.items():
120 | setattr(self, key.strip(), value.strip())
121 |
122 | def to_dict(self):
123 | result = {}
124 | for k, v in vars(self).items():
125 |
126 | if not k.startswith("_"):
127 | result[k] = v
128 |
129 | return result
130 |
131 | def __str__(self):
132 | string = ""
133 | for k, v in vars(self).items():
134 |
135 | if not k.startswith("_"):
136 | string += f"{k}={v};"
137 |
138 | return string
139 |
140 |
141 | class UploadedMedia:
142 | FILE_CHUNK_SIZE = 2 * 1024 * 1024 # 2 mb
143 |
144 | def __init__(
145 | self,
146 | file_path,
147 | client,
148 | alt_text=None,
149 | sensitive_media_warning=None,
150 | media_category=constants.UPLOAD_TYPE_TWEET_IMAGE
151 | ):
152 | self.media_id = None
153 | self._file = file_path
154 | self._client = client
155 | self._alt_text = alt_text
156 | self._sensitive_media_warning = sensitive_media_warning if sensitive_media_warning else []
157 | self._source_url = self._get_source_url()
158 | self.size = self._get_size()
159 | self.mime_type = self.get_mime_type()
160 | self._media_category = self._get_media_category(media_category)
161 | self.md5_hash = calculate_md5(self._file)
162 |
163 | def _get_source_url(self):
164 | if isinstance(self._file, Gif):
165 | return self._file.url
166 | elif str(self._file).startswith("https://"):
167 | return self._file
168 |
169 | return None
170 |
171 | def _get_media_category(self, category):
172 | media_for = category.split("_")[0]
173 | media_type = self.mime_type.split("/")[0]
174 | return f"{media_for}_{media_type}" if "gif" not in self.mime_type else f"{media_for}_gif"
175 |
176 | def _get_size(self):
177 | if isinstance(self._file, str):
178 | if not self._source_url:
179 | return os.path.getsize(self._file)
180 | else:
181 | self._file = self._source_url
182 | return 0
183 | if not self._source_url and isinstance(self._file, str):
184 | return os.path.getsize(self._file)
185 | elif isinstance(self._file, bytes):
186 | return len(self._file)
187 | elif isinstance(self._file, BytesIO):
188 | self._file = self._file.getvalue()
189 | return len(self._file)
190 | elif isinstance(self._file, Gif):
191 | self._file = self._source_url
192 | return 0
193 |
194 | def get_mime_type(self):
195 | return check_if_file_is_supported(self._file)
196 |
197 | @staticmethod
198 | def _create_boundary():
199 | return bytes(f'----WebKitFormBoundary{get_random_string(16)}', "utf-8")
200 |
201 | async def _initiate_upload(self):
202 | response = await self._client.http.upload_media_init(self.size, self.mime_type, self._media_category, source_url=self._source_url)
203 | media_id = response.get('media_id_string')
204 |
205 | if not media_id:
206 | error = response["error"] if response.get("error") else response
207 | raise ValueError(f"Unable to Initiate the Media Upload: {error}")
208 |
209 | return media_id
210 |
211 | @staticmethod
212 | def get_multipart_headers(multipart) -> dict[str, str]:
213 | content_length = multipart.get_content_length()
214 | content_type = multipart.content_type
215 | if content_length is None:
216 | return {"transfer-encoding": "chunked", "content-type": content_type}
217 | return {"content-length": str(content_length), "content-type": content_type}
218 |
219 | async def _append_upload(self, media_id):
220 | segments, remainder = divmod(self.size, self.FILE_CHUNK_SIZE)
221 | segments += bool(remainder)
222 |
223 | if isinstance(self._file, bytes):
224 | data_bytes = self._file
225 | else:
226 | with open(self._file, "rb") as f:
227 | data_bytes = f.read()
228 |
229 | for segment_index in range(segments):
230 | start = segment_index * self.FILE_CHUNK_SIZE
231 | end = start + self.FILE_CHUNK_SIZE
232 | this_chunk = data_bytes[start:end]
233 | boundary = self._create_boundary()
234 | _, multipart = encode_multipart_data({}, {"media": ('blob', this_chunk, "application/octet-stream")}, boundary)
235 | headers = self.get_multipart_headers(multipart)
236 | headers.update({"x-media-type": self.mime_type})
237 | await self._client.http.upload_media_append(media_id, b"".join([i for i in multipart.iter_chunks()]), headers, segment_index)
238 |
239 | async def set_metadata(self):
240 | await self._client.http.set_media_set_metadata(self.media_id, self._alt_text, self._sensitive_media_warning)
241 |
242 | async def _finish_upload(self, media_id):
243 | if not self._source_url:
244 | response = await self._client.http.upload_media_finalize(media_id, self.md5_hash)
245 | else:
246 | response = {"processing_info": {"state": "pending", "check_after_secs": 1}}
247 |
248 | if response.get("error"):
249 | raise UploadFailed(
250 | message=response.get("error", "Unknown Error Occurred while uploading File"),
251 | response=response
252 | )
253 |
254 | if not response.get('processing_info'):
255 | return
256 |
257 | while True:
258 | processing_info = response['processing_info']
259 |
260 | if processing_info.get('state') in ('pending', 'in_progress') and 'error' not in processing_info:
261 | time.sleep(processing_info['check_after_secs'])
262 | response = await self._client.http.upload_media_status(self.media_id)
263 | elif processing_info.get("error"):
264 | error = processing_info["error"]
265 | code, name, message = error.get("code", 1), error.get("name", ""), error.get("message", "")
266 | raise TwitterError(
267 | error_code=code,
268 | error_name=name,
269 | response=response,
270 | message=message
271 | )
272 | else:
273 | return
274 |
275 | async def upload(self):
276 | if self.size == 0 and self._source_url is None:
277 | raise UploadFailed(message="Looks like the file is not valid")
278 |
279 | self.media_id = await self._initiate_upload()
280 |
281 | if not self._source_url:
282 | await self._append_upload(self.media_id)
283 |
284 | await self._finish_upload(self.media_id)
285 |
286 | if self._alt_text:
287 | await self.set_metadata()
288 |
289 | return self
290 |
291 | def __repr__(self):
292 | return "UploadedMedia(media_id={}, uploaded={}, mime_type={}, size={})".format(
293 | self.media_id, True if self.media_id else False, self.mime_type, self.size
294 | )
295 |
296 |
297 |
298 |
299 |
300 |
--------------------------------------------------------------------------------
/src/tweety/types/notification.py:
--------------------------------------------------------------------------------
1 | from .base import BaseGeneratorClass
2 | from .twDataTypes import Tweet
3 |
4 |
5 | class TweetNotifications(BaseGeneratorClass):
6 | _RESULT_ATTR = "tweets"
7 |
8 | def __init__(self, user_id, client, pages=1, wait_time=2, cursor=None):
9 | super().__init__()
10 | self.tweets = []
11 | self.cursor = cursor
12 | self.cursor_top = cursor
13 | self.is_next_page = True
14 | self.client = client
15 | self.user_id = user_id
16 | self.pages = pages
17 | self.wait_time = wait_time
18 |
19 | async def get_page(self, cursor):
20 | _tweets = []
21 |
22 | response = await self.client.http.get_tweet_notifications(cursor=cursor)
23 | users = response.get('globalObjects', {}).get('users', {})
24 | tweets = response.get('globalObjects', {}).get('tweets', {})
25 |
26 | for tweet_id, tweet in tweets.items():
27 | user = users.get(str(tweet['user_id']))
28 | user['__typename'] = "User"
29 | tweet['author'], tweet['rest_id'], tweet['__typename'] = user, tweet_id, "Tweet"
30 |
31 | try:
32 | parsed = Tweet(self.client, tweet, response)
33 | if parsed:
34 | _tweets.append(parsed)
35 | except:
36 | pass
37 |
38 | cursor = self._get_cursor_(response)
39 | cursor_top = self._get_cursor_(response, "Top")
40 |
41 | return _tweets, cursor, cursor_top
42 |
--------------------------------------------------------------------------------
/src/tweety/types/places.py:
--------------------------------------------------------------------------------
1 | from .twDataTypes import Place
2 |
3 |
4 | class Places(dict):
5 | def __init__(self, client, lat, long, search_term):
6 | super().__init__()
7 | self.lat = lat
8 | self.client = client
9 | self.long = long
10 | self.search_term = search_term
11 | self.results = []
12 |
13 | async def get_page(self):
14 | if all(value is None for value in [self.lat, self.long, self.search_term]):
15 | raise ValueError("Either 'lat' and 'long' OR 'search_term' is Required")
16 |
17 | _results = []
18 | response = await self.client.http.search_place(self.lat, self.long, self.search_term)
19 |
20 | for place in response.get('places', []):
21 | _results.append(Place(self.client, place))
22 |
23 | self.results = _results
24 |
25 | def __repr__(self):
26 | return "Places(results={})".format(len(self.results))
27 |
28 | def __getitem__(self, index):
29 | if isinstance(index, str):
30 | return getattr(self, index)
31 |
32 | return self.results[index]
33 |
34 | def __iter__(self):
35 | for i in self.results:
36 | yield i
37 |
38 | def __len__(self):
39 | return len(self.results)
40 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/src/tweety/types/retweets.py:
--------------------------------------------------------------------------------
1 | from .base import BaseGeneratorClass
2 | from .twDataTypes import User
3 |
4 |
5 | class TweetRetweets(BaseGeneratorClass):
6 | _RESULT_ATTR = "users"
7 |
8 | def __init__(self, tweet_id, client, pages=1, wait_time=2, cursor=None):
9 | super().__init__()
10 | self.users = []
11 | self.cursor = cursor
12 | self.cursor_top = cursor
13 | self.is_next_page = True
14 | self.client = client
15 | self.tweet_id = tweet_id
16 | self.pages = pages
17 | self.wait_time = wait_time
18 |
19 | def __repr__(self):
20 | return "TweetRetweets(tweet_id={}, count={})".format(
21 | self.tweet_id,
22 | len(self.users)
23 | )
24 |
25 | async def get_page(self, cursor):
26 | _users = []
27 | response = await self.client.http.get_tweet_retweets(tweet_id=self.tweet_id, cursor=cursor)
28 |
29 | entries = self._get_entries(response)
30 |
31 | for entry in entries:
32 | try:
33 | parsed = User(self.client, entry)
34 | if parsed:
35 | _users.append(parsed)
36 | except:
37 | pass
38 |
39 | cursor = self._get_cursor_(response)
40 | cursor_top = self._get_cursor_(response, "Top")
41 |
42 | return _users, cursor, cursor_top
43 |
--------------------------------------------------------------------------------
/src/tweety/types/search.py:
--------------------------------------------------------------------------------
1 | import traceback
2 |
3 | from . import Tweet, Excel, User, List
4 | from .base import BaseGeneratorClass
5 | from .twDataTypes import SelfThread
6 | from ..utils import find_objects
7 | from ..filters import SearchFilters
8 |
9 |
10 | class Search(BaseGeneratorClass):
11 | OBJECTS_TYPES = {
12 | "tweet": Tweet,
13 | "search": Tweet,
14 | "homeConversation": SelfThread,
15 | "profile": SelfThread,
16 | "user": User,
17 | "list": List
18 | }
19 | _RESULT_ATTR = "results"
20 |
21 | def __init__(self, keyword, client, pages=1, filter_=None, wait_time=2, cursor=None):
22 | super().__init__()
23 | self.results = []
24 | self.keyword = keyword
25 | self.cursor = cursor
26 | self.cursor_top = cursor
27 | self.is_next_page = True
28 | self.client = client
29 | self.pages = pages
30 | self.wait_time = wait_time
31 | self.filter = filter_.strip() if filter_ else None
32 |
33 | def __repr__(self):
34 | return "Search(keyword={}, count={}, filter={})".format(
35 | self.keyword, len(self.results), self.filter
36 | )
37 |
38 | async def get_page(self, cursor):
39 | thisObjects = []
40 | response = await self.client.http.perform_search(self.keyword, cursor, self.filter)
41 | entries = self._get_entries(response)
42 |
43 | if self.filter == SearchFilters.Lists:
44 | entries = self._get_list_entries(entries)
45 | elif self.filter == SearchFilters.Media:
46 | entries = self._get_grid_entries(entries)
47 |
48 | for entry in entries:
49 | object_type = self._get_target_object(entry)
50 | try:
51 | if object_type is None:
52 | continue
53 | parsed = object_type(self.client, entry, None)
54 | if parsed:
55 | thisObjects.append(parsed)
56 | except:
57 | pass
58 | cursor = self._get_cursor_(response)
59 | cursor_top = self._get_cursor_(response, "Top")
60 |
61 | return thisObjects, cursor, cursor_top
62 |
63 | def _get_target_object(self, obj):
64 | entry_type = str(obj['entryId']).split("-")[0]
65 | return self.OBJECTS_TYPES.get(entry_type)
66 |
67 | @staticmethod
68 | def _get_grid_entries(entries):
69 | results = []
70 | for entry in entries:
71 | obj = find_objects(entry, "displayType", "VerticalGrid", none_value={}, recursive=False)
72 | if obj:
73 | results.extend(obj.get("items", []))
74 | return results
75 |
76 | @staticmethod
77 | def _get_list_entries(entries):
78 | results = []
79 | for entry in entries:
80 | if str(entry['entryId']).split("-")[0] == "list":
81 | for item in entry['content']['items']:
82 | results.append(item)
83 | return results
84 |
85 | def to_xlsx(self, filename=None):
86 | if self.filter == "users":
87 | return AttributeError("to_xlsx with 'users' filter isn't supported yet")
88 |
89 | return Excel(self.results, f"search-{self.keyword}", filename)
90 |
91 |
92 | class TypeHeadSearch(dict):
93 | DATA_TYPES = {
94 | "users": User
95 | }
96 |
97 | def __init__(self, client, keyword, result_type='events,users,topics,lists'):
98 | super().__init__()
99 | self.client = client
100 | self.keyword = keyword
101 | self.result_type = result_type
102 | self.results = []
103 |
104 | async def get_results(self):
105 | response = await self.client.http.search_typehead(self.keyword, self.result_type)
106 | for _type_name, _type_object in self.DATA_TYPES.items():
107 | for result in response.get(_type_name, []):
108 | try:
109 | if _type_name == "users":
110 | result['__typename'] = "User"
111 |
112 | parsed = _type_object(self.client, result)
113 | self.results.append(parsed)
114 | except:
115 | pass
116 | self['results'] = self.results
117 | return self.results
118 |
119 | def __getitem__(self, index):
120 | if isinstance(index, str):
121 | return getattr(self, index)
122 |
123 | return self.results[index]
124 |
125 | def __iter__(self):
126 | for i in self.results:
127 | yield i
128 |
129 | def __len__(self):
130 | return len(self.results)
131 |
132 | def __repr__(self):
133 | return "TypeHeadSearch(keyword={})".format(self.keyword)
134 |
135 |
--------------------------------------------------------------------------------
/src/tweety/types/topic.py:
--------------------------------------------------------------------------------
1 | from .base import BaseGeneratorClass
2 | from .twDataTypes import Topic, Tweet
3 |
4 |
5 | class TopicTweets(BaseGeneratorClass):
6 | _RESULT_ATTR = "tweets"
7 |
8 | def __init__(self, topic_id, client, pages=1, cursor=None, wait_time=2):
9 | super().__init__()
10 | self.topic_id = topic_id
11 | self.client = client
12 | self.pages = pages
13 | self.cursor = cursor
14 | self.wait_time = wait_time
15 | self.is_next_page = True
16 | self.topic = None
17 | self.tweets = []
18 |
19 | async def get_page(self, cursor):
20 | _tweets = []
21 | response = await self.client.http.get_topic_landing_page(self.topic_id, cursor)
22 |
23 | if not self.topic:
24 | self.topic = Topic(self.client, response)
25 |
26 | entries = self._get_entries(response)
27 |
28 | for entry in entries:
29 | try:
30 | parsed = Tweet(self.client, entry, None)
31 | if parsed:
32 | _tweets.append(parsed)
33 | except:
34 | pass
35 |
36 | cursor = self._get_cursor_(response)
37 | cursor_top = self._get_cursor_(response, "Top")
38 | return _tweets, cursor, cursor_top
39 |
40 | def __repr__(self):
41 | return "TopicTweets(topic={}, tweets={})".format(self.topic, len(self.tweets))
42 |
43 |
44 |
45 |
--------------------------------------------------------------------------------
/src/tweety/types/usertweet.py:
--------------------------------------------------------------------------------
1 | import traceback
2 |
3 | from .twDataTypes import SelfThread, ConversationThread, Tweet, Excel, ScheduledTweet
4 | from ..exceptions import UserProtected, UserNotFound
5 | from .base import BaseGeneratorClass, find_objects
6 | from ..filters import TweetCommentFilters
7 |
8 |
9 | class UserTweets(BaseGeneratorClass):
10 | OBJECTS_TYPES = {
11 | "tweet": Tweet,
12 | "homeConversation": SelfThread,
13 | "profile": SelfThread
14 | }
15 | _RESULT_ATTR = "tweets"
16 |
17 | def __init__(self, user_id, client, pages=1, get_replies: bool = True, wait_time=2, cursor=None):
18 | super().__init__()
19 | self.tweets = []
20 | self.get_replies = get_replies
21 | self.cursor = cursor
22 | self.cursor_top = cursor
23 | self.is_next_page = True
24 | self.client = client
25 | self.user_id = user_id
26 | self.pages = pages
27 | self.wait_time = wait_time
28 | self.pinned = None
29 |
30 | def _get_target_object(self, tweet):
31 | entry_type = str(tweet['entryId']).split("-")[0]
32 | return self.OBJECTS_TYPES.get(entry_type)
33 |
34 | def _get_pinned_tweet(self, response):
35 | pinned = find_objects(response, "type", "TimelinePinEntry", recursive=False, none_value={})
36 | pinned_tweet = Tweet(self.client, pinned, None)
37 | return pinned_tweet
38 |
39 | async def get_page(self, cursor):
40 | _tweets = []
41 |
42 | response = await self.client.http.get_tweets(self.user_id, replies=self.get_replies, cursor=cursor)
43 |
44 | if not response['data']['user'].get("result"):
45 | raise UserNotFound(response=response)
46 |
47 | if response['data']['user']['result']['__typename'] == "UserUnavailable":
48 | raise UserProtected(403, "UserUnavailable", response)
49 |
50 | entries = self._get_entries(response)
51 |
52 | if not self.pinned:
53 | self.pinned = self._get_pinned_tweet(response)
54 |
55 | for entry in entries:
56 | object_type = self._get_target_object(entry)
57 |
58 | try:
59 | if object_type is None:
60 | continue
61 |
62 | parsed = object_type(self.client, entry, None)
63 | if parsed:
64 | _tweets.append(parsed)
65 | except:
66 | pass
67 |
68 | cursor = self._get_cursor_(response)
69 | cursor_top = self._get_cursor_(response, "Top")
70 |
71 | return _tweets, cursor, cursor_top
72 |
73 | def to_xlsx(self, filename=None):
74 | return Excel(self, filename)
75 |
76 |
77 | class UserHighlights(BaseGeneratorClass):
78 | OBJECTS_TYPES = {
79 | "tweet": Tweet,
80 | "homeConversation": SelfThread,
81 | "profile": SelfThread
82 | }
83 | _RESULT_ATTR = "tweets"
84 |
85 | def __init__(self, user_id, client, pages=1, get_replies: bool = True, wait_time=2, cursor=None):
86 | super().__init__()
87 | self.tweets = []
88 | self.get_replies = get_replies
89 | self.cursor = cursor
90 | self.cursor_top = cursor
91 | self.is_next_page = True
92 | self.client = client
93 | self.user_id = user_id
94 | self.pages = pages
95 | self.wait_time = wait_time
96 | self.pinned = None
97 |
98 | def _get_target_object(self, tweet):
99 | entry_type = str(tweet['entryId']).split("-")[0]
100 | return self.OBJECTS_TYPES.get(entry_type)
101 |
102 | async def get_page(self, cursor):
103 | _tweets = []
104 |
105 | response = await self.client.http.get_highlights(self.user_id, cursor=cursor)
106 |
107 | if not response['data']['user'].get("result"):
108 | raise UserNotFound(response=response)
109 |
110 | if response['data']['user']['result']['__typename'] == "UserUnavailable":
111 | raise UserProtected(403, "UserUnavailable", response)
112 |
113 | entries = self._get_entries(response)
114 |
115 | for entry in entries:
116 | object_type = self._get_target_object(entry)
117 |
118 | try:
119 | if object_type is None:
120 | continue
121 |
122 | parsed = object_type(self.client, entry, None)
123 | if parsed:
124 | _tweets.append(parsed)
125 | except:
126 | pass
127 |
128 | cursor = self._get_cursor_(response)
129 | cursor_top = self._get_cursor_(response, "Top")
130 |
131 | return _tweets, cursor, cursor_top
132 |
133 | def to_xlsx(self, filename=None):
134 | return Excel(self, filename)
135 |
136 |
137 | class UserLikes(BaseGeneratorClass):
138 | OBJECTS_TYPES = {
139 | "tweet": Tweet,
140 | "homeConversation": SelfThread,
141 | "profile": SelfThread
142 | }
143 | _RESULT_ATTR = "tweets"
144 |
145 | def __init__(self, user_id, client, pages=1, get_replies: bool = True, wait_time=2, cursor=None):
146 | super().__init__()
147 | self.tweets = []
148 | self.get_replies = get_replies
149 | self.cursor = cursor
150 | self.cursor_top = cursor
151 | self.is_next_page = True
152 | self.client = client
153 | self.user_id = user_id
154 | self.pages = pages
155 | self.wait_time = wait_time
156 | self.pinned = None
157 |
158 | def _get_target_object(self, tweet):
159 | entry_type = str(tweet['entryId']).split("-")[0]
160 | return self.OBJECTS_TYPES.get(entry_type)
161 |
162 | async def get_page(self, cursor):
163 | _tweets = []
164 |
165 | response = await self.client.http.get_likes(self.user_id, cursor=cursor)
166 |
167 | if not response['data']['user'].get("result"):
168 | raise UserNotFound(response=response)
169 |
170 | if response['data']['user']['result']['__typename'] == "UserUnavailable":
171 | raise UserProtected(403, "UserUnavailable", response)
172 |
173 | entries = self._get_entries(response)
174 |
175 | for entry in entries:
176 | object_type = self._get_target_object(entry)
177 |
178 | try:
179 | if object_type is None:
180 | continue
181 |
182 | parsed = object_type(self.client, entry, None)
183 | if parsed:
184 | _tweets.append(parsed)
185 | except:
186 | pass
187 |
188 | cursor = self._get_cursor_(response)
189 | cursor_top = self._get_cursor_(response, "Top")
190 |
191 | return _tweets, cursor, cursor_top
192 |
193 | def to_xlsx(self, filename=None):
194 | return Excel(self, filename)
195 |
196 |
197 | class UserMedia(BaseGeneratorClass):
198 | OBJECTS_TYPES = {
199 | "tweet": Tweet,
200 | "homeConversation": SelfThread,
201 | "profile": Tweet
202 | }
203 | _RESULT_ATTR = "tweets"
204 |
205 | def __init__(self, user_id, client, pages=1, wait_time=2, cursor=None):
206 | super().__init__()
207 | self.tweets = []
208 | self.cursor = cursor
209 | self.cursor_top = cursor
210 | self.is_next_page = True
211 | self.client = client
212 | self.user_id = user_id
213 | self.pages = pages
214 | self.wait_time = wait_time
215 |
216 | @staticmethod
217 | def _result_attr():
218 | return "tweets"
219 |
220 | def _get_target_object(self, tweet):
221 | entry_type = str(tweet['entryId']).split("-")[0]
222 | return self.OBJECTS_TYPES.get(entry_type)
223 |
224 | async def get_page(self, cursor):
225 | _tweets = []
226 |
227 | response = await self.client.http.get_medias(self.user_id, cursor=cursor)
228 | if not response['data']['user'].get("result"):
229 | raise UserNotFound(response=response)
230 |
231 | if response['data']['user']['result']['__typename'] == "UserUnavailable":
232 | raise UserProtected(403, "UserUnavailable", response)
233 |
234 | entries = find_objects(response, "tweetDisplayType", "MediaGrid", none_value=[])
235 |
236 | for entry in entries:
237 | object_type = Tweet
238 |
239 | try:
240 | if object_type is None:
241 | continue
242 |
243 | parsed = object_type(self.client, entry, None)
244 | if parsed:
245 | _tweets.append(parsed)
246 | except:
247 | pass
248 |
249 | cursor = self._get_cursor_(response)
250 | cursor_top = self._get_cursor_(response, "Top")
251 |
252 | return _tweets, cursor, cursor_top
253 |
254 |
255 | class SelfTimeline(BaseGeneratorClass):
256 | OBJECTS_TYPES = {
257 | "tweet": Tweet,
258 | "homeConversation": SelfThread,
259 | "profile": SelfThread
260 | }
261 | _RESULT_ATTR = "tweets"
262 |
263 | def __init__(self, user_id, client, timeline_type, pages=1, wait_time=2, cursor=None):
264 | super().__init__()
265 | self.tweets = []
266 | self.cursor = cursor
267 | self.is_next_page = True
268 | self.timeline_type = timeline_type
269 | self.client = client
270 | self.user_id = user_id
271 | self.pages = pages
272 | self.wait_time = wait_time
273 |
274 | def _get_target_object(self, tweet):
275 | entry_type = str(tweet['entryId']).split("-")[0]
276 | return self.OBJECTS_TYPES.get(entry_type)
277 |
278 | async def get_page(self, cursor):
279 | _tweets = []
280 | response = await self.client.http.get_home_timeline(timeline_type=self.timeline_type,cursor=cursor)
281 |
282 | entries = self._get_entries(response)
283 |
284 | for entry in entries:
285 | object_type = self._get_target_object(entry)
286 |
287 | try:
288 | if object_type is None:
289 | continue
290 |
291 | parsed = object_type(self.client, entry, None)
292 | if parsed:
293 | _tweets.append(parsed)
294 | except:
295 | pass
296 |
297 | cursor = self._get_cursor_(response)
298 | cursor_top = self._get_cursor_(response, "Top")
299 |
300 | return _tweets, cursor, cursor_top
301 |
302 |
303 | class TweetComments(BaseGeneratorClass):
304 | OBJECTS_TYPES = {
305 | "conversationthread": ConversationThread,
306 | "tweet": Tweet,
307 | }
308 | _RESULT_ATTR = "tweets"
309 |
310 | def __init__(self, tweet_id, client, get_hidden=False, filter_=TweetCommentFilters.Relevant, pages=1, wait_time=2, cursor=None):
311 | super().__init__()
312 | self.tweets = []
313 | self.cursor = cursor
314 | self.is_next_page = True
315 | self.get_hidden = get_hidden
316 | self.client = client
317 | self.tweet_id = tweet_id
318 | self.pages = pages
319 | self.filter= filter_
320 | self.wait_time = wait_time
321 | self.parent = None
322 | self.ignore_empty_list = False
323 |
324 | def _get_target_object(self, tweet):
325 | entry_type = str(tweet['entryId']).split("-")[0]
326 | return self.OBJECTS_TYPES.get(entry_type)
327 |
328 | async def _get_parent(self):
329 | return self.tweet_id if isinstance(self.tweet_id, Tweet) else await self.client.tweet_detail(self.tweet_id)
330 |
331 | async def get_page(self, cursor):
332 | _comments = []
333 | if not self.parent:
334 | self.parent = await self._get_parent()
335 | if self.get_hidden:
336 | response = await self.client.http.get_hidden_comments(self.tweet_id, cursor)
337 | else:
338 | response = await self.client.http.get_tweet_detail(self.tweet_id, cursor, self.filter)
339 |
340 | entries = self._get_entries(response)
341 |
342 | for entry in entries:
343 | object_type = self._get_target_object(entry)
344 |
345 | try:
346 | if object_type is None:
347 | continue
348 | if "Tweet" in str(object_type):
349 | entry = [entry]
350 | object_type = ConversationThread
351 | else:
352 | entry = [i for i in entry.get('content', {}).get('items', [])]
353 |
354 |
355 | if len(entry) > 0:
356 | parsed = object_type(self.client, self.parent, entry)
357 | _comments.append(parsed)
358 | except:
359 | pass
360 |
361 | cursor = self._get_cursor_(response)
362 | cursor_top = self._get_cursor_(response, "Top")
363 | cursor_spam = self._get_cursor_(response, "ShowMoreThreadsPrompt") or self._get_cursor_(response, "ShowMoreThreads")
364 | if cursor_spam:
365 | cursor = cursor_spam
366 |
367 | return _comments, cursor, cursor_top
368 |
369 | def __repr__(self):
370 | return "TweetComments(tweet_id={}, count={}, filter={}, parent={})".format(
371 | self.tweet_id, len(self.tweets), self.filter, self.parent
372 | )
373 |
374 |
375 | class TweetHistory(BaseGeneratorClass):
376 | LATEST_TWEET_ENTRY_ID = "latestTweet"
377 |
378 | def __init__(self, tweet_id, client):
379 | super().__init__()
380 | self.tweets = []
381 | self.client = client
382 | self._tweet_id = tweet_id
383 | self.latest = None
384 |
385 | async def get_history(self):
386 | results = []
387 | response = await self.client.http.get_tweet_edit_history(self._tweet_id)
388 | entries = find_objects(response, "type", "TimelineAddEntries", recursive=False, none_value={})
389 | entries = entries.get('entries', [])
390 | if not entries:
391 | _tweet = self.client.tweet_detail(self._tweet_id)
392 | self.latest = self['latest'] = _tweet
393 | results.append(_tweet)
394 | else:
395 | for entry in entries:
396 | _tweet = Tweet(self.client, entry, None)
397 |
398 | if entry['entryId'] == self.LATEST_TWEET_ENTRY_ID:
399 | self.latest = self['latest'] = _tweet
400 |
401 | results.append(_tweet)
402 | self.tweets = self["tweets"] = results
403 | return results
404 |
405 | def __getitem__(self, index):
406 | if isinstance(index, str):
407 | return getattr(self, index)
408 |
409 | return self.tweets[index]
410 |
411 | def __iter__(self):
412 | for __tweet in self.tweets:
413 | yield __tweet
414 |
415 | def __len__(self):
416 | return len(self.tweets)
417 |
418 | def __repr__(self):
419 | return "TweetHistory(tweets={}, author={})".format(
420 | len(self.tweets), self.tweets[0].author
421 | )
422 |
423 |
424 | class ScheduledTweets(dict):
425 | def __init__(self, client):
426 | super().__init__()
427 | self._client = client
428 | self.tweets = []
429 | self.get_page()
430 |
431 | async def get_page(self):
432 | res = await self._client.http.get_scheduled_tweets()
433 | tweets_list = find_objects(res, "scheduled_tweet_list", value=None, none_value=[])
434 |
435 | for tweet in tweets_list:
436 | try:
437 | self.tweets.append(ScheduledTweet(self._client, tweet))
438 | except:
439 | pass
440 |
441 | self["tweets"] = self.tweets
442 |
443 | def __getitem__(self, index):
444 | if isinstance(index, str):
445 | return getattr(self, index)
446 |
447 | return self.tweets[index]
448 |
449 | def __iter__(self):
450 | for __tweet in self.tweets:
451 | yield __tweet
452 |
453 | def __len__(self):
454 | return len(self.tweets)
455 |
456 | def __repr__(self):
457 | return "ScheduledTweets(tweets={})".format(len(self.tweets))
458 |
459 |
--------------------------------------------------------------------------------
/src/tweety/updates.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 | from .utils import get_running_loop
3 | from .events.base import BaseUpdateMethod
4 |
5 | class UpdateMethods:
6 |
7 | def on(self, event):
8 | def decorator(f):
9 | self.add_event_handler(f, event)
10 | return f
11 |
12 | return decorator
13 |
14 | def add_event_handler(self, callback, event):
15 | if not isinstance(event, BaseUpdateMethod):
16 | event = event()
17 |
18 | self._event_builders.append((event, callback))
19 |
20 | async def _run_until_disconnected(self):
21 | tasks = []
22 | for event in self._event_builders:
23 | update = event[0](self, event[1])
24 | task = get_running_loop().create_task(update.start())
25 | tasks.append(task)
26 | try:
27 | await asyncio.gather(*tasks)
28 | except KeyboardInterrupt:
29 | raise asyncio.CancelledError
30 |
31 | def run_until_disconnected(self):
32 | if get_running_loop().is_running():
33 | return self._run_until_disconnected()
34 | try:
35 | return get_running_loop().run_until_complete(self._run_until_disconnected())
36 | except KeyboardInterrupt:
37 | raise asyncio.CancelledError
38 |
39 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/src/tweety/utils.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 | import base64
3 | import datetime
4 | import inspect
5 | import json
6 | import os.path
7 | import string
8 | import subprocess
9 | import sys
10 | import uuid
11 | import warnings
12 | from functools import wraps
13 | from io import BytesIO
14 | from dateutil import parser as date_parser
15 | from urllib.parse import urlparse, parse_qs
16 | from .exceptions import AuthenticationRequired
17 | from .filters import Language
18 | import re
19 | import random
20 | import hashlib
21 | from typing import Union, List
22 |
23 | GUEST_TOKEN_REGEX = re.compile("gt=(.*?);")
24 | MIGRATION_REGEX = re.compile(r"""(http(?:s)?://(?:www\.)?(twitter|x){1}\.com(/x)?/migrate([/?])?tok=[a-zA-Z0-9%\-_]+)+""", re.VERBOSE)
25 | MIME_TYPES = {
26 | "png": ("image/png", [b"\x89PNG\r\n\x1a\n"]),
27 | "jpg": ("image/jpeg", [b"\xFF\xD8\xFF"]),
28 | "jpeg": ("image/jpeg", [b"\xFF\xD8\xFF"]),
29 | "jfif": ("image/jpeg", [b"\xFF\xD8\xFF\xE0", b"\xFF\xD8\xFF\xE1"]),
30 | "gif": ("image/gif", [b"GIF87a", b"GIF89a"]),
31 | "webp": ("image/webp", [b"RIFF"]),
32 | "mp4": ("video/mp4", [b"\x00\x00\x00\x18ftypisom", b"\x00\x00\x00\x18ftypmp42",
33 | b"\x00\x00\x00\x18ftypisom", b"\x00\x00\x00\x18ftypMSNV",
34 | b"\x00\x00\x00\x18ftypmp41"]),
35 | "mov": ("video/quicktime", [b"\x00\x00\x00\x14ftypqt"]),
36 | "m4v": ("video/x-m4v", [b"\x00\x00\x00\x18ftypM4V", b"\x00\x00\x00\x20ftypM4V"])
37 | }
38 |
39 | WORKBOOK_HEADERS = ['Date', 'Author', 'id', 'text', 'is_retweet', 'is_reply', 'language', 'likes',
40 | 'retweet_count', 'source', 'medias', 'user_mentioned', 'urls', 'hashtags', 'symbols']
41 |
42 | SENSITIVE_MEDIA_TAGS = ['adult_content', 'graphic_violence', 'other']
43 | def Warn(text, category=DeprecationWarning):
44 | this_text = text
45 | this_category = category
46 |
47 | def decorator(method):
48 | if inspect.iscoroutinefunction(method):
49 | @wraps(method)
50 | async def async_wrapper(self, *args, **kwargs):
51 | warnings.warn(message=this_text, category=this_category)
52 | return await method(self, *args, **kwargs)
53 |
54 | return async_wrapper
55 | else:
56 | @wraps(method)
57 | def sync_wrapper(self, *args, **kwargs):
58 | warnings.warn(message=this_text, category=this_category)
59 | return method(self, *args, **kwargs)
60 |
61 | return sync_wrapper
62 | return decorator
63 |
64 | def DictRequestData(cls):
65 |
66 | def method_wrapper_decorator(func):
67 | request_keys = ["method", "url", "params", "json", "data"]
68 |
69 | def wrapper(self, *args, **kwargs):
70 | request_data = func(self, *args, **kwargs)
71 |
72 | request = {"headers": {}}
73 | for index, data in enumerate(request_data):
74 | this_key = request_keys[index]
75 | request[this_key] = data
76 | return request
77 |
78 | return wrapper
79 |
80 | if inspect.isclass(cls):
81 | for name, method in vars(cls).items():
82 | if name != "__init__" and callable(method):
83 | setattr(cls, name, method_wrapper_decorator(method))
84 | return cls
85 | return method_wrapper_decorator(cls)
86 |
87 |
88 | def AuthRequired(cls):
89 | def method_async_wrapper_decorator(func):
90 | async def wrapper(self, *args, **kwargs):
91 | if self.me is None:
92 | raise AuthenticationRequired(200, "GenericForbidden", None)
93 |
94 | return await func(self, *args, **kwargs)
95 | return wrapper
96 |
97 | def method_wrapper_decorator(func):
98 | def wrapper(self, *args, **kwargs):
99 | if self.me is None:
100 | raise AuthenticationRequired(200, "GenericForbidden", None)
101 |
102 | return func(self, *args, **kwargs)
103 | return wrapper
104 |
105 | if inspect.isclass(cls):
106 | for name, method in vars(cls).items():
107 | if name != "__init__" and callable(method):
108 | if "iter_" in name:
109 | setattr(cls, name, method_wrapper_decorator(method))
110 | else:
111 | setattr(cls, name, method_async_wrapper_decorator(method))
112 | return cls
113 |
114 | if "iter_" in cls.__name__:
115 | return method_wrapper_decorator(cls)
116 | return method_async_wrapper_decorator(cls)
117 |
118 | def mime_from_buffer(file_buffer_or_bytes):
119 | try:
120 | import magic
121 | mime_detector = magic.Magic(mime=True)
122 | return mime_detector.from_buffer(file_buffer_or_bytes)
123 | except ImportError:
124 | for file_type, (mime_type, this_file_headers) in MIME_TYPES.items():
125 | for header in this_file_headers:
126 | if file_buffer_or_bytes.startswith(header):
127 | return mime_type
128 |
129 |
130 | return None
131 |
132 |
133 |
134 | def get_running_loop():
135 | if sys.version_info >= (3, 7):
136 | try:
137 | return asyncio.get_running_loop()
138 | except RuntimeError:
139 | loop = asyncio.new_event_loop()
140 | asyncio.set_event_loop(loop)
141 | return loop
142 | # return asyncio.get_event_loop_policy().get_event_loop()
143 | else:
144 | return asyncio.get_event_loop()
145 |
146 |
147 | async def async_list(generator_base_object):
148 | async for _ in generator_base_object.generator():
149 | pass
150 | return generator_base_object
151 |
152 |
153 | def float_to_hex(x):
154 | result = []
155 | quotient = int(x)
156 | fraction = x - quotient
157 |
158 | while quotient > 0:
159 | quotient = int(x / 16)
160 | remainder = int(x - (float(quotient) * 16))
161 |
162 | if remainder > 9:
163 | result.insert(0, chr(remainder + 55))
164 | else:
165 | result.insert(0, str(remainder))
166 |
167 | x = float(quotient)
168 |
169 | if fraction == 0:
170 | return ''.join(result)
171 |
172 | result.append('.')
173 |
174 | while fraction > 0:
175 | fraction *= 16
176 | integer = int(fraction)
177 | fraction -= float(integer)
178 |
179 | if integer > 9:
180 | result.append(chr(integer + 55))
181 | else:
182 | result.append(str(integer))
183 |
184 | return ''.join(result)
185 |
186 |
187 | def is_odd(num: Union[int, float]):
188 | if num % 2:
189 | return -1.0
190 | return 0.0
191 |
192 |
193 | def base64_encode(this_string):
194 | this_string = this_string.encode() if isinstance(this_string, str) else this_string
195 | return base64.b64encode(this_string).decode()
196 |
197 |
198 | def base64_decode(this_input):
199 | try:
200 | data = base64.b64decode(this_input)
201 | return data.decode()
202 | except Exception: # noqa
203 | return list(bytes(this_input, "utf-8"))
204 |
205 |
206 | def replace_between_indexes(original_string, from_index, to_index, replacement_text):
207 | new_string = original_string[:from_index] + replacement_text + original_string[to_index:]
208 | return new_string
209 |
210 |
211 | def decodeBase64(encoded_string):
212 | return base64.b64decode(encoded_string).decode("utf-8")
213 |
214 |
215 | def bar_progress(filename, total, current, width=80):
216 | progress_message = f"[{filename}] Downloading: %d%% [%d / %d] bytes" % (current / total * 100, current, total)
217 | sys.stdout.write("\r" + progress_message)
218 | sys.stdout.flush()
219 |
220 |
221 | def parse_wait_time(wait_time):
222 | if not wait_time:
223 | return 0
224 |
225 | if isinstance(wait_time, (tuple, list)):
226 |
227 | if len(wait_time) == 1:
228 | return int(wait_time[0])
229 |
230 | wait_time = [int(i) for i in wait_time[:2]]
231 | return random.randint(*wait_time)
232 |
233 | return int(wait_time)
234 |
235 |
236 | def get_next_index(iterable, current_index, __default__=None):
237 | try:
238 | _ = iterable[current_index + 1]
239 | return current_index + 1
240 | except IndexError:
241 | return __default__
242 |
243 |
244 | def custom_json(self, **kwargs):
245 | try:
246 | return json.loads(self.content, **kwargs)
247 | except:
248 | return None
249 |
250 |
251 | def create_request_id():
252 | return str(uuid.uuid1())
253 |
254 | def create_conversation_id(sender, receiver):
255 | sender = int(sender)
256 | receiver = int(receiver)
257 |
258 | if sender > receiver:
259 | return f"{receiver}-{sender}"
260 | else:
261 | return f"{sender}-{receiver}"
262 |
263 |
264 | def create_query_id():
265 | return get_random_string(22)
266 |
267 |
268 | def check_if_file_is_supported(file):
269 | if isinstance(file, str) and not str(file).startswith("https://") and not os.path.exists(file):
270 | raise ValueError("Path {} doesn't exists".format(file))
271 |
272 | if isinstance(file, bytes):
273 | file = file
274 | file_mime = mime_from_buffer(file)
275 | elif isinstance(file, BytesIO):
276 | file = file.getvalue()
277 | file_mime = mime_from_buffer(file)
278 | elif str(file.__class__.__name__) == "Gif":
279 | file_extension = "gif"
280 | file_mime = MIME_TYPES.get(file_extension)
281 | else:
282 | file = file.split("?")[0]
283 | file_extension = file.split(".")[-1]
284 | file_mime = MIME_TYPES.get(file_extension)[0]
285 |
286 | if file_mime not in [i[0] for i in list(MIME_TYPES.values())]:
287 | raise ValueError("File Extension is not supported. Use any of {}".format(list(MIME_TYPES.keys())))
288 |
289 | return file_mime
290 |
291 |
292 | def get_random_string(length):
293 | return ''.join(random.choices(string.ascii_letters + string.digits, k=int(length)))
294 |
295 |
296 | def calculate_md5(file_path):
297 | if str(file_path).startswith("https://"):
298 | return None
299 |
300 | md5_hash = hashlib.md5()
301 | if isinstance(file_path, bytes):
302 | md5_hash.update(file_path)
303 | else:
304 | with open(file_path, "rb") as file:
305 | for chunk in iter(lambda: file.read(4096), b""):
306 | md5_hash.update(chunk)
307 | return md5_hash.hexdigest()
308 |
309 |
310 | def create_media_entities(files):
311 | entities = []
312 | for file in files:
313 | media_id = file.media_id if hasattr(file, "media_id") else file
314 | entities.append({
315 | "media_id": media_id,
316 | "tagged_users": []
317 | })
318 |
319 | return entities
320 |
321 |
322 | def check_sensitive_media_tags(tags):
323 | return [tag for tag in tags if tag in SENSITIVE_MEDIA_TAGS]
324 |
325 |
326 | def find_objects(obj, key, value, recursive=True, none_value=None):
327 | results = []
328 |
329 | def find_matching_objects(_obj, _key, _value):
330 | if isinstance(_obj, dict):
331 | if _key in _obj:
332 | found = False
333 | if _value is None:
334 | found = True
335 | results.append(_obj[_key])
336 | elif (isinstance(_value, list) and _obj[_key] in _value) or _obj[_key] == _value:
337 | found = True
338 | results.append(_obj)
339 |
340 | if not recursive and found:
341 | return results[0]
342 |
343 | for sub_obj in _obj.values():
344 | find_matching_objects(sub_obj, _key, _value)
345 | elif isinstance(_obj, list):
346 | for item in _obj:
347 | find_matching_objects(item, _key, _value)
348 |
349 | find_matching_objects(obj, key, value)
350 |
351 | if len(results) == 1:
352 | return results[0]
353 |
354 | if len(results) == 0:
355 | return none_value
356 |
357 | if not recursive:
358 | return results[0]
359 |
360 | return results
361 |
362 |
363 | def create_pool(duration: int, *choices):
364 | data = {
365 | "twitter:long:duration_minutes": duration,
366 | "twitter:api:api:endpoint": "1",
367 | "twitter:card": f"poll{len(choices)}choice_text_only"
368 | }
369 |
370 | for index, choice in enumerate(choices, start=1):
371 | key = f"twitter:string:choice{index}_label"
372 | data[key] = choice
373 |
374 | return data
375 |
376 |
377 | def parse_time(time):
378 | if not time:
379 | return None
380 |
381 | if isinstance(time, (datetime.datetime, datetime.date)):
382 | return time
383 |
384 | if isinstance(time, float):
385 | time = int(time)
386 |
387 | if isinstance(time, int) or str(time).isdigit():
388 | try:
389 | return datetime.datetime.fromtimestamp(int(time))
390 | except (OSError, ValueError):
391 | return datetime.datetime.fromtimestamp(int(time) / 1000)
392 |
393 | return date_parser.parse(time)
394 |
395 |
396 | async def get_user_from_typehead(target_username, users):
397 | for user in users:
398 | if str(user.username).lower() == str(target_username).lower():
399 | return user
400 | return None
401 |
402 |
403 | def get_tweet_id(tweet_identifier):
404 | if str(tweet_identifier.__class__.__name__) == "Tweet":
405 | return tweet_identifier.id
406 | else:
407 | return urlparse(str(tweet_identifier)).path.split("/")[-1]
408 |
409 |
410 | def is_tweet_protected(raw):
411 | protected = find_objects(raw, "__typename", ["TweetUnavailable", "TweetTombstone"], recursive=False)
412 |
413 | if protected is None:
414 | is_not_dummy_object = find_objects(raw, "tweet_results", None, recursive=False)
415 | if isinstance(is_not_dummy_object, dict) and len(is_not_dummy_object) == 0:
416 | return True
417 |
418 | return protected
419 |
420 |
421 | def check_translation_lang(lang):
422 | for k, v in vars(Language).items():
423 | if not str(k).startswith("_"):
424 | if str(k).lower() == str(lang).lower() or str(v).lower() == str(lang).lower():
425 | return v
426 |
427 | raise ValueError(f"Language {lang} is not supported")
428 |
429 |
430 | def iterable_to_string(__iterable__: Union[list, tuple], __delimiter__: str = ",", __attr__: str = None):
431 | if not isinstance(__iterable__, (list, tuple)) or len(__iterable__) == 0:
432 | return ""
433 |
434 | if __attr__:
435 | __iterable__ = [str(getattr(i, __attr__)) for i in __iterable__]
436 |
437 | return __delimiter__.join(__iterable__)
438 |
439 |
440 | def dict_to_string(__dict__: dict, __object_delimiter__: str = "=", __end_delimiter__: str = ";"):
441 | actual_string = ""
442 | for key, value in __dict__.items():
443 | actual_string += f"{key}{__object_delimiter__}{value}{__end_delimiter__}"
444 |
445 | return actual_string
446 |
447 |
448 | def get_url_parts(url):
449 | parsed_url = urlparse(url)
450 | query_params = parse_qs(parsed_url.query)
451 |
452 | url_parts = {
453 | "scheme": parsed_url.scheme,
454 | "netloc": parsed_url.netloc,
455 | "path": parsed_url.path,
456 | "params": parsed_url.params,
457 | "query": query_params,
458 | "fragment": parsed_url.fragment,
459 | "host": f"{parsed_url.scheme}://{parsed_url.netloc}"
460 | }
461 |
462 | return url_parts
463 |
464 |
465 | def unpack_proxy(proxy_dict):
466 | username, password, host, port = None, None, None, None
467 | if str(proxy_dict.__class__.__name__) == "Proxy":
468 | proxy_dict = proxy_dict.get_dict()
469 |
470 | proxy = proxy_dict.get("http://") or proxy_dict.get("https://")
471 | scheme, url = proxy.split("://")
472 | creds, host_with_port = None, None
473 | url_split = url.split("@")
474 | if len(url_split) == 2:
475 | creds, host_with_port = url_split
476 | else:
477 | host_with_port = url_split[0]
478 |
479 | host, port = host_with_port.split(":")
480 | if creds is not None:
481 | username, password = creds.split(":")
482 |
483 | return {
484 | "type": scheme,
485 | "host": host,
486 | "port": port,
487 | "username": username,
488 | "password": password
489 | }
490 |
491 |
492 | def run_command(command):
493 | try:
494 | if isinstance(command, (list, tuple)):
495 | command = " ".join(command)
496 |
497 | result = subprocess.run(command, shell=True, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
498 | return result.stdout.decode('utf-8')
499 | except subprocess.CalledProcessError as e:
500 | raise Exception(f"Command '{command}' failed with error: {e.stderr.decode('utf-8')}")
501 |
502 |
503 | def encode_audio_message(input_filename, ffmpeg_path=None):
504 | """
505 | Encode the mp3 or audio file to Twitter Audio Message Format
506 |
507 | :param input_filename: Filename of mp3/ogg or audio file
508 | :param ffmpeg_path: Path of 'ffmpeg' binary for your platform
509 | :return: str (`encoded_filename`)
510 | """
511 |
512 | if not ffmpeg_path:
513 | ffmpeg_path = "ffmpeg"
514 |
515 | _input_filename = f'"{input_filename}"'
516 | _output_aac_filename = f'"{input_filename}.aac"'
517 | output_filename = f'"{input_filename}.mp4"'
518 |
519 | commands = [
520 | [ffmpeg_path, "-y", "-i", _input_filename, "-c:a", "aac", "-b:a", "65k", "-ar", "44100", "-ac", "1", _output_aac_filename],
521 | [ffmpeg_path, "-y", "-f", "lavfi", "-i", "color=c=black:s=854x480", "-i", _output_aac_filename, "-c:v", "libx264", "-c:a", "copy", "-shortest", output_filename]
522 | ]
523 |
524 | for command in commands:
525 | run_command(command)
526 |
527 | try:
528 | # Attempt to delete aac audio file in order to save disk space
529 | os.remove(_output_aac_filename)
530 | except:
531 | pass
532 |
533 | return output_filename[1:-1]
534 |
535 |
536 | def tweet_id_to_datetime(tweet_id: int):
537 | return datetime.datetime.fromtimestamp(((tweet_id >> 22) + 1288834974657) / 1000.0)
538 |
539 | def json_stringify(json_data):
540 | return str(json.dumps(json_data, separators=(",", ":")))
541 |
542 | def create_search_query(
543 | search_term=None,
544 | from_users=None,
545 | to_users=None,
546 | mentioning_these_users=None,
547 | exact_word=None,
548 | none_of_these_words=None,
549 | language=None,
550 | include_replies=True,
551 | only_replies=False,
552 | include_links=True,
553 | only_links=False,
554 | minimum_replies=None,
555 | minimum_likes=None,
556 | minimum_reposts=None,
557 | from_date=None,
558 | to_date=None
559 | ):
560 | date_format = "%Y-%m-%d"
561 | new_search_term = ""
562 |
563 | if search_term:
564 | new_search_term += f" {search_term} "
565 |
566 | if exact_word:
567 | new_search_term += f' "{exact_word}" '
568 |
569 | if from_users:
570 | if not isinstance(from_users, list):
571 | from_users = [from_users]
572 |
573 | from_users = [f"from:{i}" for i in from_users]
574 | from_users = " OR ".join(from_users)
575 | new_search_term += f" ({from_users}) "
576 |
577 | if to_users:
578 | if not isinstance(to_users, list):
579 | to_users = [to_users]
580 |
581 | to_users = [f"to:{i}" for i in to_users]
582 | to_users = " OR ".join(to_users)
583 | new_search_term += f" ({to_users}) "
584 |
585 | if mentioning_these_users:
586 | if not isinstance(mentioning_these_users, list):
587 | mentioning_these_users = [mentioning_these_users]
588 |
589 | mentioning_these_users = [f"@{i}" for i in mentioning_these_users]
590 | mentioning_these_users = " OR ".join(mentioning_these_users)
591 | new_search_term += f" ({mentioning_these_users}) "
592 |
593 | if none_of_these_words:
594 | if not isinstance(none_of_these_words, list):
595 | none_of_these_words = [none_of_these_words]
596 |
597 | none_of_these_words = ",".join(none_of_these_words)
598 | new_search_term += f" -{none_of_these_words} "
599 |
600 | if language:
601 | new_search_term += f" lang:{language} "
602 |
603 | if not include_replies:
604 | new_search_term += " -filter:replies "
605 | elif include_replies and only_replies:
606 | new_search_term += " filter:replies "
607 |
608 | if not include_links:
609 | new_search_term += " -filter:links "
610 | elif include_links and only_links:
611 | new_search_term += " filter:links "
612 |
613 | if minimum_likes:
614 | new_search_term += f" min_faves:{minimum_likes} "
615 |
616 | if minimum_replies:
617 | new_search_term += f" min_replies:{minimum_replies} "
618 |
619 | if minimum_reposts:
620 | new_search_term += f" min_retweets:{minimum_reposts} "
621 |
622 | if from_date:
623 | if isinstance(from_date, (datetime.datetime, datetime.date)):
624 | from_date = from_date.strftime(date_format)
625 | elif isinstance(from_date, str):
626 | from_date = parse_time(from_date).strftime(date_format)
627 | new_search_term += f" since:{from_date} "
628 |
629 | if to_date:
630 | if isinstance(to_date, (datetime.datetime, datetime.date)):
631 | to_date = to_date.strftime(date_format)
632 | elif isinstance(to_date, str):
633 | to_date = parse_time(to_date).strftime(date_format)
634 | new_search_term += f" until:{to_date} "
635 |
636 | return new_search_term
637 |
--------------------------------------------------------------------------------