[
  {
    "path": ".bumpversion.cfg",
    "content": "[bumpversion]\ncommit = True\ntag = True\ncurrent_version = 0.6.4\n\n[bumpversion:file:pyproject.toml]\n\n[bumpversion:file:tiktok_bot/__init__.py]\n\n[bumpversion:file:tests/test_tiktok-bot.py]\n\n"
  },
  {
    "path": ".flake8",
    "content": "[flake8]\nmax-line-length = 100\nignore = C812,W503\n"
  },
  {
    "path": ".github/FUNDING.yml",
    "content": "# These are supported funding model platforms\n\ngithub: [sudoguy]\npatreon: # Replace with a single Patreon username\nopen_collective: # Replace with a single Open Collective username\nko_fi: # Replace with a single Ko-fi username\ntidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel\ncommunity_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry\nliberapay: # Replace with a single Liberapay username\nissuehunt: # Replace with a single IssueHunt username\notechie: # Replace with a single Otechie username\ncustom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']\n"
  },
  {
    "path": ".github/workflows/pythonpackage.yml",
    "content": "name: Main\n\non: [push, pull_request]\n\njobs:\n  Linting:\n    runs-on: ubuntu-latest\n\n    steps:\n      - uses: actions/checkout@v1\n      - name: Set up Python 3.7\n        uses: actions/setup-python@v1\n        with:\n          python-version: 3.7\n      - name: Linting\n        run: |\n          pip install pre-commit\n          pre-commit run --all-files\n\n  Linux:\n    needs: Linting\n    runs-on: ubuntu-latest\n    strategy:\n      matrix:\n        python-version: [3.6, 3.7]\n\n    steps:\n    - uses: actions/checkout@v1\n    - name: Set up Python ${{ matrix.python-version }}\n      uses: actions/setup-python@v1\n      with:\n        python-version: ${{ matrix.python-version }}\n    - name: Install Poetry\n      run: |\n        PATH=$PATH:/home/runner/.local/bin\n\n        pip install --pre --user poetry\n\n        poetry config virtualenvs.create false\n        poetry shell\n    - name: Install dependencies\n      run: |\n        PATH=$PATH:/home/runner/.local/bin\n\n        poetry install\n    - name: Test\n      run: |\n        PATH=$PATH:/home/runner/.local/bin\n\n        poetry run pytest -q tests\n\n  MacOS:\n    needs: Linting\n    runs-on: macos-latest\n    strategy:\n      matrix:\n        python-version: [3.6, 3.7]\n\n    steps:\n    - uses: actions/checkout@v1\n    - name: Set up Python ${{ matrix.python-version }}\n      uses: actions/setup-python@v1\n      with:\n        python-version: ${{ matrix.python-version }}\n    - name: Install Poetry\n      run: |\n        PATH=$PATH:/Users/runner/.local/bin\n\n        pip install --pre --user poetry\n\n        poetry config virtualenvs.create false\n        poetry shell\n    - name: Install dependencies\n      run: |\n        PATH=$PATH:/Users/runner/.local/bin\n\n        poetry install\n    - name: Test\n      run: |\n        PATH=$PATH:/Users/runner/.local/bin\n\n        poetry run pytest -q tests\n\n  Windows:\n    needs: Linting\n    runs-on: windows-latest\n    strategy:\n      matrix:\n        python-version: [3.6, 3.7]\n\n    steps:\n    - uses: actions/checkout@v1\n    - name: Set up Python ${{ matrix.python-version }}\n      uses: actions/setup-python@v1\n      with:\n        python-version: ${{ matrix.python-version }}\n    - name: Install Poetry\n      run: |\n        pip install --pre --user poetry\n        poetry shell\n    - name: Install dependencies\n      run: |\n        poetry install\n    - name: Test\n      run: |\n        poetry run pytest -q tests\n"
  },
  {
    "path": ".gitignore",
    "content": "# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\n*$py.class\n\n# C extensions\n*.so\n\n# Distribution / packaging\n.Python\nbuild/\ndevelop-eggs/\ndist/\ndownloads/\neggs/\n.eggs/\nlib/\nlib64/\nparts/\nsdist/\nvar/\nwheels/\n*.egg-info/\n.installed.cfg\n*.egg\nMANIFEST\n\n# PyInstaller\n#  Usually these files are written by a python script from a template\n#  before PyInstaller builds the exe, so as to inject date/other infos into it.\n*.manifest\n*.spec\n\n# Installer logs\npip-log.txt\npip-delete-this-directory.txt\n\n# Unit test / coverage reports\nhtmlcov/\n.tox/\n.coverage\n.coverage.*\n.cache\nnosetests.xml\ncoverage.xml\n*.cover\n.hypothesis/\n.pytest_cache/\n\n# Translations\n*.mo\n*.pot\n\n# Django stuff:\n*.log\nlocal_settings.py\ndb.sqlite3\n\n# Flask stuff:\ninstance/\n.webassets-cache\n\n# Scrapy stuff:\n.scrapy\n\n# Sphinx documentation\ndocs/_build/\n\n# PyBuilder\ntarget/\n\n# Jupyter Notebook\n.ipynb_checkpoints\n\n# pyenv\n.python-version\n\n# celery beat schedule file\ncelerybeat-schedule\n\n# SageMath parsed files\n*.sage.py\n\n# Environments\n.env\n.venv\nenv/\nvenv/\nENV/\nenv.bak/\nvenv.bak/\n\n# Spyder project settings\n.spyderproject\n.spyproject\n\n# Rope project settings\n.ropeproject\n\n# mkdocs documentation\n/site\n\n# mypy\n.mypy_cache/\n\n# VS Code\n.vscode\n"
  },
  {
    "path": ".isort.cfg",
    "content": "[settings]\nline_length=100\nindent=4\n\nmulti_line_output=3\ninclude_trailing_comma=True\n\nknown_first_party=tiktok_bot\nknown_third_party = httpx,loguru,pydantic,pytest,tqdm,typing_extensions\n"
  },
  {
    "path": ".pre-commit-config.yaml",
    "content": "repos:\n- repo: https://github.com/pre-commit/pre-commit-hooks\n  rev: v2.4.0\n  hooks:\n    - id: check-added-large-files\n    - id: debug-statements\n    - id: end-of-file-fixer\n      exclude: '.bumpversion.cfg'\n    - id: check-symlinks\n    - id: trailing-whitespace\n    - id: mixed-line-ending\n      args: ['--fix=lf']\n\n- repo: https://github.com/humitos/mirrors-autoflake\n  rev: v1.1\n  hooks:\n    - id: autoflake\n      args: ['--in-place', '--remove-all-unused-imports', '--remove-unused-variable']\n\n- repo: https://github.com/asottile/seed-isort-config\n  rev: v1.9.3\n  hooks:\n  - id: seed-isort-config\n- repo: https://github.com/pre-commit/mirrors-isort\n  rev: v4.3.21\n  hooks:\n  - id: isort\n\n- repo: https://github.com/psf/black\n  rev: 19.10b0\n  hooks:\n  - id: black\n    files: '\\.py$'\n\n- repo: https://github.com/pre-commit/pre-commit-hooks\n  rev: v2.4.0\n  hooks:\n    - id: flake8\n      additional_dependencies: [\n      'flake8-blind-except',\n      'flake8-commas',\n      'flake8-comprehensions',\n      'flake8-deprecated',\n      'flake8-mutable',\n      'flake8-tidy-imports',\n      'flake8-print',\n      ]\n"
  },
  {
    "path": ".travis.yml",
    "content": "language: python\n\nos:\n  - linux\n\nstages:\n  - lint\n  - test\n  - name: deploy\n    if: tag IS present\n\npython:\n  - \"3.6\"\n  - \"3.7\"\n  - \"3.8\"\n  - \"nightly\"\n  - \"pypy3\"\n\ninstall:\n  - pip install --upgrade pip\n\nscript: skip\n\njobs:\n  include:\n    - stage: lint\n      install:\n        - pip install pre-commit\n        - pre-commit install-hooks\n      script:\n        - pre-commit run --all-files\n    - stage: test\n      install:\n        - pip install --pre poetry\n        - poetry install -v\n      script:\n        - poetry run pytest\n    - stage: deploy\n      script:\n        - echo Deploying to PyPI...\n\nbefore_deploy:\n  - pip install --upgrade pip\n  - pip install poetry\n  - poetry config http-basic.pypi $PYPI_USER $PYPI_PASSWORD\n  - poetry build\n\ndeploy:\n  provider: script\n  script: poetry publish\n  skip_cleanup: true\n  on:\n    all_branches: true  # Travis recognizes tag names as \"branches\"\n    condition: $TRAVIS_BUILD_STAGE_NAME = deploy\n    repo: sudoguy/tiktok_bot\n    tags: true\n\nnotifications:\n  email:\n    on_success: change\n    on_failure: always\n\nbefore_cache:\n  - rm -f $HOME/.cache/pip/log/debug.log\n\ncache:\n  pip: true\n  directories:\n    - \"$HOME/.cache/pre-commit\"\n    - .venv\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "# Changelog\n\nAll notable changes to this project will be documented in this file.\n\nThe format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).\n\n## 0.6.1 (November 17, 2019)\n\n### Added\n\n- Changelog\n\n## 0.6.0 (November 17, 2019)\n\n### Added\n\n- Method for searches posts(videos) by hashtag name - `search_posts_by_hashtag`\n\n### Fixed\n\n- Set some fields in `music` and `post` models as `Optional`\n"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "content": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nIn the interest of fostering an open and welcoming environment, we as\ncontributors and maintainers pledge to making participation in our project and\nour community a harassment-free experience for everyone, regardless of age, body\nsize, disability, ethnicity, sex characteristics, gender identity and expression,\nlevel of experience, education, socio-economic status, nationality, personal\nappearance, race, religion, or sexual identity and orientation.\n\n## Our Standards\n\nExamples of behavior that contributes to creating a positive environment\ninclude:\n\n* Using welcoming and inclusive language\n* Being respectful of differing viewpoints and experiences\n* Gracefully accepting constructive criticism\n* Focusing on what is best for the community\n* Showing empathy towards other community members\n\nExamples of unacceptable behavior by participants include:\n\n* The use of sexualized language or imagery and unwelcome sexual attention or\n advances\n* Trolling, insulting/derogatory comments, and personal or political attacks\n* Public or private harassment\n* Publishing others' private information, such as a physical or electronic\n address, without explicit permission\n* Other conduct which could reasonably be considered inappropriate in a\n professional setting\n\n## Our Responsibilities\n\nProject maintainers are responsible for clarifying the standards of acceptable\nbehavior and are expected to take appropriate and fair corrective action in\nresponse to any instances of unacceptable behavior.\n\nProject maintainers have the right and responsibility to remove, edit, or\nreject comments, commits, code, wiki edits, issues, and other contributions\nthat are not aligned to this Code of Conduct, or to ban temporarily or\npermanently any contributor for other behaviors that they deem inappropriate,\nthreatening, offensive, or harmful.\n\n## Scope\n\nThis Code of Conduct applies both within project spaces and in public spaces\nwhen an individual is representing the project or its community. Examples of\nrepresenting a project or community include using an official project e-mail\naddress, posting via an official social media account, or acting as an appointed\nrepresentative at an online or offline event. Representation of a project may be\nfurther defined and clarified by project maintainers.\n\n## Enforcement\n\nInstances of abusive, harassing, or otherwise unacceptable behavior may be\nreported by contacting the project team at eskemerov@gmail.com. All\ncomplaints will be reviewed and investigated and will result in a response that\nis deemed necessary and appropriate to the circumstances. The project team is\nobligated to maintain confidentiality with regard to the reporter of an incident.\nFurther details of specific enforcement policies may be posted separately.\n\nProject maintainers who do not follow or enforce the Code of Conduct in good\nfaith may face temporary or permanent repercussions as determined by other\nmembers of the project's leadership.\n\n## Attribution\n\nThis Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,\navailable at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html\n\n[homepage]: https://www.contributor-covenant.org\n\nFor answers to common questions about this code of conduct, see\nhttps://www.contributor-covenant.org/faq\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2019 Evgeny Kemerov\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# This project is no longer under development.\n# A NEW project is being developed here: [sudoguy/tiktokpy](https://github.com/sudoguy/tiktokpy/)\n\n---\n\n<h1 align=\"center\" style=\"font-size: 3rem;\">\ntiktok-bot\n</h1>\n<p align=\"center\">\n <em>The most intelligent TikTok bot for Python.</em></p>\n\n<p align=\"center\">\n<a href=\"https://travis-ci.org/sudoguy/tiktok_bot\">\n    <img src=\"https://travis-ci.org/sudoguy/tiktok_bot.svg?branch=master\" alt=\"Build Status\">\n</a>\n<a href=\"https://pepy.tech/project/tiktok-bot\">\n    <img src=\"https://pepy.tech/badge/tiktok-bot\" alt=\"Downloads\">\n</a>\n<a href=\"https://pypi.org/project/tiktok-bot/\">\n    <img src=\"https://badge.fury.io/py/tiktok-bot.svg\" alt=\"Package version\">\n</a>\n</p>\n\n\n**Note**: *This project should be considered as an **\"alpha\"** release.*\n\n---\n\n## Quickstart\n\n```python\nfrom tiktok_bot import TikTokBot\n\nbot = TikTokBot()\n\n# getting your feed (list of posts)\nmy_feed = bot.list_for_you_feed(count=25)\n\npopular_posts = [post for post in my_feed if post.statistics.play_count > 1_000_000]\n\n# extract video urls without watermark (every post has helpers)\nurls = [post.video_url_without_watermark for post in popular_posts]\n\n# searching videos by hashtag name\nposts = bot.search_posts_by_hashtag(\"cat\", count=50)\n```\n\n## Installation\n\nInstall with pip:\n\n```shell\npip install tiktok-bot\n```\n\ntiktok-bot requires Python 3.6+\n"
  },
  {
    "path": "docs/css/custom.css",
    "content": "div.autodoc-docstring {\n  padding-left: 20px;\n  margin-bottom: 30px;\n  border-left: 5px solid rgba(230, 230, 230);\n}\n\ndiv.autodoc-members {\n  padding-left: 20px;\n  margin-bottom: 15px;\n}\n"
  },
  {
    "path": "docs/index.md",
    "content": "# Welcome to MkDocs\n\nFor full documentation visit [mkdocs.org](https://mkdocs.org).\n\n## Commands\n\n* `mkdocs new [dir-name]` - Create a new project.\n* `mkdocs serve` - Start the live-reloading docs server.\n* `mkdocs build` - Build the documentation site.\n* `mkdocs help` - Print this help message.\n\n## Project layout\n\n    mkdocs.yml    # The configuration file.\n    docs/\n        index.md  # The documentation homepage.\n        ...       # Other markdown pages, images and other files.\n"
  },
  {
    "path": "mkdocs.yml",
    "content": "site_name: TikTok Bot\nsite_description: Free TikTok bot for Python.\n\ntheme:\n    name: \"material\"\n\nrepo_name: sudoguy/tiktok_bot\nrepo_url: https://github.com/sudoguy/tiktok-bot\nedit_uri: \"\"\n\nnav:\n    - Introduction: 'index.md'\n\nmarkdown_extensions:\n  - admonition\n  - codehilite\n  - mkautodoc\n\nextra_css:\n    - css/custom.css\n"
  },
  {
    "path": "pyproject.toml",
    "content": "[tool.poetry]\nname = \"tiktok_bot\"\nversion = \"0.6.4\"\ndescription = \"Tik Tok API\"\n\nlicense = \"MIT\"\nauthors = [\"Evgeny Kemerov <eskemerov@gmail.com>\"]\n\nreadme = \"README.md\"\ninclude = [\"CHANGELOG.md\", \"LICENSE\"]\n\nrepository = \"https://github.com/sudoguy/tiktok_bot/\"\nhomepage = \"https://github.com/sudoguy/tiktok_bot/\"\n\nkeywords = [\"tiktok\", \"bot\", \"api\", \"wrapper\", \"tiktokbot\"]\n\nclassifiers = [\n    \"Development Status :: 3 - Alpha\",\n    \"Environment :: Console\",\n    \"Intended Audience :: Science/Research\",\n    \"Intended Audience :: Information Technology\",\n    \"Intended Audience :: End Users/Desktop\",\n    \"License :: OSI Approved :: MIT License\",\n    \"Operating System :: OS Independent\",\n    \"Topic :: Software Development :: Libraries :: Python Modules\",\n]\n\n[tool.poetry.dependencies]\npython = \"^3.6\"\nhttpx = \"^0.7.4\"\npydantic = \"^0.32.2\"\nloguru = \"^0.3.2\"\ntyping-extensions = \"^3.7.4\"\ntqdm = \"^4.38.0\"\n\n[tool.poetry.dev-dependencies]\npytest = \"^5.1.2\"\npytest-cov = \"^2.7.1\"\npytest-mock = \"^1.10.4\"\npre-commit = \"^1.18.3\"\npytest-sugar = \"^0.9.2\"\nblack = {version = \"^19.3b0\", allow-prereleases = true}\nipython = \"^7.8.0\"\nflake8 = \"^3.7.8\"\nflake8-blind-except = \"^0.1.1\"\nflake8-commas = \"^2.0.0\"\nflake8-comprehensions = \"^2.2.0\"\nflake8-deprecated = \"^1.3\"\nflake8-mutable = \"^1.2.0\"\nflake8-tidy-imports = \"^2.0.0\"\nflake8-print = \"^3.1.0\"\nisort = \"^4.3.21\"\nmypy = \"^0.740\"\nmkdocs = \"^1.0.4\"\nmkdocs-material = \"^4.4.3\"\nmkautodoc = \"^0.1.0\"\nbump2version = \"^0.5.11\"\n\n[tool.black]\nline-length = 100\ntarget-version = ['py36', 'py37', 'py38']\ninclude = '\\.pyi?$'\nexclude = '''\n/(\n    \\.eggs\n  | \\.git\n  | \\.hg\n  | \\.mypy_cache\n  | \\.tox\n  | \\.venv\n  | _build\n  | buck-out\n  | build\n  | dist\n)/\n'''\n[build-system]\nrequires = [\"poetry>=0.12\"]\nbuild-backend = \"poetry.masonry.api\"\n"
  },
  {
    "path": "tests/api/test_api.py",
    "content": "from tiktok_bot.api import TikTokAPI\n\n\ndef test_encrypt_with_xor(api: TikTokAPI):\n    assert api.encrypt_with_XOR(\"user@example.com\") == \"7076607745607d64687569602b666a68\"\n    assert api.encrypt_with_XOR(\"password\") == \"75647676726a7761\"\n"
  },
  {
    "path": "tests/conftest.py",
    "content": "import pytest\n\nfrom tiktok_bot.api import TikTokAPI\n\n\n@pytest.fixture()\ndef api():\n    return TikTokAPI()\n"
  },
  {
    "path": "tests/test_tiktok-bot.py",
    "content": "from tiktok_bot import __version__\n\n\ndef test_version():\n    assert __version__ == \"0.6.4\"\n"
  },
  {
    "path": "tiktok_bot/__init__.py",
    "content": "from .bot import TikTokBot\nfrom .client import client\n\n__all__ = [\"TikTokBot\", \"client\"]\n\n__version__ = \"0.6.4\"\n"
  },
  {
    "path": "tiktok_bot/api/__init__.py",
    "content": "from .api import TikTokAPI\n\n__all__ = [\"TikTokAPI\"]\n"
  },
  {
    "path": "tiktok_bot/api/api.py",
    "content": "import itertools\nfrom typing import List\n\nfrom loguru import logger\nfrom tqdm import tqdm\n\nfrom tiktok_bot.client import HTTPClient\nfrom tiktok_bot.models.category import ListCategoriesRequest, ListCategoriesResponse\nfrom tiktok_bot.models.feed import ListFeedRequest, ListFeedResponse, ListForYouFeedResponse\nfrom tiktok_bot.models.feed_enums import FeedType, PullType\nfrom tiktok_bot.models.follow import FollowRequest, FollowResponse\nfrom tiktok_bot.models.hashtag import ListPostsInHashtagRequest, ListPostsInHashtagResponse\nfrom tiktok_bot.models.login import LoginRequest, LoginResponse\nfrom tiktok_bot.models.post import Post\nfrom tiktok_bot.models.search import (\n    ChallengeInfo,\n    HashtagSearchResponse,\n    HashtagSearchResult,\n    SearchRequest,\n    UserSearchRequest,\n    UserSearchResponse,\n    UserSearchResult,\n)\nfrom tiktok_bot.models.user import UserProfileResponse\n\nfrom .config import DEFAULT_HEADERS, DEFAULT_PARAMS\n\n\nclass TikTokAPI:\n    def __init__(self):\n        self.client = HTTPClient(\n            base_url=\"https://api2.musical.ly/\",\n            default_headers=DEFAULT_HEADERS,\n            default_params=DEFAULT_PARAMS,\n        )\n\n    @staticmethod\n    def encrypt_with_XOR(value: str, key=5) -> str:\n        return \"\".join([hex(int(x ^ key))[2:] for x in value.encode(\"utf-8\")])\n\n    def login(self, login_request: LoginRequest) -> LoginResponse:\n        \"\"\" FIXME: Under development \"\"\"\n\n        url = \"passport/user/login/\"\n\n        response = self.client.post(url=url, data=login_request.dict())\n\n        login = LoginResponse(**response.json())\n\n        return login\n\n    def login_with_email(self, email: str, password: str, captcha: str = \"\"):\n        \"\"\" FIXME: Under development \"\"\"\n\n        email = self.encrypt_with_XOR(email)\n        password = self.encrypt_with_XOR(password)\n\n        request = LoginRequest(email=email, password=password, captcha=captcha)\n\n        return self.login(request)\n\n    def _list_for_you_feed(self, list_feed_request: ListFeedRequest) -> ListForYouFeedResponse:\n        \"Lists posts in the For You feed.\"\n\n        url = \"aweme/v1/feed/\"\n\n        response = self.client.get(url=url, params=list_feed_request.dict())\n\n        feed = ListForYouFeedResponse(**response.json())\n\n        return feed\n\n    def list_for_you_feed(self, count: int) -> List[Post]:\n        \"Lists posts in the For You feed with paginate.\"\n        feed: List[Post] = []\n\n        logger.info(f\"Getting {count} posts from your feed\")\n\n        with tqdm(total=count, desc=\"List for you feed\") as pbar:\n            for cursor in itertools.count(start=0, step=6):\n                request = ListFeedRequest(\n                    count=count,\n                    max_cursor=cursor,\n                    pull_type=PullType.LoadMore,\n                    type=FeedType.ForYou,\n                    is_cold_start=1,\n                )\n                response = self._list_for_you_feed(list_feed_request=request)\n\n                feed += response.aweme_list\n\n                pbar.update(len(response.aweme_list))\n\n                if not response.has_more or len(feed) >= count:\n                    feed = feed[:count]\n                    logger.info(f\"Found {len(feed)} results\")\n                    break\n\n        return feed\n\n    def list_following_feed(self, list_feed_request: ListFeedRequest) -> ListFeedResponse:\n        \"Lists posts in the Following feed.\"\n\n        url = \"aweme/v1/feed/\"\n\n        response = self.client.get(url=url, params=list_feed_request.dict())\n\n        feed = ListFeedResponse(**response.json())\n\n        return feed\n\n    def list_categories(\n        self, list_categories_request: ListCategoriesRequest\n    ) -> ListCategoriesResponse:\n        \"Lists popular categories/hashtags.\"\n\n        url = \"aweme/v1/category/list/\"\n\n        response = self.client.get(url=url, params=list_categories_request.dict())\n\n        categories = ListCategoriesResponse(**response.json())\n\n        return categories\n\n    def get_user(self, user_id: str) -> UserProfileResponse:\n        \"Gets a user's profile.\"\n\n        url = \"aweme/v1/user/\"\n\n        response = self.client.get(url=url, params={\"user_id\": user_id})\n\n        user = UserProfileResponse(**response.json())\n\n        return user\n\n    def _search_users(self, user_search_request: UserSearchRequest) -> UserSearchResponse:\n        \"Searches for users.\"\n\n        url = \"aweme/v1/discover/search/\"\n\n        response = self.client.get(url=url, params=user_search_request.dict())\n\n        user_search = UserSearchResponse(**response.json())\n\n        return user_search\n\n    def search_users(self, keyword: str, count: int) -> List[UserSearchResult]:\n        \"Searches for users with paginate.\"\n\n        results: List[UserSearchResult] = []\n\n        logger.info(f'Search {count} users with keyword: \"{keyword}\"')\n\n        with tqdm(total=count, desc=\"Searching users\") as pbar:\n            for cursor in itertools.count(start=0, step=10):\n                user_search_request = UserSearchRequest(keyword=keyword, cursor=cursor)\n                response = self._search_users(user_search_request=user_search_request)\n\n                results += response.user_list\n                pbar.update(len(response.user_list))\n\n                if not response.has_more or len(results) >= count:\n                    results = results[:count]\n                    logger.info(f\"Found {len(results)} results\")\n                    break\n\n        return results\n\n    def _search_posts_by_hashtag(\n        self, search_request: ListPostsInHashtagRequest\n    ) -> ListPostsInHashtagResponse:\n        \"Search posts by hashtag id.\"\n\n        url = \"aweme/v1/challenge/aweme/\"\n\n        response = self.client.get(url=url, params=search_request.dict())\n\n        search = ListPostsInHashtagResponse(**response.json())\n\n        return search\n\n    def search_posts_by_hashtag(self, hashtag: ChallengeInfo, count: int) -> List[Post]:\n        \"Search posts by hashtag with paginate.\"\n\n        results: List[Post] = []\n\n        logger.info(f'Search {count} posts with hashtag: \"{hashtag.cha_name}\"')\n\n        with tqdm(total=count, desc=\"Searching posts\") as pbar:\n            for cursor in itertools.count(start=0, step=10):\n                search_request = ListPostsInHashtagRequest(ch_id=hashtag.cid, cursor=cursor)\n                response = self._search_posts_by_hashtag(search_request)\n\n                results += response.aweme_list\n                pbar.update(len(response.aweme_list))\n\n                if not response.has_more or len(results) >= count:\n                    results = results[:count]\n                    logger.info(f\"Found {len(results)} results\")\n                    break\n\n        return results\n\n    def _search_hashtags(self, hashtag_search_request: SearchRequest) -> HashtagSearchResponse:\n        \"Searches for hashtags.\"\n\n        url = \"aweme/v1/challenge/search/\"\n\n        response = self.client.get(url=url, params=hashtag_search_request.dict())\n\n        hashtag_search = HashtagSearchResponse(**response.json())\n\n        return hashtag_search\n\n    def _follow(self, request: FollowRequest) -> FollowResponse:\n        \"Send follow request.\"\n\n        url = \"aweme/v1/commit/follow/user/\"\n\n        response = self.client.get(url=url, params=request.dict())\n\n        follow = FollowResponse(**response.json())\n\n        return follow\n\n    def search_hashtags(self, keyword: str, count: int) -> List[HashtagSearchResult]:\n        \"Searches for hashtags with paginate.\"\n\n        results: List[HashtagSearchResult] = []\n\n        logger.info(f'Search {count} hashtags with keyword: \"{keyword}\"')\n\n        with tqdm(total=count, desc=\"Searching hashtags\") as pbar:\n            for cursor in itertools.count(start=0, step=10):\n                search_request = SearchRequest(keyword=keyword, cursor=cursor)\n                response = self._search_hashtags(search_request)\n\n                results += response.challenge_list\n                pbar.update(len(response.challenge_list))\n\n                if not response.has_more or len(results) >= count:\n                    results = results[:count]\n                    logger.info(f\"Found {len(results)} results\")\n                    break\n\n        return results\n"
  },
  {
    "path": "tiktok_bot/api/config.py",
    "content": "# ToDo: Make it configurable\nDEFAULT_PARAMS = {\n    \"os_api\": \"23\",\n    \"device_type\": \"Pixel\",\n    \"ssmix\": \"a\",\n    \"manifest_version_code\": \"2018111632\",\n    \"dpi\": 420,\n    \"app_name\": \"musical_ly\",\n    \"version_name\": \"9.1.0\",\n    \"is_my_cn\": 0,\n    \"ac\": \"wifi\",\n    \"update_version_code\": \"2018111632\",\n    \"channel\": \"googleplay\",\n    \"device_platform\": \"android\",\n    \"build_number\": \"9.9.0\",\n    \"version_code\": 910,\n    \"timezone_name\": \"America/New_York\",\n    \"timezone_offset\": 36000,\n    \"resolution\": \"1080*1920\",\n    \"os_version\": \"7.1.2\",\n    \"device_brand\": \"Google\",\n    \"mcc_mnc\": \"23001\",\n    \"is_my_cn\": 0,\n    \"fp\": \"\",\n    \"app_language\": \"en\",\n    \"language\": \"en\",\n    \"region\": \"US\",\n    \"sys_region\": \"US\",\n    \"account_region\": \"US\",\n    \"carrier_region\": \"US\",\n    \"carrier_region_v2\": \"505\",\n    \"aid\": \"1233\",\n    \"pass-region\": 1,\n    \"pass-route\": 1,\n    \"app_type\": \"normal\",\n    # \"iid\": \"6742828344465966597\",\n    # \"device_id\": \"6746627788566021893\",\n    # ToDo: Make it dynamic\n    \"iid\": \"6749111388298184454\",\n    \"device_id\": \"6662384847253865990\",\n}\n\nDEFAULT_HEADERS = {\n    \"Host\": \"api2.musical.ly\",\n    \"X-SS-TC\": \"0\",\n    \"User-Agent\": f\"com.zhiliaoapp.musically/{DEFAULT_PARAMS['manifest_version_code']}\"\n    + f\" (Linux; U; Android {DEFAULT_PARAMS['os_version']};\"\n    + f\" {DEFAULT_PARAMS['language']}_{DEFAULT_PARAMS['region']};\"\n    + f\" {DEFAULT_PARAMS['device_type']};\"\n    + \" Build/NHG47Q; Cronet/58.0.2991.0)\",\n    \"Accept-Encoding\": \"gzip\",\n    \"Accept\": \"*/*\",\n    \"Connection\": \"keep-alive\",\n    \"X-Tt-Token\": \"\",\n    \"sdk-version\": \"1\",\n    \"Cookie\": \"null = 1;\",\n}\n"
  },
  {
    "path": "tiktok_bot/bot/__init__.py",
    "content": "from .bot import TikTokBot\n\n__all__ = [\"TikTokBot\"]\n"
  },
  {
    "path": "tiktok_bot/bot/bot.py",
    "content": "import sys\nfrom typing import List\n\nfrom loguru import logger\nfrom typing_extensions import Literal\n\nfrom tiktok_bot.api import TikTokAPI\nfrom tiktok_bot.models.category import Category, ListCategoriesRequest\nfrom tiktok_bot.models.feed import ListFeedRequest\nfrom tiktok_bot.models.feed_enums import FeedType, PullType\nfrom tiktok_bot.models.post import Post\nfrom tiktok_bot.models.search import ChallengeInfo\nfrom tiktok_bot.models.user import CommonUserDetails, UserProfile\n\n\nclass TikTokBot:\n    def __init__(self, log_level: Literal[\"INFO\", \"DEBUG\"] = \"INFO\"):\n        self.api = TikTokAPI()\n\n        logger.remove()\n        logger.add(sys.stderr, level=log_level)\n\n    def list_categories(self, count: int = 10, cursor: int = 0) -> List[Category]:\n        request = ListCategoriesRequest(count=count, cursor=cursor)\n        categories = self.api.list_categories(request)\n\n        return categories.category_list\n\n    def get_user_by_id(self, user_id: str) -> UserProfile:\n        user_response = self.api.get_user(user_id=user_id)\n\n        return user_response.user\n\n    def search_users(self, keyword: str, count: int = 6) -> List[CommonUserDetails]:\n        users_search = self.api.search_users(keyword=keyword, count=count)\n\n        users = [user.user_info for user in users_search]\n\n        return users\n\n    def search_hashtags(self, keyword: str, count: int = 6) -> List[ChallengeInfo]:\n        hashtags_search = self.api.search_hashtags(keyword=keyword, count=count)\n\n        hashtags = [tag.challenge_info for tag in hashtags_search]\n\n        return hashtags\n\n    def search_posts_by_hashtag(self, hashtag_name: str, count: int = 6) -> List[Post]:\n        tags = self.search_hashtags(keyword=hashtag_name, count=1)\n\n        if not tags:\n            logger.info(f'Tag \"{hashtag_name}\" not found')\n            return []\n\n        posts = self.api.search_posts_by_hashtag(hashtag=tags[0], count=count)\n\n        return posts\n\n    def list_for_you_feed(self, count: int = 6) -> List[Post]:\n        feed = self.api.list_for_you_feed(count=count)\n\n        return feed\n\n    def list_following_feed(self, count: int = 6, cursor: int = 0) -> List[Post]:\n        \"\"\"\n        Lists posts in the Following feed.\n\n        * Login required\n        \"\"\"\n        request = ListFeedRequest(\n            count=count,\n            max_cursor=cursor,\n            pull_type=PullType.LoadMore,\n            type=FeedType.Following,\n            is_cold_start=1,\n        )\n        feed = self.api.list_following_feed(request)\n\n        return feed.aweme_list\n"
  },
  {
    "path": "tiktok_bot/client/__init__.py",
    "content": "from .client import HTTPClient\n\n__all__ = [\"HTTPClient\"]\n"
  },
  {
    "path": "tiktok_bot/client/client.py",
    "content": "from collections import deque\nfrom time import time\nfrom typing import Deque, Optional\nfrom uuid import uuid4\n\nfrom httpx import Client, Response\nfrom loguru import logger\n\nfrom .utils import generate_as, generate_cp, generate_mas\n\n\nclass HTTPClient:\n    def __init__(\n        self,\n        base_url: str,\n        default_headers: Optional[dict] = None,\n        default_params: Optional[dict] = None,\n        history_len: int = 30,\n    ):\n        self.base_url = base_url\n        self.default_headers = default_headers or {}\n        self.default_params = default_params or {}\n\n        self.history: Deque[Response] = deque(maxlen=history_len)\n\n        self.http_client = Client(\n            base_url=self.base_url, headers=default_headers, params=self.default_params\n        )\n\n    def get(self, url: str, params: dict, headers: Optional[dict] = None):\n        custom_headers = headers or {}\n        all_params = {**self._generate_params(), **params}\n\n        logger.debug(f\"Sending request to {url}\", params=all_params, custom_headers=custom_headers)\n        response = self.http_client.get(url=url, params=all_params, headers=custom_headers)\n\n        self.history.append(response)\n\n        body = response.text or \"is empty!\"\n\n        logger.debug(f\"Response return status_code: {response.status_code}, body: {body}\")\n\n        for cookie_name, cookie_data in response.cookies.items():\n            self.http_client.cookies.set(cookie_name, cookie_data)\n            logger.debug(f\"New cookies: {dict(response.cookies)}\")\n\n        return response\n\n    def post(\n        self, url: str, data: dict, headers: Optional[dict] = None, params: Optional[dict] = None\n    ):\n        custom_headers = headers or {}\n        custom_params = params or {}\n        # merge parameters\n        all_params = {**self._generate_params(), **custom_params}\n\n        logger.debug(\n            f\"Sending request to {url}\", params=all_params, custom_headers=custom_headers, data=data\n        )\n\n        response = self.http_client.post(\n            url=url, params=all_params, data=data, headers=custom_headers,\n        )\n\n        self.history.append(response)\n\n        body = response.text or \"is empty!\"\n        logger.debug(f\"Response return status_code: {response.status_code}, body: {body}\")\n\n        for cookie_name, cookie_data in response.cookies.items():\n            self.http_client.cookies.set(cookie_name, cookie_data)\n            logger.debug(f\"New cookies: {dict(response.cookies)}\")\n\n        return response\n\n    def _generate_params(self):\n        now = str(int(round(time() * 1000)))\n\n        params = {\n            \"_rticket\": now,\n            \"ts\": now,\n            \"mas\": generate_mas(now),\n            \"as\": generate_as(now),\n            \"cp\": generate_cp(now),\n            \"idfa\": str(uuid4()).upper(),\n        }\n\n        return params\n"
  },
  {
    "path": "tiktok_bot/client/utils.py",
    "content": "from hashlib import md5, sha1\nfrom time import time\n\n\ndef generate_as(now: str) -> str:\n    as_md5 = md5(now.encode()).hexdigest()\n\n    return as_md5\n\n\ndef generate_cp(now: str) -> str:\n    now += str(time())\n\n    cp_md5 = md5(now.encode()).hexdigest()\n\n    return cp_md5\n\n\ndef generate_mas(now: str) -> str:\n    mas_sha = sha1(now.encode()).hexdigest()\n    mas_md5 = md5(mas_sha.encode()).hexdigest()\n\n    return mas_md5\n"
  },
  {
    "path": "tiktok_bot/models/__init__.py",
    "content": ""
  },
  {
    "path": "tiktok_bot/models/category.py",
    "content": "from typing import List, Union\n\nfrom pydantic import BaseModel, Schema\n\nfrom .post import Post\nfrom .request import CountOffsetParams, ListRequestParams, ListResponseData\nfrom .user import CommonUserDetails\n\n\nclass ChallengeInfo(BaseModel):\n    # The user who created the challenge, or an empty object\n    author: Union[CommonUserDetails, BaseModel]\n\n    # The name of the challenge\n    cha_name: str\n\n    # The ID of the challenge\n    cid: str\n\n    # A description of the challenge\n    desc: str\n\n    # ???\n    is_pgcshow: bool\n\n    # An in-app link to the challenge\n    schema_: str = Schema(default=..., alias=\"schema\")\n\n    # The type of challenge - 0 for hashtag?\n    type: int\n\n    # The number of users who have uploaded a video for the challenge\n    user_count: int\n\n    class Config:\n        fields = {\"schema_\": \"schema\"}\n\n\nclass Category(BaseModel):\n    # A list of posts in the category\n    aweme_list: List[Post]\n\n    # The type of category - 0 for hashtag?\n    category_type: int\n\n    # Information about the category\n    challenge_info: ChallengeInfo\n\n    # A description of the category type, e.g. \"Trending Hashtag\"\n    desc: str\n\n\nclass ListCategoriesRequest(ListRequestParams, CountOffsetParams):\n    pass\n\n\nclass ListCategoriesResponse(ListResponseData, CountOffsetParams):\n    # A list of categories\n    category_list: List[Category]\n"
  },
  {
    "path": "tiktok_bot/models/comment.py",
    "content": "from typing import List, Optional\n\nfrom pydantic import BaseModel\nfrom typing_extensions import Literal\n\nfrom .request import BaseResponseData, CountOffsetParams, ListRequestParams, ListResponseData\nfrom .tag import Tag\nfrom .user import CommonUserDetails\n\n\nclass Comment(BaseModel):\n    # The ID of the post\n    aweme_id: str\n\n    # The ID of the comment\n    cid: str\n\n    # The timestamp in seconds when the comment was posted\n    create_time: int\n\n    # The number of times the comment has been liked\n    digg_count: int\n\n    # If this comment is replying to a comment, this array contains the original comment\n    reply_comment: Optional[List[\"Comment\"]] = None\n\n    # If this comment is replying to a comment, the ID of that comment - \"0\" if not a reply\n    reply_id: str\n\n    # The status of the comment - 1 = published, 4 = published by you?\n    status: int\n\n    # The comment text\n    text: str\n\n    # Details about any tags in the comment\n    text_extra: List[Tag]\n\n    # Details about the author\n    user: CommonUserDetails\n\n    # 1 if the user likes the comment\n    user_digged: Literal[0, 1]\n\n\nclass ListCommentsRequest(ListRequestParams, CountOffsetParams):\n    # The ID of the post to list comments for\n    aweme_id: str\n\n    # ??? - default is 2\n    comment_style: Optional[int] = None\n\n    # ???\n    digged_cid = None\n\n    # ???\n    insert_cids = None\n\n\nclass ListCommentsResponse(ListResponseData, CountOffsetParams):\n    comments: List[Comment]\n\n\nclass PostCommentRequest(BaseModel):\n    # The ID of the post to comment on\n    aweme_id: str\n\n    # The comment text\n    text: str\n\n    # The ID of the comment that is being replied to\n    reply_id: Optional[str] = None\n\n    # Details about any tags in the comment\n    text_extra: List[Tag]\n\n    # ???\n    is_self_see: Literal[0, 1]\n\n\nclass PostCommentResponse(BaseResponseData):\n    # The comment that was posted\n    comment: Comment\n"
  },
  {
    "path": "tiktok_bot/models/feed.py",
    "content": "from typing import List, Optional\n\nfrom .feed_enums import FeedType, PullType\nfrom .post import Post\nfrom .request import (\n    CursorOffsetRequestParams,\n    CursorOffsetResponseParams,\n    ListRequestParams,\n    ListResponseData,\n)\n\n\nclass ListFeedRequest(ListRequestParams, CursorOffsetRequestParams):\n    # The type of feed to load\n    type: FeedType\n\n    # Your device's current volume level on a scale of 0 to 1, e.g. 0.5\n    volume: float = 0.5\n\n    # How the feed was requested\n    pull_type: PullType\n\n    # ??? - empty\n    req_from: Optional[str] = None\n\n    # ??? - 0\n    is_cold_start: Optional[int] = None\n\n    # ???\n    gaid: Optional[str] = None\n\n    # A user agent for your device\n    ad_user_agent: Optional[str] = None\n\n    class Config:\n        use_enum_values = True\n\n\nclass ListFeedResponse(ListResponseData, CursorOffsetResponseParams):\n    # A list of posts in the feed\n    aweme_list: List[Post]\n\n\nclass ListForYouFeedResponse(ListFeedResponse):\n    # ??? - 1\n    home_model: int\n\n    # ??? - 1\n    refresh_clear: int\n"
  },
  {
    "path": "tiktok_bot/models/feed_enums.py",
    "content": "from enum import IntEnum\n\n\nclass FeedType(IntEnum):\n    ForYou = 0\n    Following = 1\n\n\nclass PullType(IntEnum):\n    # The feed was loaded by default, e.g. by clicking the tab or loading the app\n    Default = 0\n\n    # The feed was explicitly refreshed by the user, e.g. by swiping down\n    Refresh = 1\n\n    # More posts were requested by the user, e.g. by swiping up\n    LoadMore = 2\n"
  },
  {
    "path": "tiktok_bot/models/follow.py",
    "content": "from typing import List\n\nfrom pydantic import BaseModel\nfrom typing_extensions import Literal\n\nfrom .request import (\n    BaseResponseData,\n    ListRequestParams,\n    ListResponseData,\n    TimeOffsetRequestParams,\n    TimeOffsetResponseParams,\n)\nfrom .user import CommonUserDetails\n\n\nclass FollowRequest(BaseModel):\n    # The id of the user to follow\n    user_id: str\n\n    # 0 to unfollow, 1 to follow\n    type: Literal[0, 1]\n\n\nclass FollowResponse(BaseResponseData):\n    # 0 if not following, 1 if following\n    follow_status: Literal[0, 1]\n\n    # 0 if not watching, 1 if watching\n    watch_status: Literal[0, 1]\n\n\nclass ListReceivedFollowRequestsRequest(ListRequestParams, TimeOffsetRequestParams):\n    pass\n\n\nclass ListReceivedFollowRequestsResponse(ListResponseData, TimeOffsetResponseParams):\n    # A list of users who have requested to follow you\n    request_users: List[CommonUserDetails]\n\n\nclass ApproveFollowRequest(BaseModel):\n    # The id of the user to approve\n    from_user_id: str\n\n\nclass ApproveFollowResponse(BaseResponseData):\n    # 0 if the user was successfully approved\n    approve_status: int\n\n\nclass RejectFollowRequest(BaseModel):\n    # The id of the user to reject\n    from_user_id: str\n\n\nclass RejectFollowResponse(BaseResponseData):\n    # 0 if the user was successfully rejected\n    reject_status: int\n"
  },
  {
    "path": "tiktok_bot/models/follower.py",
    "content": "from typing import List\n\nfrom .request import (\n    ListRequestParams,\n    ListResponseData,\n    TimeOffsetRequestParams,\n    TimeOffsetResponseParams,\n)\nfrom .user import CommonUserDetails\n\n\nclass ListFollowersRequest(ListRequestParams, TimeOffsetRequestParams):\n    # The id of the user whose followers to retrieve\n    user_id: str\n\n\nclass ListFollowersResponse(ListResponseData, TimeOffsetResponseParams):\n    # A list of the user's followers\n    followers: List[CommonUserDetails]\n\n\nclass ListFollowingRequest(ListRequestParams, TimeOffsetRequestParams):\n    # The id of the user whose followers to retrieve\n    user_id: str\n\n\nclass ListFollowingResponse(ListResponseData, TimeOffsetResponseParams):\n    # A list of users the user is following\n    followings: List[CommonUserDetails]\n"
  },
  {
    "path": "tiktok_bot/models/hashtag.py",
    "content": "from typing import List\n\nfrom .post import Post\nfrom .request import CountOffsetParams, ListRequestParams, ListResponseData\n\n\nclass ListPostsInHashtagRequest(ListRequestParams, CountOffsetParams):\n    # The ID of the hashtag\n    ch_id: str\n\n    # ??? - set to 0\n    query_type: int = 0\n\n    # ??? - set to 5\n    type: int = 5\n\n\nclass ListPostsInHashtagResponse(ListResponseData, CountOffsetParams):\n    # A list of posts containing the hashtag\n    aweme_list: List[Post]\n"
  },
  {
    "path": "tiktok_bot/models/like.py",
    "content": "from typing_extensions import Literal\n\nfrom .request import BaseResponseData\n\n\nclass LikePostRequest(BaseResponseData):\n    # The id of the post to like\n    aweme_id: str\n\n    # 0 to unlike, 1 to like\n    type: Literal[0, 1]\n\n\nclass LikePostResponse(BaseResponseData):\n    #\n    # 0 if liked, 1 if not liked\n    #\n    # Note: for some reason, this value is the opposite of what you would expect\n    is_digg: Literal[0, 1]\n"
  },
  {
    "path": "tiktok_bot/models/login.py",
    "content": "from typing import List, Union\n\nfrom pydantic import BaseModel\n\n\nclass LoginRequest(BaseModel):\n    # Unsure, but looks to be hard-coded to 1\n    mix_mode: int = 1\n\n    # The unique username (\"username\") of the user\n    username: str = \"\"\n\n    # The email address associated with the user account\n    email: str = \"\"\n\n    # The mobile number associated with the user account\n    mobile: str = \"\"\n\n    # ???\n    account: str = \"\"\n\n    # The password to the user account\n    password: str = \"\"\n\n    # The captcha answer - only required if a captcha was shown\n    captcha: str = \"\"\n\n\nclass LoginSuccessData(BaseModel):\n    # ???\n    area: str\n\n    # The URL of the user's avatar\n    avatar_url: str\n\n    # ???\n    bg_img_url: str\n\n    # The user's birthday\n    birthday: str\n\n    # If the user allows people to find them by their phone number\n    can_be_found_by_phone: int\n\n    # ???\n    connects: List[BaseModel]\n\n    # ???\n    description: str\n\n    # The email address associated with the account\n    email: str\n\n    # The number of users that follow the user\n    followers_count: int\n\n    # The number of users the user is following\n    followings_count: int\n\n    # An integer representing the gender of the user\n    gender: int\n\n    # ???\n    industry: str\n\n    # Indicates if the user account is blocked\n    is_blocked: int\n\n    # ???\n    is_blocking: int\n\n    # ???\n    is_recommend_allowed: int\n\n    # ???\n    media_id: int\n\n    # The mobile number of the user\n    mobile: str\n\n    # The name of the user - does not appear to be used\n    name: str\n\n    # Indicates if the user is new or not\n    new_user: int\n\n    # A Chinese character hint\n    recommend_hint_message: str\n\n    # The screen name of the user - does not appear to be used\n    screen_name: str\n\n    # The session ID used to authenticate subsequent requests in the sessionid cookie\n    session_key: str\n\n    # ???\n    skip_edit_profile: int\n\n    # ???\n    user_auth_info: str\n\n    # The ID of the user\n    user_id: str\n\n    # If the user is verified or not\n    user_verified: bool\n\n    # ???\n    verified_agency: str\n\n    # ???\n    verified_content: str\n\n    # The number of users that have visited the user's profile recently\n    visit_count_recent: int\n\n\nclass LoginErrorData(BaseModel):\n    # If required, the captcha that must solved\n    captcha: str\n\n    # A message explaining why the request failed\n    description: str\n\n    # An error code\n    error_code: int\n\n\nclass LoginResponse(BaseModel):\n    data: Union[LoginSuccessData, LoginErrorData]\n\n    # A message indicating whether the request was successful or not\n    message: str\n"
  },
  {
    "path": "tiktok_bot/models/music.py",
    "content": "from typing import Optional\n\nfrom pydantic import BaseModel\n\nfrom .request import Media\n\n\nclass MusicTrack(BaseModel):\n    # The name of the musician\n    author: str\n\n    # A HD version of the music's cover art\n    cover_hd: Optional[Media]\n\n    # A large version of the music's cover art\n    cover_large: Optional[Media]\n\n    # A medium version of the music's cover art\n    cover_medium: Optional[Media]\n\n    # A thumbnail version of the music's cover art\n    cover_thumb: Media\n\n    # The duration of the track\n    duration: int\n\n    # The ID of the track\n    id: str\n\n    # The handle of the owner of the track\n    owner_handle: Optional[str]\n\n    # The ID of the owner of the track\n    owner_id: Optional[str]\n\n    # The nickname of the owner of the track\n    owner_nickname: Optional[str]\n\n    # The link to play this track\n    play_url: Media\n\n    # The title of this track\n    title: str\n\n    # The number of posts that use this track\n    user_count: int\n"
  },
  {
    "path": "tiktok_bot/models/post.py",
    "content": "from typing import List, Optional\n\nfrom pydantic import BaseModel\n\nfrom .music import MusicTrack\nfrom .request import (\n    BaseResponseData,\n    CursorOffsetRequestParams,\n    CursorOffsetResponseParams,\n    ListRequestParams,\n    ListResponseData,\n)\nfrom .user import CommonUserDetails\nfrom .video import Video\n\n\nclass PostStatistics(BaseModel):\n    # The ID of the post\n    aweme_id: str\n\n    # The number of comments on the post\n    comment_count: int\n\n    # The number of times the post has been liked\n    digg_count: int\n\n    # The number of times the post has been forwarded (looks unused?)\n    forward_count: Optional[int]\n\n    # The number of times the post has been viewed - doesn't appear to be public, so always 0\n    play_count: int\n\n    # The number of times the post has been shared\n    share_count: int\n\n\nclass PostStatus(BaseModel):\n    # True if the post allows comments\n    allow_comment: bool\n\n    # True if the post allows sharing\n    allow_share: bool\n\n    # 0 if the post can be downloaded\n    download_status: int\n\n    # True if the post is currently being reviewed\n    in_reviewing: Optional[bool]\n\n    # True if the post has been deleted\n    is_delete: bool\n\n    # True if the post is private\n    is_private: bool\n\n    # True if the post contains content that is not allowed on the platform\n    is_prohibited: Optional[bool]\n\n    # 0 if the post is public\n    private_status: Optional[int]\n\n    # 1 if the post has been reviewed\n    reviewed: Optional[int]\n\n\nclass PostTags(BaseModel):\n    # 0 if the tag is for a user; 1 if the tag is for a hashtag\n    type: int\n\n    # The name of the hashtag\n    hashtag_name: Optional[str]\n\n    # The ID of the tagged user\n    user_id: Optional[str]\n\n\nclass RiskInfo(BaseModel):\n    # The text shown if the post has been flagged\n    content: str\n\n    # ???\n    risk_sink: bool = False\n\n    # The risk type associated with the post - 0 if no risk; 1 if low; 2 if high\n    type: int\n\n    # ??? - only present if the post has been flagged\n    vote: Optional[bool]\n\n    # True if a warning should be shown to the user\n    warn: bool\n\n\nclass ShareInfo(BaseModel):\n    # ???\n    bool_persist: Optional[int]\n\n    # The description used when sharing (if set)\n    share_desc: str\n\n    # The description used when sharing a link only (if set)\n    share_link_desc: Optional[str]\n\n    # The quote used when sharing (if set)\n    share_quote: Optional[str]\n\n    # The signature used when sharing (if set)\n    share_signature_desc: Optional[str]\n\n    # The signature URL used when sharing (if set)\n    share_signature_url: Optional[str]\n\n    # The title used when sharing\n    share_title: str\n\n    # The link to share\n    share_url: str\n\n    # The description used when sharing on Weibo\n    share_weibo_desc: str\n\n\nclass StickerInfo(BaseModel):\n    # The ID of the sticker, e.g. 22094\n    id: str\n\n    # The display name of the sticker, e.g. Long Face\n    name: str\n\n\nclass Post(BaseModel):\n    # Details about the author\n    author: Optional[CommonUserDetails]\n\n    # The ID of the author\n    author_user_id: str\n\n    # The ID of the post\n    aweme_id: str\n\n    # The type of post - 0 for a musical.ly\n    aweme_type: int\n\n    # The timestamp in seconds when the post was created\n    create_time: int\n\n    # A description of the post\n    desc: str\n\n    # Details about the music used in the post\n    music: Optional[MusicTrack]\n\n    # True if the end user should not be provided the option to download the video\n    prevent_download: Optional[bool]\n\n    # An age rating for the post, e.g. 12\n    rate: int\n\n    # The 2-letter region the post was created in, e.g. US\n    region: str\n\n    # Risk information about the post\n    risk_infos: Optional[RiskInfo]\n\n    # Information used when sharing the post\n    share_info: Optional[ShareInfo]\n\n    # A link to the video on the musical.ly website that is used when sharing\n    share_url: str\n\n    # Statistics about the post\n    statistics: PostStatistics\n\n    # Status information about the post\n    status: PostStatus\n\n    # Information about the sticker used in the post\n    sticker_detail: Optional[StickerInfo]\n\n    # The ID of the sticker used in the post (looks to be deprecated by sticker_detail)\n    stickers: Optional[str]\n\n    # Tagged users and hashtags used in the description\n    text_extra: List[PostTags]\n\n    # 1 if the logged in user has liked this post\n    user_digged: int\n\n    # Details about the video in the post\n    video: Video\n\n    @property\n    def video_url(self):\n        url = filter(lambda url: \"watermark\" in url, self.video.download_addr.url_list)\n\n        return next(url)\n\n    @property\n    def video_url_without_watermark(self):\n        return self.video_url.replace(\"watermark=1\", \"watermark=0\")\n\n\nclass GetPostResponse(BaseResponseData):\n    aweme_detail: Post\n\n\nclass ListPostsRequest(ListRequestParams, CursorOffsetRequestParams):\n    # The id of the user whose posts to retrieve\n    user_id: str\n\n\nclass ListPostsResponse(ListResponseData, CursorOffsetResponseParams):\n    aweme_list: List[Post]\n"
  },
  {
    "path": "tiktok_bot/models/qr-code.py",
    "content": "from typing import List\n\nfrom pydantic import BaseModel\n\nfrom .request import BaseResponseData\n\n\nclass QRCodeRequest(BaseModel):\n    # The internal version to use; currently 4\n    schema_type: int\n\n    # The ID of the user to get a QR code for\n    object_id: str\n\n\nclass QRCodeUrl(BaseModel):\n    # An in-app link to the QR code\n    uri: str\n\n    # Contains a public link to the QR code image (first element in array)\n    url_list: List[str]\n\n\nclass QRCodeResponse(BaseResponseData):\n    # Contains a link to the QR code\n    qrcode_url: QRCodeUrl\n"
  },
  {
    "path": "tiktok_bot/models/request.py",
    "content": "import abc\nfrom typing import List, Optional, Union\n\nfrom pydantic import BaseModel\n\n\nclass RequiredUserDefinedRequestParams(BaseModel, abc.ABC):\n    # The 16-character ID of your installation, e.g. 4549764744226841084\n    iid: str\n\n    # A 16-character hexadecimal identifier associated with your device, e.g. 4b903fbb9d457937\n    openudid: str\n\n    # The ID of your device that has already been registered with musical.ly\n    device_id: str\n\n    # An anti-fraud fingerprint of your device requested from a different API\n    fp: str\n\n\nclass StaticRequestParams(RequiredUserDefinedRequestParams):\n    # Your Android version, e.g. 23\n    os_api: str\n\n    # Your device model, e.g. Pixel 2\n    device_type: str\n\n    # ??? - set to \"a\"\n    ssmix: str\n\n    # The SS_VERSION_CODE metadata value from the AndroidManifest.xml file, e.g. 2018060103\n    manifest_version_code: str\n\n    # Your device's pixel density, e.g. 480\n    dpi: int\n\n    # The application name - hard-coded to \"musical_ly\"\n    app_name: str\n\n    # The SS_VERSION_NAME metadata value from the AndroidManifest.xml file, e.g. 7.2.0\n    version_name: str\n\n    # The UTC offset in seconds of your timezone, e.g. 37800 for Australia/Lord_Howe\n    timezone_offset: int\n\n    # ??? - are we in China / using the Chinese version? Set to 0\n    is_my_cn: int\n\n    # Network connection type, e.g. \"wifi\"\n    ac: str\n\n    # The UPDATE_VERSION_CODE metadata value from the AndroidManifest.xml file, e.g. 2018060103\n    update_version_code: str\n\n    # The channel you downloaded the app through, e.g. googleplay\n    channel: str\n\n    # Your device's platform, e.g. android\n    device_platform: str\n\n    # The build int of the application, e.g. 7.2.0\n    build_number: str\n\n    # A numeric version of the version_name metadata value, e.g. 720\n    version_code: int\n\n    # The name of your timezone as per the tz database, e.g. Australia/Sydney\n    timezone_name: str\n\n    # The region of the account you are logging into, e.g. AU.\n    # This field is only present if you are logging in from a device that hasn't had a\n    # user logged in before.\n    account_region: Optional[str] = None\n\n    # Your Optional[str]ice's resolution, e.g. 1080*1920\n    resolution: str\n\n    # Your device's operating system version, e.g. 8.0.0\n    os_version: str\n\n    # Your device's brand, e.g. Google\n    device_brand: str\n\n    # ??? - empty\n    mcc_mnc: str\n\n    # The application's two-letter language code, e.g. en\n    app_language: str\n\n    # Your i18n language, e.g. en\n    language: str\n\n    # Your region's i18n locale, e.g. US\n    region: str\n\n    # Your device's i18n locale, e.g. US\n    sys_region: str\n\n    # Your carrier's region (a two-letter country code), e.g. AU\n    carrier_region: str\n\n    # You carrer's mobile country code (MCC), e.g. 505\n    carrier_region_v2: str\n\n    # A hard-coded i18n constant set to \"1233\"\n    aid: str\n\n    # ??? - set to 1\n    pass_region: int\n\n    # ??? - set to 1\n    pass_route: int\n\n    class Config:\n        fields = {\"pass_region\": \"pass-region\", \"pass_route\": \"pass-route\"}\n\n\nclass AntiSpamParams(BaseModel):\n    # A 20-character anti-spam parameter\n    as_: str\n\n    # A 20-character anti-spam parameter\n    cp: str\n\n    # An encoded version of the 'as' anti-spam parameter\n    mas: str\n\n    class Config:\n        fields = {\"as_\": \"as\"}\n\n\nclass BaseRequestParams(StaticRequestParams, AntiSpamParams):\n    # The current timestamp in seconds since UNIX epoch\n    ts: int\n\n    # The current timestamp in milliseconds since UNIX epoch\n    _rticket: str\n\n\nclass ListRequestParams(BaseModel):\n    # The number of results to return\n    count: int = 10\n\n    # How the request will be retried on failure - defaults to \"no_retry\"\n    retry_type: Optional[str] = None\n\n\nclass TimeOffsetRequestParams(BaseModel):\n    \"\"\"\n    A timestamp in seconds - the most recent results before this time will be listed.\n    Use min_time from the response data here for pagination.\n    \"\"\"\n\n    max_time: int\n\n\nclass TimeOffsetResponseParams(BaseModel):\n    # The timestamp in seconds associated with the first result\n    max_time: int\n\n    # The timestamp in seconds associated with the last result - use as max_time for pagination\n    min_time: int\n\n\nclass CursorOffsetRequestParams(BaseModel):\n    \"\"\"\n    A timestamp in milliseconds - the most recent results before this time will be listed.\n    Use max_cursor from the response data here for pagination. Use 0 for the most recent.\n    \"\"\"\n\n    max_cursor: int\n\n\nclass CursorOffsetResponseParams(BaseModel):\n    # The timestamp in milliseconds associated with the first result\n    min_cursor: int\n\n    # The timestamp in milliseconds associated with the last result - use for pagination\n    max_cursor: int\n\n\nclass CountOffsetParams(BaseModel):\n    # The number of results to skip\n    cursor: int = 0\n\n\nclass ExtraResponseData(BaseModel):\n    # ???\n    fatal_item_ids: Optional[List[int]] = None\n\n    # A log ID for this request\n    logid: Optional[str] = None\n\n    # The current timestamp in milliseconds\n    now: int\n\n\nclass BaseResponseData(BaseModel, abc.ABC):\n    # 0 if the request was successful\n    status_code: int\n\n    extra: ExtraResponseData\n\n\nclass ListResponseData(BaseResponseData):\n    # Whether there are more results that can be requested\n    has_more: Union[bool, int]\n\n    # The total number of results returned - not present in all list requests\n    total: Optional[int] = None\n\n\nclass Media(BaseModel):\n    # A list of HTTP URLs to this media\n    url_list: List[str]\n"
  },
  {
    "path": "tiktok_bot/models/search.py",
    "content": "from typing import List, Optional\n\nfrom pydantic import BaseModel\nfrom typing_extensions import Literal\n\nfrom .category import ChallengeInfo\nfrom .request import CountOffsetParams, ListRequestParams, ListResponseData\nfrom .user import CommonUserDetails\n\n\nclass SearchRequest(ListRequestParams, CountOffsetParams):\n    # The term to search for\n    keyword: str\n\n\nclass UserSearchRequest(SearchRequest):\n    # Required - the scope of the search - users = 1.\n    type: int = 1\n\n\nclass SubstringPosition(BaseModel):\n    \"\"\"\n    Represents the location of a substring in a string.\n    e.g. For the string \"The quick brown fox\", the substring \"quick\" would be:\n    {\n        begin: 4,\n        end: 8\n    }\n    \"\"\"\n\n    # The start index of the substring\n    begin: int\n\n    # The end index of the substring\n    end: int\n\n\nclass UserSearchResult(BaseModel):\n    # If the user's nickname contains the search term, this array contains the location of the term\n    position: Optional[List[SubstringPosition]] = None\n\n    # If the user's username (unique_id) contains the search term,\n    # this array contains the location of the term\n    uniqid_position: Optional[List[SubstringPosition]] = None\n\n    # Information about the user\n    user_info: CommonUserDetails\n\n\nclass UserSearchResponse(ListResponseData, CountOffsetParams):\n    # A list of users that match the search term\n    user_list: List[UserSearchResult]\n\n    # The scope of the search - users = 1\n    type: int\n\n\nclass HashtagSearchResult(BaseModel):\n    # Information about the hashtag\n    challenge_info: ChallengeInfo\n\n    # If the hashtag contains the search term, this array contains the location of the term\n    position: Optional[List[SubstringPosition]] = None\n\n\nclass HashtagSearchResponse(ListResponseData, CountOffsetParams):\n    # A list of hashtags that match the search term\n    challenge_list: List[HashtagSearchResult]\n\n    # True if a challenge matches the search term\n    is_match: bool\n\n    # 1 if the search term is disabled\n    keyword_disabled: Literal[0, 1]\n"
  },
  {
    "path": "tiktok_bot/models/sticker.py",
    "content": "from typing import Any, List\n\nfrom pydantic import BaseModel\n\nfrom .post import Post\nfrom .request import BaseResponseData, CountOffsetParams, ListRequestParams, ListResponseData, Media\n\n\nclass Sticker(BaseModel):\n    # ???\n    children: Any\n\n    # A description of the sticker\n    desc: str\n\n    # The ID of the sticker\n    effect_id: str\n\n    # The icon associated with the sticker\n    icon_url: Media\n\n    # The ID of the sticker\n    id: str\n\n    # True if the current user has favorited the sticker\n    is_favorite: bool\n\n    # The name of the sticker\n    name: str\n\n    # The ID the user that owns the sticker (empty if owned by the Effect Assistant)\n    owner_id: str\n\n    # The nickname of the owner, e.g. \"Effect Assistant\"\n    owner_nickname: str\n\n    # ???\n    tags: List[Any]\n\n    # The total number of posts using this sticker\n    user_count: int\n\n\nclass ListPostsByStickerRequest(ListRequestParams, CountOffsetParams):\n    # The ID of the sticker\n    sticker_id: str\n\n\nclass ListPostsByStickerResponse(ListResponseData, CountOffsetParams):\n    # A list of posts using the sticker\n    aweme_list: List[Post]\n\n    # Currently empty\n    stickers: List[Any]\n\n\nclass GetStickersRequest(BaseModel):\n    # A list of sticker ids to get information about\n    sticker_ids: str\n\n\nclass GetStickersResponse(BaseResponseData):\n    sticker_infos: List[Sticker]\n"
  },
  {
    "path": "tiktok_bot/models/tag.py",
    "content": "\"\"\"\n  Represents a text link to a user, e.g. \"@username\" in a comment\n\"\"\"\nfrom pydantic import BaseModel\n\n\nclass Tag(BaseModel):\n    # The type of user being tagged?\n    at_user_type: str\n\n    # The zero-based index in the text where the tag starts\n    end: int\n\n    # The zero-based index in the text where the tag ends\n    start: int\n\n    # The type of tag?\n    type: int\n\n    # The ID of the user being tagged\n    user_id: str\n"
  },
  {
    "path": "tiktok_bot/models/user.py",
    "content": "from typing import Optional, Union\n\nfrom pydantic import BaseModel\n\nfrom .request import BaseResponseData, Media\n\n\nclass CommonUserDetails(BaseModel):\n    # A large version of the user's avatar\n    avatar_larger: Media\n\n    # A medium version of the user's avatar\n    avatar_medium: Media\n\n    # A thumbnail version of the user's avatar\n    avatar_thumb: Media\n\n    # The timestamp in seconds when the user's account was created\n    create_time: Optional[int] = None\n\n    # The badge name with a verified user (e.g. comedian, style guru)\n    custom_verify: str\n\n    # 1 if you follow this user\n    follow_status: int\n\n    # 1 if this user follows you\n    follower_status: int\n\n    # The user's Instagram handle\n    ins_id: str\n\n    # Indicates if the user has been crowned\n    is_verified: bool\n\n    # The user's profile name\n    nickname: str\n\n    # A 2-letter country code representing the user's region, e.g. US\n    region: str\n\n    # If the user is live, a str ID used to join their stream, else 0\n    room_id: Optional[Union[str, int]] = None\n\n    # 1 if the user's profile is set to private\n    secret: int\n\n    # The user's profile signature\n    signature: str\n\n    # The user's Twitter handle\n    twitter_id: str\n\n    # The user's ID\n    uid: str\n\n    # The user's musername\n    unique_id: str\n\n    # 1 if the user has been crowned\n    verification_type: int\n\n    # The user's YouTube channel ID\n    youtube_channel_id: str\n\n\nclass UserProfile(CommonUserDetails):\n    # The number of videos the user has uploaded\n    aweme_count: int\n\n    # The number of videos the user has liked\n    favoriting_count: int\n\n    # The number of users who follow this user\n    follower_count: int\n\n    # The number of users this user follows\n    following_count: int\n\n    # The total number of likes the user has received\n    total_favorited: int\n\n\nclass UserProfileResponse(BaseResponseData):\n    user: UserProfile\n"
  },
  {
    "path": "tiktok_bot/models/video.py",
    "content": "from pydantic import BaseModel\n\nfrom .request import Media\n\n\nclass Video(BaseModel):\n    # A medium version of the video's cover image\n    cover: Media\n\n    # A high-quality link to download the video\n    download_addr: Media\n\n    # The video's duration in milliseconds\n    duration: int\n\n    # Whether the download link has a watermark\n    has_watermark: bool\n\n    # The video's height, e.g. 960\n    height: int\n\n    # A high-quality version of the video's cover image\n    origin_cover: Media\n\n    # The quality of the video, e.g. 720p\n    ratio: str\n\n    # The video's width, e.g. 540\n    width: int\n"
  }
]