├── .github └── workflows │ └── python-publish.yml ├── .gitignore ├── LICENSE ├── MANIFEST.in ├── README.md ├── examples ├── get_messages.py ├── get_unread_messages.py ├── send_mms.py ├── send_sms.py └── simple_bot.py ├── get_cookie.mp4 ├── get_username.mp4 ├── pytextnow ├── TNAPI.py ├── __init__.py ├── error.py ├── login.py ├── message.py ├── message_container.py └── multi_media_message.py ├── requirements.txt └── setup.py /.github/workflows/python-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will upload a Python Package using Twine when a release is created 2 | # For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries 3 | 4 | # This workflow uses actions that are not certified by GitHub. 5 | # They are provided by a third-party and are governed by 6 | # separate terms of service, privacy policy, and support 7 | # documentation. 8 | 9 | name: Upload Python Package 10 | 11 | on: 12 | release: 13 | types: [published] 14 | 15 | jobs: 16 | deploy: 17 | 18 | runs-on: ubuntu-latest 19 | 20 | steps: 21 | - uses: actions/checkout@v2 22 | - name: Set up Python 23 | uses: actions/setup-python@v2 24 | with: 25 | python-version: '3.x' 26 | - name: Install dependencies 27 | run: | 28 | python -m pip install --upgrade pip 29 | pip install build 30 | - name: Build package 31 | run: python -m build 32 | - name: Publish package 33 | uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29 34 | with: 35 | user: __token__ 36 | password: ${{ secrets.PYPI_API_TOKEN }} 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by .ignore support plugin (hsz.mobi) 3 | ### Python template 4 | # Byte-compiled / optimized / DLL files 5 | __pycache__/ 6 | *.py[cod] 7 | *$py.class 8 | 9 | # C extensions 10 | *.so 11 | 12 | # Distribution / packaging 13 | .Python 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | wheels/ 26 | share/python-wheels/ 27 | *.egg-info/ 28 | .installed.cfg 29 | *.egg 30 | MANIFEST 31 | test.* 32 | 33 | # PyInstaller 34 | # Usually these files are written by a python script from a template 35 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 36 | *.manifest 37 | *.spec 38 | 39 | # Installer logs 40 | pip-log.txt 41 | pip-delete-this-directory.txt 42 | 43 | # Unit test / coverage reports 44 | htmlcov/ 45 | .tox/ 46 | .nox/ 47 | .coverage 48 | .coverage.* 49 | .cache 50 | nosetests.xml 51 | coverage.xml 52 | *.cover 53 | *.py,cover 54 | .hypothesis/ 55 | .pytest_cache/ 56 | cover/ 57 | 58 | # Translations 59 | *.mo 60 | *.pot 61 | 62 | # Django stuff: 63 | *.log 64 | local_settings.py 65 | db.sqlite3 66 | db.sqlite3-journal 67 | 68 | # Flask stuff: 69 | instance/ 70 | .webassets-cache 71 | 72 | # Scrapy stuff: 73 | .scrapy 74 | 75 | # Sphinx documentation 76 | docs/_build/ 77 | 78 | # PyBuilder 79 | .pybuilder/ 80 | target/ 81 | 82 | # Jupyter Notebook 83 | .ipynb_checkpoints 84 | 85 | # IPython 86 | profile_default/ 87 | ipython_config.py 88 | 89 | # pyenv 90 | # For a library or package, you might want to ignore these files since the code is 91 | # intended to run in multiple environments; otherwise, check them in: 92 | # .python-version 93 | 94 | # pipenv 95 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 96 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 97 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 98 | # install all needed dependencies. 99 | #Pipfile.lock 100 | 101 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 102 | __pypackages__/ 103 | 104 | # Celery stuff 105 | celerybeat-schedule 106 | celerybeat.pid 107 | 108 | # SageMath parsed files 109 | *.sage.py 110 | 111 | # Environments 112 | .env 113 | .venv 114 | env/ 115 | venv/ 116 | ENV/ 117 | env.bak/ 118 | venv.bak/ 119 | 120 | # Spyder project settings 121 | .spyderproject 122 | .spyproject 123 | 124 | # Rope project settings 125 | .ropeproject 126 | 127 | # mkdocs documentation 128 | /site 129 | 130 | # mypy 131 | .mypy_cache/ 132 | .dmypy.json 133 | dmypy.json 134 | 135 | # Pyre type checker 136 | .pyre/ 137 | 138 | # pytype static type analyzer 139 | .pytype/ 140 | 141 | # Cython debug symbols 142 | cython_debug/ 143 | 144 | ### Python template 145 | # Byte-compiled / optimized / DLL files 146 | 147 | # C extensions 148 | 149 | # Distribution / packaging 150 | 151 | # PyInstaller 152 | # Usually these files are written by a python script from a template 153 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 154 | 155 | # Installer logs 156 | 157 | # Unit test / coverage reports 158 | 159 | # Translations 160 | 161 | # Django stuff: 162 | 163 | # Flask stuff: 164 | 165 | # Scrapy stuff: 166 | 167 | # Sphinx documentation 168 | 169 | # PyBuilder 170 | 171 | # Jupyter Notebook 172 | 173 | # IPython 174 | 175 | # pyenv 176 | # For a library or package, you might want to ignore these files since the code is 177 | # intended to run in multiple environments; otherwise, check them in: 178 | # .python-version 179 | 180 | # pipenv 181 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 182 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 183 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 184 | # install all needed dependencies. 185 | #Pipfile.lock 186 | 187 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 188 | 189 | # Celery stuff 190 | 191 | # SageMath parsed files 192 | 193 | # Environments 194 | 195 | # Spyder project settings 196 | 197 | # Rope project settings 198 | 199 | # mkdocs documentation 200 | 201 | # mypy 202 | 203 | # Pyre type checker 204 | 205 | # pytype static type analyzer 206 | 207 | # Cython debug symbols 208 | 209 | ### SublimeText template 210 | # Cache files for Sublime Text 211 | *.tmlanguage.cache 212 | *.tmPreferences.cache 213 | *.stTheme.cache 214 | 215 | # Workspace files are user-specific 216 | *.sublime-workspace 217 | 218 | # Project files should be checked into the repository, unless a significant 219 | # proportion of contributors will probably not be using Sublime Text 220 | # *.sublime-project 221 | 222 | # SFTP configuration file 223 | sftp-config.json 224 | sftp-config-alt*.json 225 | 226 | # Package control specific files 227 | Package Control.last-run 228 | Package Control.ca-list 229 | Package Control.ca-bundle 230 | Package Control.system-ca-bundle 231 | Package Control.cache/ 232 | Package Control.ca-certs/ 233 | Package Control.merged-ca-bundle 234 | Package Control.user-ca-bundle 235 | oscrypto-ca-bundle.crt 236 | bh_unicode_properties.cache 237 | 238 | # Sublime-github package stores a github token in this file 239 | # https://packagecontrol.io/packages/sublime-github 240 | GitHub.sublime-settings 241 | 242 | ### macOS template 243 | # General 244 | .DS_Store 245 | .AppleDouble 246 | .LSOverride 247 | 248 | # Icon must end with two \r 249 | Icon 250 | 251 | # Thumbnails 252 | ._* 253 | 254 | # Files that might appear in the root of a volume 255 | .DocumentRevisions-V100 256 | .fseventsd 257 | .Spotlight-V100 258 | .TemporaryItems 259 | .Trashes 260 | .VolumeIcon.icns 261 | .com.apple.timemachine.donotpresent 262 | 263 | # Directories potentially created on remote AFP share 264 | .AppleDB 265 | .AppleDesktop 266 | Network Trash Folder 267 | Temporary Items 268 | .apdisk 269 | 270 | ### VisualStudio template 271 | ## Ignore Visual Studio temporary files, build results, and 272 | ## files generated by popular Visual Studio add-ons. 273 | ## 274 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 275 | 276 | # User-specific files 277 | *.rsuser 278 | *.suo 279 | *.user 280 | *.userosscache 281 | *.sln.docstates 282 | 283 | # User-specific files (MonoDevelop/Xamarin Studio) 284 | *.userprefs 285 | 286 | # Mono auto generated files 287 | mono_crash.* 288 | 289 | # Build results 290 | [Dd]ebug/ 291 | [Dd]ebugPublic/ 292 | [Rr]elease/ 293 | [Rr]eleases/ 294 | x64/ 295 | x86/ 296 | [Ww][Ii][Nn]32/ 297 | [Aa][Rr][Mm]/ 298 | [Aa][Rr][Mm]64/ 299 | bld/ 300 | [Bb]in/ 301 | [Oo]bj/ 302 | [Ll]og/ 303 | [Ll]ogs/ 304 | 305 | # Visual Studio 2015/2017 cache/options directory 306 | .vs/ 307 | # Uncomment if you have tasks that create the project's static files in wwwroot 308 | #wwwroot/ 309 | 310 | # Visual Studio 2017 auto generated files 311 | Generated\ Files/ 312 | 313 | # MSTest test Results 314 | [Tt]est[Rr]esult*/ 315 | [Bb]uild[Ll]og.* 316 | 317 | # NUnit 318 | *.VisualState.xml 319 | TestResult.xml 320 | nunit-*.xml 321 | 322 | # Build Results of an ATL Project 323 | [Dd]ebugPS/ 324 | [Rr]eleasePS/ 325 | dlldata.c 326 | 327 | # Benchmark Results 328 | BenchmarkDotNet.Artifacts/ 329 | 330 | # .NET Core 331 | project.lock.json 332 | project.fragment.lock.json 333 | artifacts/ 334 | 335 | # ASP.NET Scaffolding 336 | ScaffoldingReadMe.txt 337 | 338 | # StyleCop 339 | StyleCopReport.xml 340 | 341 | # Files built by Visual Studio 342 | *_i.c 343 | *_p.c 344 | *_h.h 345 | *.ilk 346 | *.meta 347 | *.obj 348 | *.iobj 349 | *.pch 350 | *.pdb 351 | *.ipdb 352 | *.pgc 353 | *.pgd 354 | *.rsp 355 | *.sbr 356 | *.tlb 357 | *.tli 358 | *.tlh 359 | *.tmp 360 | *.tmp_proj 361 | *_wpftmp.csproj 362 | *.vspscc 363 | *.vssscc 364 | .builds 365 | *.pidb 366 | *.svclog 367 | *.scc 368 | 369 | # Chutzpah Test files 370 | _Chutzpah* 371 | 372 | # Visual C++ cache files 373 | ipch/ 374 | *.aps 375 | *.ncb 376 | *.opendb 377 | *.opensdf 378 | *.sdf 379 | *.cachefile 380 | *.VC.db 381 | *.VC.VC.opendb 382 | 383 | # Visual Studio profiler 384 | *.psess 385 | *.vsp 386 | *.vspx 387 | *.sap 388 | 389 | # Visual Studio Trace Files 390 | *.e2e 391 | 392 | # TFS 2012 Local Workspace 393 | $tf/ 394 | 395 | # Guidance Automation Toolkit 396 | *.gpState 397 | 398 | # ReSharper is a .NET coding add-in 399 | _ReSharper*/ 400 | *.[Rr]e[Ss]harper 401 | *.DotSettings.user 402 | 403 | # TeamCity is a build add-in 404 | _TeamCity* 405 | 406 | # DotCover is a Code Coverage Tool 407 | *.dotCover 408 | 409 | # AxoCover is a Code Coverage Tool 410 | .axoCover/* 411 | !.axoCover/settings.json 412 | 413 | # Coverlet is a free, cross platform Code Coverage Tool 414 | coverage*.json 415 | coverage*.xml 416 | coverage*.info 417 | 418 | # Visual Studio code coverage results 419 | *.coverage 420 | *.coveragexml 421 | 422 | # NCrunch 423 | _NCrunch_* 424 | .*crunch*.local.xml 425 | nCrunchTemp_* 426 | 427 | # MightyMoose 428 | *.mm.* 429 | AutoTest.Net/ 430 | 431 | # Web workbench (sass) 432 | .sass-cache/ 433 | 434 | # Installshield output folder 435 | [Ee]xpress/ 436 | 437 | # DocProject is a documentation generator add-in 438 | DocProject/buildhelp/ 439 | DocProject/Help/*.HxT 440 | DocProject/Help/*.HxC 441 | DocProject/Help/*.hhc 442 | DocProject/Help/*.hhk 443 | DocProject/Help/*.hhp 444 | DocProject/Help/Html2 445 | DocProject/Help/html 446 | 447 | # Click-Once directory 448 | publish/ 449 | 450 | # Publish Web Output 451 | *.[Pp]ublish.xml 452 | *.azurePubxml 453 | # Note: Comment the next line if you want to checkin your web deploy settings, 454 | # but database connection strings (with potential passwords) will be unencrypted 455 | *.pubxml 456 | *.publishproj 457 | 458 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 459 | # checkin your Azure Web App publish settings, but sensitive information contained 460 | # in these scripts will be unencrypted 461 | PublishScripts/ 462 | 463 | # NuGet Packages 464 | *.nupkg 465 | # NuGet Symbol Packages 466 | *.snupkg 467 | # The packages folder can be ignored because of Package Restore 468 | **/[Pp]ackages/* 469 | # except build/, which is used as an MSBuild target. 470 | !**/[Pp]ackages/build/ 471 | # Uncomment if necessary however generally it will be regenerated when needed 472 | #!**/[Pp]ackages/repositories.config 473 | # NuGet v3's project.json files produces more ignorable files 474 | *.nuget.props 475 | *.nuget.targets 476 | 477 | # Microsoft Azure Build Output 478 | csx/ 479 | *.build.csdef 480 | 481 | # Microsoft Azure Emulator 482 | ecf/ 483 | rcf/ 484 | 485 | # Windows Store app package directories and files 486 | AppPackages/ 487 | BundleArtifacts/ 488 | Package.StoreAssociation.xml 489 | _pkginfo.txt 490 | *.appx 491 | *.appxbundle 492 | *.appxupload 493 | 494 | # Visual Studio cache files 495 | # files ending in .cache can be ignored 496 | *.[Cc]ache 497 | # but keep track of directories ending in .cache 498 | !?*.[Cc]ache/ 499 | 500 | # Others 501 | ClientBin/ 502 | ~$* 503 | *~ 504 | *.dbmdl 505 | *.dbproj.schemaview 506 | *.jfm 507 | *.pfx 508 | *.publishsettings 509 | orleans.codegen.cs 510 | 511 | # Including strong name files can present a security risk 512 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 513 | #*.snk 514 | 515 | # Since there are multiple workflows, uncomment next line to ignore bower_components 516 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 517 | #bower_components/ 518 | 519 | # RIA/Silverlight projects 520 | Generated_Code/ 521 | 522 | # Backup & report files from converting an old project file 523 | # to a newer Visual Studio version. Backup files are not needed, 524 | # because we have git ;-) 525 | _UpgradeReport_Files/ 526 | Backup*/ 527 | UpgradeLog*.XML 528 | UpgradeLog*.htm 529 | ServiceFabricBackup/ 530 | *.rptproj.bak 531 | 532 | # SQL Server files 533 | *.mdf 534 | *.ldf 535 | *.ndf 536 | 537 | # Business Intelligence projects 538 | *.rdl.data 539 | *.bim.layout 540 | *.bim_*.settings 541 | *.rptproj.rsuser 542 | *- [Bb]ackup.rdl 543 | *- [Bb]ackup ([0-9]).rdl 544 | *- [Bb]ackup ([0-9][0-9]).rdl 545 | 546 | # Microsoft Fakes 547 | FakesAssemblies/ 548 | 549 | # GhostDoc plugin setting file 550 | *.GhostDoc.xml 551 | 552 | # Node.js Tools for Visual Studio 553 | .ntvs_analysis.dat 554 | node_modules/ 555 | 556 | # Visual Studio 6 build log 557 | *.plg 558 | 559 | # Visual Studio 6 workspace options file 560 | *.opt 561 | 562 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 563 | *.vbw 564 | 565 | # Visual Studio LightSwitch build output 566 | **/*.HTMLClient/GeneratedArtifacts 567 | **/*.DesktopClient/GeneratedArtifacts 568 | **/*.DesktopClient/ModelManifest.xml 569 | **/*.Server/GeneratedArtifacts 570 | **/*.Server/ModelManifest.xml 571 | _Pvt_Extensions 572 | 573 | # Paket dependency manager 574 | .paket/paket.exe 575 | paket-files/ 576 | 577 | # FAKE - F# Make 578 | .fake/ 579 | 580 | # CodeRush personal settings 581 | .cr/personal 582 | 583 | # Python Tools for Visual Studio (PTVS) 584 | *.pyc 585 | 586 | # Cake - Uncomment if you are using it 587 | # tools/** 588 | # !tools/packages.config 589 | 590 | # Tabs Studio 591 | *.tss 592 | 593 | # Telerik's JustMock configuration file 594 | *.jmconfig 595 | 596 | # BizTalk build output 597 | *.btp.cs 598 | *.btm.cs 599 | *.odx.cs 600 | *.xsd.cs 601 | 602 | # OpenCover UI analysis results 603 | OpenCover/ 604 | 605 | # Azure Stream Analytics local run output 606 | ASALocalRun/ 607 | 608 | # MSBuild Binary and Structured Log 609 | *.binlog 610 | 611 | # NVidia Nsight GPU debugger configuration file 612 | *.nvuser 613 | 614 | # MFractors (Xamarin productivity tool) working folder 615 | .mfractor/ 616 | 617 | # Local History for Visual Studio 618 | .localhistory/ 619 | 620 | # BeatPulse healthcheck temp database 621 | healthchecksdb 622 | 623 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 624 | MigrationBackup/ 625 | 626 | # Ionide (cross platform F# VS Code tools) working folder 627 | .ionide/ 628 | 629 | # Fody - auto-generated XML schema 630 | FodyWeavers.xsd 631 | 632 | ### Linux template 633 | 634 | # temporary files which can be created if a process still has a handle open of a deleted file 635 | .fuse_hidden* 636 | 637 | # KDE directory preferences 638 | .directory 639 | 640 | # Linux trash folder which might appear on any partition or disk 641 | .Trash-* 642 | 643 | # .nfs files are created when an open file is removed but is still being accessed 644 | .nfs* 645 | 646 | ### Vim template 647 | # Swap 648 | [._]*.s[a-v][a-z] 649 | !*.svg # comment out if you don't need vector files 650 | [._]*.sw[a-p] 651 | [._]s[a-rt-v][a-z] 652 | [._]ss[a-gi-z] 653 | [._]sw[a-p] 654 | 655 | # Session 656 | Session.vim 657 | Sessionx.vim 658 | 659 | # Temporary 660 | .netrwhist 661 | # Auto-generated tag files 662 | tags 663 | # Persistent undo 664 | [._]*.un~ 665 | 666 | ### VirtualEnv template 667 | # Virtualenv 668 | # http://iamzed.com/2009/05/07/a-primer-on-virtualenv/ 669 | [Bb]in 670 | [Ii]nclude 671 | [Ll]ib 672 | [Ll]ib64 673 | [Ll]ocal 674 | [Ss]cripts 675 | pyvenv.cfg 676 | pip-selfcheck.json 677 | 678 | ### JetBrains template 679 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider 680 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 681 | 682 | # User-specific stuff 683 | .idea/**/workspace.xml 684 | .idea/**/tasks.xml 685 | .idea/**/usage.statistics.xml 686 | .idea/**/dictionaries 687 | .idea/**/shelf 688 | 689 | # Generated files 690 | .idea/**/contentModel.xml 691 | 692 | # Sensitive or high-churn files 693 | .idea/**/dataSources/ 694 | .idea/**/dataSources.ids 695 | .idea/**/dataSources.local.xml 696 | .idea/**/sqlDataSources.xml 697 | .idea/**/dynamic.xml 698 | .idea/**/uiDesigner.xml 699 | .idea/**/dbnavigator.xml 700 | 701 | # Gradle 702 | .idea/**/gradle.xml 703 | .idea/**/libraries 704 | 705 | # Gradle and Maven with auto-import 706 | # When using Gradle or Maven with auto-import, you should exclude module files, 707 | # since they will be recreated, and may cause churn. Uncomment if using 708 | # auto-import. 709 | # .idea/artifacts 710 | # .idea/compiler.xml 711 | # .idea/jarRepositories.xml 712 | # .idea/modules.xml 713 | # .idea/*.iml 714 | # .idea/modules 715 | # *.iml 716 | # *.ipr 717 | 718 | # CMake 719 | cmake-build-*/ 720 | 721 | # Mongo Explorer plugin 722 | .idea/**/mongoSettings.xml 723 | 724 | # File-based project format 725 | *.iws 726 | 727 | # IntelliJ 728 | out/ 729 | 730 | # mpeltonen/sbt-idea plugin 731 | .idea_modules/ 732 | 733 | # JIRA plugin 734 | atlassian-ide-plugin.xml 735 | 736 | # Cursive Clojure plugin 737 | .idea/replstate.xml 738 | 739 | # Crashlytics plugin (for Android Studio and IntelliJ) 740 | com_crashlytics_export_strings.xml 741 | crashlytics.properties 742 | crashlytics-build.properties 743 | fabric.properties 744 | 745 | # Editor-based Rest Client 746 | .idea/httpRequests 747 | 748 | # Android studio 3.1+ serialized cache file 749 | .idea/caches/build_file_checksums.ser 750 | 751 | ### Xcode template 752 | # Xcode 753 | # 754 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 755 | 756 | ## User settings 757 | xcuserdata/ 758 | 759 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 760 | *.xcscmblueprint 761 | *.xccheckout 762 | 763 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 764 | DerivedData/ 765 | *.moved-aside 766 | *.pbxuser 767 | !default.pbxuser 768 | *.mode1v3 769 | !default.mode1v3 770 | *.mode2v3 771 | !default.mode2v3 772 | *.perspectivev3 773 | !default.perspectivev3 774 | 775 | ## Gcc Patch 776 | /*.gcno 777 | 778 | ### Windows template 779 | # Windows thumbnail cache files 780 | Thumbs.db 781 | Thumbs.db:encryptable 782 | ehthumbs.db 783 | ehthumbs_vista.db 784 | 785 | # Dump file 786 | *.stackdump 787 | 788 | # Folder config file 789 | [Dd]esktop.ini 790 | 791 | # Recycle Bin used on file shares 792 | $RECYCLE.BIN/ 793 | 794 | # Windows Installer files 795 | *.cab 796 | *.msi 797 | *.msix 798 | *.msm 799 | *.msp 800 | *.bat 801 | 802 | # Windows shortcuts 803 | *.lnk 804 | 805 | ### VisualStudioCode template 806 | .vscode/* 807 | !.vscode/settings.json 808 | !.vscode/tasks.json 809 | !.vscode/launch.json 810 | !.vscode/extensions.json 811 | *.code-workspace 812 | 813 | # Local History for Visual Studio Code 814 | .history/ 815 | 816 | /.idea/ 817 | 818 | ./pytextnow/user_cookies.json -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 WuGomezCode 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. -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include pytextnow/user_cookies.json 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TextNow API 2 | ### TNAPI is a python module that uses [TextNow](https://www.textnow.com/) to enable free programmable texting 3 | 4 | # THIS PROJECT IS NO LONGER BEING MAINTAINED 5 | 6 | ## Credit 7 | - Developer: Leonardo Wu-Gomez 8 | - Please tell me if you have any ideas for the API or reporting a bug 9 | 10 | ## Installation 11 | #### Method One: ***Using git clone*** 12 | ```bash 13 | git clone https://github.com/WuGomezCode/TextNow-API.git 14 | ``` 15 | #### Method Two: ***Using pip*** 16 | ```bash 17 | pip install PyTextNow 18 | ``` 19 | *Note: If there is an unexplained error with the pip install, try adding the **--user** flag to it.* 20 | 21 | 22 | 23 | ## Usage 24 | 25 | ### How to get the cookie 26 | [How to get the cookie](get_cookie.mp4) 27 | 28 | ### How to get your username 29 | [How to get TextNow username](get_username.mp4) 30 | 31 | ### Ways to authenticate 32 | ```python 33 | import pytextnow 34 | 35 | # Way 1. Include connect.sid and csrf cookie in the constructor 36 | client = pytextnow.Client("username", sid_cookie="sid", csrf_cookie="csrf"). 37 | 38 | # Way 2. Just instantiate and a prompt will appear on the command line 39 | 40 | # Way 3. If you inputed the wrong cookie and are getting RequestFailed. This is how to reset it 41 | client.auth_reset() 42 | # will redo the prompt 43 | ``` 44 | 45 | ### How to send an sms message 46 | ```python 47 | client.send_sms("number", "Hello World!") 48 | ``` 49 | ### How to send an mms message 50 | ```python 51 | file_path = "./img.jpeg" 52 | client.send_mms("number", file_path) 53 | ``` 54 | ### How to get new messages 55 | ```python 56 | new_messages = client.get_unread_messages() -> MessageContainer list 57 | for message in new_messages: 58 | message.mark_as_read() 59 | print(message) 60 | # Class Message | Class MultiMediaMessage 61 | # Message 62 | # content: "body of sms" 63 | # number: "number of sender" 64 | # date: datetime object of when the message was received 65 | # read: bool 66 | # id: int 67 | # direction: SENT_MESSAGE_TYPE or RECEIVED_MESSAGE_TYPE 68 | # first_contact: bool if its the first time that number texted you 69 | # type: MESSAGE_TYPE if class is Message and MULTIMEDIAMESSAGE_TYPE if class is MultiMediaMessage 70 | 71 | # Functions 72 | # mark_as_read() will post the server as read 73 | # send_sms() will send an sms to the number who sent the message 74 | # send_mms() will send an mms to the number who sent the message 75 | 76 | # MultiMediaMessage 77 | # All the attributes of Message 78 | # content: url of media 79 | # raw_data: bytes of the media 80 | # content_type: str the MIME type ie. "image/jpeg" or "video/mp4" 81 | # extension: str of the file extension is. "jpeg" or "mp4" 82 | # Functions 83 | # mv(file_path): downloads the file to file_path 84 | 85 | print(message.number) 86 | print(message.content) 87 | 88 | # MultiMediaMessage 89 | 90 | print(message.content_type) 91 | message.mv("./image." + message.extension) 92 | 93 | ``` 94 | ### How to get all messages 95 | ```python 96 | messages = client.get_messages() -> MessageContainer list 97 | # Same as above 98 | ``` 99 | ### How to get all sent messages 100 | ```python 101 | sent_messages = client.get_sent_messages() -> MessageContainer list 102 | #Same as above 103 | ``` 104 | 105 | ### How to filter messages 106 | ```python 107 | filtered = client.get_messages().get(number="number") 108 | ``` 109 | 110 | ### How to synchronously block until a response 111 | ```python 112 | # This will wait for a response from someone and return the Message 113 | 114 | msg = client.wait_for_response("number") 115 | 116 | # This function will work with a message object too 117 | 118 | unreads = client.get_unread_messages() 119 | for unread in unreads: 120 | msg = unread.wait_for_response() 121 | ``` 122 | 123 | ## NEW Simple bot snippet 124 | ```python 125 | import pytextnow as pytn 126 | 127 | client = pytn.Client("username", sid_cookie="connect.sid", csrf_token="_csrf") 128 | 129 | @client.on("message") 130 | def handler(msg): 131 | print(msg) 132 | if msg.type == pytn.MESSAGE_TYPE: 133 | if msg.content == "ping": 134 | msg.send_sms("pong") 135 | else: 136 | msg.mv("test" + msg.extension) 137 | ``` 138 | 139 | ## Custom Module Exceptions 140 | 141 | ### FailedRequest: 142 | #### This API runs on web requests and if the request fails this Exception will be raised 143 | 144 | ### AuthError: 145 | #### During an auth reset if a cookie is not passed and there is no stored cookie in the file it will raise this error. 146 | 147 | 148 | ## Patch Notes 149 | 150 | ### 1.2.1 151 | - Fixed MultiMediaMessage causing error 152 | - Removed user_cookies.json 153 | 154 | ### 1.2.0 155 | - Bug fixes 156 | - Added pauses after requests 157 | - Fixed newline bug 158 | - Fixed login bug 159 | 160 | ### 1.1.9 161 | - Updated MANIFEST 162 | 163 | ### 1.1.8 164 | - Fixed 'Messgage not sending' error 165 | - Added new required cookie 166 | - `csrf_token` header is automatically fetched 167 | 168 | ### 1.1.7 169 | - Added get_username.mp4 video 170 | - Changed Client system from email to username. You now input your textnow username instead of email. 171 | - Bug fixes 172 | 173 | ### 1.1.6 174 | - Bug Fixes 175 | 176 | ### 1.1.5 177 | - New better way of getting new messages with Client.on method 178 | - Client.on works like an event handler that takes a decorator function and calls it with the parameter of one Message object 179 | - Bug Fixes 180 | - Added examples 181 | - Get cookie.mp4 video 182 | - Smarter cookie detection 183 | 184 | ### 1.1.4 185 | - bug fixes 186 | 187 | ### 1.1.3 188 | - bug fixes 189 | 190 | ### 1.1.2 191 | - bug fixes 192 | 193 | ### 1.1.1 194 | - Import Bug Fixes 195 | 196 | ## 1.1.0 197 | - bug fixes 198 | - if a cookie argument is passed to `Client` it will overide the stored cookie. 199 | - cookie argument can now be passed to `client.auth_reset()` 200 | - Changed import name from `TNAPI` to `pytextnow` 201 | ```python 202 | #Pre 1.1.0 203 | import TNAPI as tn 204 | # Now 205 | import pytextnow as pytn 206 | ``` 207 | 208 | ### 1.0.3 209 | - Bug fixes 210 | 211 | ### 1.0.2 212 | - `Client` has new function `client.wait_for_response(number, timeout=True)`. Documentation on how to use it above 213 | - `Message` has same function but the number argument is set to the number who sent the message. `client.Message.wait_for_response(timeout=True)` 214 | 215 | ### 1.0.1 216 | - Fixed config json.JSONDecodeError 217 | - new Class `MessageContainer` that acts as a list with some added functions and `__str__()` 218 | - `MessageContainer` has method `get` which will return a `MessageContainer` that filtered through all messages 219 | - Fixed readme.md Usage section. 220 | 221 | ## 1.0.0 222 | - Complete overhaul of the way this module works. 223 | 224 | - `client.get_new_messages()` is now deprecated and no longer in use. Instead of that use the new method `client.get_unread_messages()` which will return all unread messages. It will return the same thing each time unless you mark the messages as read with `Message.mark_as_read()` 225 | 226 | - `Message` and `MultiMediaMessage` class have a new `mark_as_read()` method to mark the message as read. `mark_as_read()` will make a POST to the textnow.com server. 227 | 228 | - `client.get_messages()` now returns a list of `Message` or `MultiMediaMessage` classes. For the old function which returned the raw dict use `client.get_raw_messages()` 229 | 230 | - `client.get_sent_messages()` is a new method that gets all messages you have sent 231 | 232 | - `client.get_received_messages()` is a new method that gets all messages you have received regardless of whether or not it has been read. 233 | 234 | - `client.get_read_messages()` is a new method that returns all messages that have been read by you. 235 | 236 | 237 | 238 | ### 0.9.8 239 | - Bug Fixes 240 | 241 | ### 0.9.7 242 | - Bug Fixes 243 | 244 | ### 0.9.6 245 | - Bug Fixes 246 | 247 | ### 0.9.5 248 | - Linux `__file__` not absolute. 249 | Used os.path.abspath 250 | 251 | ### 0.9.4 252 | - Bug fixes 253 | 254 | ### 0.9.3 255 | - Added constants such as 256 | - SENT_MESSAGE_TYPE 257 | - RECEIVED_MESSAGE_TYPE 258 | - MESSAGE_TYPE 259 | - MULTIMEDIAMESSAGE_TYPE 260 | - Fixed MultiMediaMessage.mv() function 261 | 262 | ### 0.9.2 263 | - No longer have to use selenium to authenticate. Now you have to manually grab connect.sid cookie. 264 | 265 | ### 0.9.1 266 | - Nothing much 267 | 268 | ## 0.9.0 269 | - Using Message and MultiMediaMessage classes instead of dictionary for /get_new_messages/get_sent_messages 270 | - get_messages still returns old dictionary 271 | - Fixed user_sids.json overwrite problem 272 | 273 | ## 0.8.0 274 | - Fixed the receiving messages. Now working 100% 275 | 276 | ## 0.7.0 277 | - Added FailedRequest and InvalidFileType errors to Client instance 278 | 279 | ## 0.5.0 280 | - bug fixes 281 | 282 | ## 0.4.0 283 | - Added `Client = TNAPI.Client` in \_\_init\_\_.py 284 | - Fixed the failed login import in TNAPI.py 285 | 286 | ## 0.3.0 287 | - Receiving messages are better but not good 288 | 289 | ## 0.2.0 290 | - Nothing much 291 | 292 | ## 0.1.0 293 | - Initial Commit 294 | - Can send messages and photos/videos 295 | - receiving messages a work in progress 296 | 297 | ## Contributing 298 | Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change. 299 | 300 | Please make sure to update tests as appropriate. 301 | 302 | ## License 303 | [MIT](https://choosealicense.com/licenses/mit/) 304 | -------------------------------------------------------------------------------- /examples/get_messages.py: -------------------------------------------------------------------------------- 1 | import pytextnow as pytn 2 | 3 | client = pytn.Client("username") # You can also include the cookie in ther Client constructor 4 | # Here you should input your connect.sid cookie 5 | 6 | messages = client.get_messages() 7 | 8 | for msg in messages: 9 | print(msg) -------------------------------------------------------------------------------- /examples/get_unread_messages.py: -------------------------------------------------------------------------------- 1 | import pytextnow as pytn 2 | 3 | client = pytn.Client("username") # You can also include the cookie in ther Client constructor 4 | # Here you should input your connect.sid cookie 5 | 6 | unreads = client.get_unread_messages() 7 | 8 | for msg in unreads: 9 | print(msg) -------------------------------------------------------------------------------- /examples/send_mms.py: -------------------------------------------------------------------------------- 1 | import pytextnow as pytn 2 | 3 | client = pytn.Client("username") # You can also include the cookie in ther Client constructor 4 | # Here you should input your connect.sid cookie 5 | 6 | client.send_mms("number", "filepath") 7 | -------------------------------------------------------------------------------- /examples/send_sms.py: -------------------------------------------------------------------------------- 1 | import pytextnow as pytn 2 | 3 | client = pytn.Client("username") # You can also include the cookie in ther Client constructor 4 | # Here you should input your connect.sid cookie 5 | 6 | client.send_sms("number", "text") 7 | -------------------------------------------------------------------------------- /examples/simple_bot.py: -------------------------------------------------------------------------------- 1 | import pytextnow as pytn 2 | 3 | client = pytn.Client("username") # You can also include the cookie in ther Client constructor 4 | # Here you should input your connect.sid cookie 5 | 6 | @client.on("message") 7 | def on_message(msg): 8 | if not msg.type == pytn.MESSAGE_TYPE: return 9 | 10 | if msg.content == "ping": 11 | msg.send_sms("pong") -------------------------------------------------------------------------------- /get_cookie.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leogomezz4t/PyTextNow_API/d54771ad1e52681a72e180d89db43f1ab975f854/get_cookie.mp4 -------------------------------------------------------------------------------- /get_username.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leogomezz4t/PyTextNow_API/d54771ad1e52681a72e180d89db43f1ab975f854/get_username.mp4 -------------------------------------------------------------------------------- /pytextnow/TNAPI.py: -------------------------------------------------------------------------------- 1 | if __name__ == "__main__": 2 | from login import login 3 | else: 4 | from pytextnow.login import login 5 | from pytextnow.error import FailedRequest, AuthError, InvalidEvent 6 | from pytextnow.message_container import MessageContainer 7 | from pytextnow.multi_media_message import MultiMediaMessage 8 | from pytextnow.message import Message 9 | import mimetypes 10 | import cloudscraper 11 | from datetime import datetime, time 12 | from dateutil.relativedelta import relativedelta 13 | 14 | import json 15 | from os.path import realpath, dirname, join 16 | import time 17 | import atexit 18 | import re 19 | 20 | MESSAGE_TYPE = 0 21 | MULTIMEDIA_MESSAGE_TYPE = 1 22 | 23 | SENT_MESSAGE_TYPE = 2 24 | RECEIVED_MESSAGE_TYPE = 1 25 | 26 | SIP_ENDPOINT = "prod.tncp.textnow.com" 27 | 28 | scraper = cloudscraper.create_scraper() 29 | 30 | class Client: 31 | def __init__(self, username: str = None, sid_cookie=None, csrf_cookie=None): 32 | self.username = username 33 | self.allowed_events = ["message"] 34 | 35 | self.events = [] 36 | self.cookies = {} 37 | 38 | sid,csrf = (sid_cookie,csrf_cookie) if sid_cookie and csrf_cookie else login() 39 | self.cookies = { 40 | 'connect.sid': sid, 41 | '_csrf': csrf, 42 | } 43 | self.headers = { 44 | 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) ' 45 | 'Chrome/88.0.4324.104 Safari/537.36 ', 46 | 'x-csrf-token': self.get_initial_csrf_token() 47 | } 48 | 49 | def on_exit(): 50 | if len(self.events) == 0: 51 | return 52 | 53 | while 1: 54 | for event, func in self.events: 55 | if event == "message": 56 | unread_msgs = self.get_unread_messages() 57 | for msg in unread_msgs: 58 | msg.mark_as_read() 59 | func(msg) 60 | time.sleep(1) 61 | 62 | atexit.register(on_exit) 63 | 64 | # Functions 65 | def _replace_newlines(self, text): 66 | return re.sub(r'(? timeout: 254 | unread_msgs = self.get_unread_messages() 255 | filtered = unread_msgs.get(number=number) 256 | if len(filtered) == 0: 257 | time.sleep(0.2) 258 | continue 259 | return filtered[0] 260 | 261 | def on(self, event: str): 262 | if event not in self.allowed_events: 263 | raise InvalidEvent(event) 264 | 265 | def deco(func): 266 | self.events.append([event, func]) 267 | 268 | return deco 269 | 270 | def request_handler(self, status_code: int): 271 | status_code = str(status_code) 272 | if status_code == '401': 273 | error = FailedRequest(status_code) 274 | print(error) 275 | 276 | self.auth_reset() 277 | return 278 | 279 | raise FailedRequest(status_code) 280 | -------------------------------------------------------------------------------- /pytextnow/__init__.py: -------------------------------------------------------------------------------- 1 | from pytextnow.TNAPI import * -------------------------------------------------------------------------------- /pytextnow/error.py: -------------------------------------------------------------------------------- 1 | # Custom Errors 2 | """ 3 | class InvalidFileType(Exception): 4 | def __init__(self, file_type): 5 | self.message = f"The file type {file_type} is not supported.\nThe only types supported are images and videos." 6 | 7 | def __str__(self): 8 | return self.message 9 | """ 10 | 11 | 12 | class FailedRequest(Exception): 13 | def __init__(self, status_code: str): 14 | self.status_code = status_code 15 | 16 | if status_code.startswith('3'): 17 | self.error_msg = "server redirected the request. Request Failed." 18 | elif status_code.startswith('4'): 19 | self.error_msg = "server returned a Client error. Request Failed. Try resetting your authentication with " \ 20 | "client.auth_reset() " 21 | elif status_code.startswith('5'): 22 | if status_code == "500": 23 | self.error_msg = "Internal Server Error. Request Failed." 24 | else: 25 | self.error_msg = "server return a Server error. Request Failed." 26 | 27 | def __str__(self): 28 | message = f'Could not send message. {self.error_msg}\nStatus Code: {self.status_code}' 29 | return message 30 | 31 | 32 | class AuthError(Exception): 33 | def __init__(self, reason): 34 | self.reason = reason 35 | 36 | def __str__(self): 37 | return self.reason 38 | 39 | 40 | class InvalidEvent(Exception): 41 | def __init__(self, event): 42 | self.event = event 43 | 44 | def __str__(self): 45 | return f"{self.event} is an invalid event." 46 | -------------------------------------------------------------------------------- /pytextnow/login.py: -------------------------------------------------------------------------------- 1 | # Functions 2 | def login(): 3 | print("Go to https://www.textnow.com/messaging and open developer tools") 4 | print("\n") 5 | print("Open application tab and copy connect.sid cookie and paste it here.") 6 | sid = input("connect.sid: ") 7 | print("Open application tab and copy _csrf cookie and paste it here.") 8 | csrf = input("_csrf: ") 9 | 10 | return sid, csrf 11 | -------------------------------------------------------------------------------- /pytextnow/message.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from dateutil.relativedelta import relativedelta 3 | from urllib.parse import quote 4 | 5 | import mimetypes 6 | import json 7 | import cloudscraper 8 | import time 9 | 10 | MESSAGE_TYPE = 0 11 | 12 | scraper = cloudscraper.create_scraper() 13 | 14 | class Message: 15 | def __init__(self, msg_obj, outer_self): 16 | self.content = msg_obj["message"] 17 | self.number = msg_obj["contact_value"] 18 | self.date = datetime.fromisoformat(msg_obj["date"].replace("Z", "+00:00")) 19 | self.first_contact = msg_obj["conversation_filtering"]["first_time_contact"] 20 | self.type = MESSAGE_TYPE 21 | self.read = msg_obj["read"] 22 | self.id = msg_obj["id"] 23 | self.direction = msg_obj["message_direction"] 24 | self.raw = msg_obj 25 | self.self = outer_self 26 | 27 | def __str__(self): 28 | class_name = self.__class__.__name__ 29 | s = f"<{class_name} number: {self.number}, content: {self.content}>" 30 | return s 31 | 32 | def send_mms(self, file): 33 | mime_type = mimetypes.guess_type(file)[0] 34 | file_type = mime_type.split("/")[0] 35 | has_video = True if file_type == "video" else False 36 | msg_type = 2 if file_type == "image" else 4 37 | 38 | file_url_holder_req = scraper.get("https://www.textnow.com/api/v3/attachment_url?message_type=2", 39 | cookies=self.self.cookies, headers=self.self.headers) 40 | if str(file_url_holder_req.status_code).startswith("2"): 41 | file_url_holder = json.loads(file_url_holder_req.text)["result"] 42 | 43 | with open(file, mode="br") as f: 44 | raw = f.read() 45 | 46 | headers_place_file = { 47 | 'accept': '*/*', 48 | 'content-type': mime_type, 49 | 'accept-language': 'en-US,en;q=0.9', 50 | "mode": "cors", 51 | "method": "PUT", 52 | "credentials": 'omit' 53 | } 54 | 55 | place_file_req = scraper.put(file_url_holder, data=raw, headers=headers_place_file, 56 | cookies=self.self.cookies) 57 | if str(place_file_req.status_code).startswith("2"): 58 | 59 | json_data = { 60 | "contact_value": self.number, 61 | "contact_type": 2, "read": 1, 62 | "message_direction": 2, "message_type": msg_type, 63 | "from_name": self.self.username, 64 | "has_video": has_video, 65 | "new": True, 66 | "date": datetime.now().isoformat(), 67 | "attachment_url": file_url_holder, 68 | "media_type": file_type 69 | } 70 | 71 | send_file_req = scraper.post("https://www.textnow.com/api/v3/send_attachment", data=json_data, 72 | headers=self.self.headers, cookies=self.self.cookies) 73 | time.sleep(1) 74 | return send_file_req 75 | else: 76 | raise self.self.FailedRequest(str(place_file_req.status_code)) 77 | else: 78 | raise self.self.FailedRequest(str(file_url_holder_req.status_code)) 79 | 80 | def send_sms(self, text): 81 | data = \ 82 | { 83 | 'json': '{"contact_value":"' + self.number 84 | + '","contact_type":2,"message":"' + text 85 | + '","read":1,"message_direction":2,"message_type":1,"from_name":"' 86 | + self.self.username + '","has_video":false,"new":true,"date":"' 87 | + datetime.now().isoformat() + '"}' 88 | } 89 | 90 | response = scraper.post('https://www.textnow.com/api/users/' + self.self.username + '/messages', 91 | headers=self.self.headers, cookies=self.self.cookies, data=data) 92 | if not str(response.status_code).startswith("2"): 93 | self.self.request_handler(response.status_code) 94 | time.sleep(1) 95 | return response 96 | 97 | def mark_as_read(self): 98 | self.patch({"read": True}) 99 | 100 | def patch(self, data): 101 | if not all(key in self.raw for key in data): 102 | return 103 | 104 | base_url = "https://www.textnow.com/api/users/" + self.self.username + "/conversations/" 105 | url = base_url + quote(self.number) 106 | 107 | params = { 108 | "latest_message_id": self.id, 109 | "http_method": "PATCH" 110 | } 111 | 112 | res = scraper.post(url, params=params, data=data, cookies=self.self.cookies, headers=self.self.headers) 113 | time.sleep(1) 114 | return res 115 | 116 | def wait_for_response(self, timeout_bool=True): 117 | self.mark_as_read() 118 | for msg in self.self.get_unread_messages(): 119 | msg.mark_as_read() 120 | timeout = datetime.now() + relativedelta(minute=10) 121 | if not timeout_bool: 122 | while 1: 123 | unread_msgs = self.self.get_unread_messages() 124 | filtered = unread_msgs.get(number=self.number) 125 | if len(filtered) == 0: 126 | time.sleep(0.2) 127 | continue 128 | return filtered[0] 129 | 130 | else: 131 | while datetime.now() > timeout: 132 | unread_msgs = self.self.get_unread_messages() 133 | filtered = unread_msgs.get(number=self.number) 134 | if len(filtered) == 0: 135 | time.sleep(0.2) 136 | continue 137 | return filtered[0] 138 | -------------------------------------------------------------------------------- /pytextnow/message_container.py: -------------------------------------------------------------------------------- 1 | class MessageContainer(list): 2 | def __init__(self, msg_list: list, outer_self): 3 | super().__init__(msg_list) 4 | self.msg_list = msg_list 5 | self.outer_self = outer_self 6 | 7 | def __str__(self): 8 | ss = [msg.__str__() for msg in self.msg_list] 9 | s = '[' + "\n".join(ss) + ']' 10 | return s 11 | 12 | def get(self, **kwargs): 13 | filtered_list = [] 14 | for msg in self.msg_list: 15 | if all(key in msg.__dict__.keys() for key in kwargs): 16 | if all(getattr(msg, key) == val for key, val in msg.__dict__.items()): 17 | filtered_list.append(msg) 18 | 19 | return self.outer_self.MessageContainer(filtered_list, self.outer_self) 20 | -------------------------------------------------------------------------------- /pytextnow/multi_media_message.py: -------------------------------------------------------------------------------- 1 | from pytextnow.message import Message 2 | import cloudscraper 3 | 4 | MULTIMEDIA_MESSAGE_TYPE = 1 5 | 6 | scraper = cloudscraper.create_scraper() 7 | 8 | from pytextnow.message import Message 9 | import requests 10 | 11 | from datetime import datetime 12 | from dateutil.relativedelta import relativedelta 13 | from urllib.parse import quote 14 | import mimetypes 15 | import json 16 | import time 17 | 18 | MESSAGE_TYPE = 0 19 | 20 | MULTIMEDIA_MESSAGE_TYPE = 1 21 | 22 | 23 | class MultiMediaMessage(Message): 24 | def __init__(self, msg_obj, outer_self): 25 | super().__init__(msg_obj, outer_self) 26 | try: 27 | file_req = requests.get(self.content) 28 | self.raw_data = file_req.content 29 | self.content_type = file_req.headers["Content-Type"] 30 | self.extension = self.content_type.split("/")[1] 31 | self.type = MULTIMEDIA_MESSAGE_TYPE 32 | self.self = outer_self 33 | except: 34 | self.content = msg_obj["message"] 35 | self.number = msg_obj["contact_value"] 36 | self.date = datetime.fromisoformat(msg_obj["date"].replace("Z", "+00:00")) 37 | self.first_contact = msg_obj["conversation_filtering"]["first_time_contact"] 38 | self.type = MESSAGE_TYPE 39 | self.read = msg_obj["read"] 40 | self.id = msg_obj["id"] 41 | self.direction = msg_obj["message_direction"] 42 | self.raw = msg_obj 43 | self.self = outer_self 44 | 45 | 46 | def mv(self, file_path=None): 47 | if not file_path: 48 | file_path = f"./file.{self.extension}" 49 | with open(file_path, mode="wb") as f: 50 | f.write(self.raw_data) 51 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | requests~=2.25.1 2 | setuptools~=57.0.0 3 | python-dateutil~=2.8.1 4 | mimetypes 5 | cloudscraper~=1.2.58 -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | import os 3 | 4 | with open("README.md", "r", encoding="utf-8") as fh: 5 | long_description = fh.read() 6 | 7 | 8 | def dependencies(): 9 | project_path = os.path.dirname(os.path.realpath(__file__)) 10 | requirement_path = os.path.join(project_path, 'requirements.txt') 11 | install_requires = [] 12 | 13 | if os.path.isfile(requirement_path): 14 | with open(requirement_path) as f: 15 | install_requires = f.read().splitlines() 16 | 17 | return install_requires 18 | 19 | 20 | setuptools.setup( 21 | name="PyTextNow", 22 | version="1.2.1", 23 | author="Leo Wu-Gomez", 24 | author_email="leojwu18@gmail.com", 25 | description="Texting python package which utilizes TextNow.", 26 | long_description=long_description, 27 | long_description_content_type="text/markdown", 28 | url="https://github.com/leogomezz4t/PyTextNow_API", 29 | packages=setuptools.find_packages(), 30 | classifiers=[ 31 | "Programming Language :: Python :: 3", 32 | "License :: OSI Approved :: MIT License", 33 | "Operating System :: OS Independent", 34 | ], 35 | python_requires='>=3.6', 36 | include_package_data=True, 37 | install_requires=dependencies(), 38 | ) 39 | --------------------------------------------------------------------------------