├── .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 | [![Downloads](https://static.pepy.tech/personalized-badge/tweety-ns?period=total&units=international_system&left_color=orange&right_color=blue&left_text=Downloads)](https://pepy.tech/project/tweety-ns) [![Ask DeepWiki](https://deepwiki.com/badge.svg)](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 | --------------------------------------------------------------------------------