Repository: sudoguy/tiktok_bot Branch: master Commit: 3740c6d0e9a1 Files: 47 Total size: 59.5 KB Directory structure: gitextract_wm8u40tj/ ├── .bumpversion.cfg ├── .flake8 ├── .github/ │ ├── FUNDING.yml │ └── workflows/ │ └── pythonpackage.yml ├── .gitignore ├── .isort.cfg ├── .pre-commit-config.yaml ├── .travis.yml ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── docs/ │ ├── css/ │ │ └── custom.css │ └── index.md ├── mkdocs.yml ├── pyproject.toml ├── tests/ │ ├── api/ │ │ └── test_api.py │ ├── conftest.py │ └── test_tiktok-bot.py └── tiktok_bot/ ├── __init__.py ├── api/ │ ├── __init__.py │ ├── api.py │ └── config.py ├── bot/ │ ├── __init__.py │ └── bot.py ├── client/ │ ├── __init__.py │ ├── client.py │ └── utils.py └── models/ ├── __init__.py ├── category.py ├── comment.py ├── feed.py ├── feed_enums.py ├── follow.py ├── follower.py ├── hashtag.py ├── like.py ├── login.py ├── music.py ├── post.py ├── qr-code.py ├── request.py ├── search.py ├── sticker.py ├── tag.py ├── user.py └── video.py ================================================ FILE CONTENTS ================================================ ================================================ FILE: .bumpversion.cfg ================================================ [bumpversion] commit = True tag = True current_version = 0.6.4 [bumpversion:file:pyproject.toml] [bumpversion:file:tiktok_bot/__init__.py] [bumpversion:file:tests/test_tiktok-bot.py] ================================================ FILE: .flake8 ================================================ [flake8] max-line-length = 100 ignore = C812,W503 ================================================ FILE: .github/FUNDING.yml ================================================ # These are supported funding model platforms github: [sudoguy] patreon: # Replace with a single Patreon username open_collective: # Replace with a single Open Collective username ko_fi: # Replace with a single Ko-fi username tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry liberapay: # Replace with a single Liberapay username issuehunt: # Replace with a single IssueHunt username otechie: # Replace with a single Otechie username custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] ================================================ FILE: .github/workflows/pythonpackage.yml ================================================ name: Main on: [push, pull_request] jobs: Linting: runs-on: ubuntu-latest steps: - uses: actions/checkout@v1 - name: Set up Python 3.7 uses: actions/setup-python@v1 with: python-version: 3.7 - name: Linting run: | pip install pre-commit pre-commit run --all-files Linux: needs: Linting runs-on: ubuntu-latest strategy: matrix: python-version: [3.6, 3.7] steps: - uses: actions/checkout@v1 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v1 with: python-version: ${{ matrix.python-version }} - name: Install Poetry run: | PATH=$PATH:/home/runner/.local/bin pip install --pre --user poetry poetry config virtualenvs.create false poetry shell - name: Install dependencies run: | PATH=$PATH:/home/runner/.local/bin poetry install - name: Test run: | PATH=$PATH:/home/runner/.local/bin poetry run pytest -q tests MacOS: needs: Linting runs-on: macos-latest strategy: matrix: python-version: [3.6, 3.7] steps: - uses: actions/checkout@v1 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v1 with: python-version: ${{ matrix.python-version }} - name: Install Poetry run: | PATH=$PATH:/Users/runner/.local/bin pip install --pre --user poetry poetry config virtualenvs.create false poetry shell - name: Install dependencies run: | PATH=$PATH:/Users/runner/.local/bin poetry install - name: Test run: | PATH=$PATH:/Users/runner/.local/bin poetry run pytest -q tests Windows: needs: Linting runs-on: windows-latest strategy: matrix: python-version: [3.6, 3.7] steps: - uses: actions/checkout@v1 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v1 with: python-version: ${{ matrix.python-version }} - name: Install Poetry run: | pip install --pre --user poetry poetry shell - name: Install dependencies run: | poetry install - name: Test run: | poetry run pytest -q tests ================================================ FILE: .gitignore ================================================ # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover .hypothesis/ .pytest_cache/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py db.sqlite3 # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder target/ # Jupyter Notebook .ipynb_checkpoints # pyenv .python-version # celery beat schedule file celerybeat-schedule # SageMath parsed files *.sage.py # Environments .env .venv env/ venv/ ENV/ env.bak/ venv.bak/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ # VS Code .vscode ================================================ FILE: .isort.cfg ================================================ [settings] line_length=100 indent=4 multi_line_output=3 include_trailing_comma=True known_first_party=tiktok_bot known_third_party = httpx,loguru,pydantic,pytest,tqdm,typing_extensions ================================================ FILE: .pre-commit-config.yaml ================================================ repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v2.4.0 hooks: - id: check-added-large-files - id: debug-statements - id: end-of-file-fixer exclude: '.bumpversion.cfg' - id: check-symlinks - id: trailing-whitespace - id: mixed-line-ending args: ['--fix=lf'] - repo: https://github.com/humitos/mirrors-autoflake rev: v1.1 hooks: - id: autoflake args: ['--in-place', '--remove-all-unused-imports', '--remove-unused-variable'] - repo: https://github.com/asottile/seed-isort-config rev: v1.9.3 hooks: - id: seed-isort-config - repo: https://github.com/pre-commit/mirrors-isort rev: v4.3.21 hooks: - id: isort - repo: https://github.com/psf/black rev: 19.10b0 hooks: - id: black files: '\.py$' - repo: https://github.com/pre-commit/pre-commit-hooks rev: v2.4.0 hooks: - id: flake8 additional_dependencies: [ 'flake8-blind-except', 'flake8-commas', 'flake8-comprehensions', 'flake8-deprecated', 'flake8-mutable', 'flake8-tidy-imports', 'flake8-print', ] ================================================ FILE: .travis.yml ================================================ language: python os: - linux stages: - lint - test - name: deploy if: tag IS present python: - "3.6" - "3.7" - "3.8" - "nightly" - "pypy3" install: - pip install --upgrade pip script: skip jobs: include: - stage: lint install: - pip install pre-commit - pre-commit install-hooks script: - pre-commit run --all-files - stage: test install: - pip install --pre poetry - poetry install -v script: - poetry run pytest - stage: deploy script: - echo Deploying to PyPI... before_deploy: - pip install --upgrade pip - pip install poetry - poetry config http-basic.pypi $PYPI_USER $PYPI_PASSWORD - poetry build deploy: provider: script script: poetry publish skip_cleanup: true on: all_branches: true # Travis recognizes tag names as "branches" condition: $TRAVIS_BUILD_STAGE_NAME = deploy repo: sudoguy/tiktok_bot tags: true notifications: email: on_success: change on_failure: always before_cache: - rm -f $HOME/.cache/pip/log/debug.log cache: pip: true directories: - "$HOME/.cache/pre-commit" - .venv ================================================ FILE: CHANGELOG.md ================================================ # Changelog All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## 0.6.1 (November 17, 2019) ### Added - Changelog ## 0.6.0 (November 17, 2019) ### Added - Method for searches posts(videos) by hashtag name - `search_posts_by_hashtag` ### Fixed - Set some fields in `music` and `post` models as `Optional` ================================================ FILE: CODE_OF_CONDUCT.md ================================================ # Contributor Covenant Code of Conduct ## Our Pledge In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. ## Our Standards Examples of behavior that contributes to creating a positive environment include: * Using welcoming and inclusive language * Being respectful of differing viewpoints and experiences * Gracefully accepting constructive criticism * Focusing on what is best for the community * Showing empathy towards other community members Examples of unacceptable behavior by participants include: * The use of sexualized language or imagery and unwelcome sexual attention or advances * Trolling, insulting/derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or electronic address, without explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. ## Scope This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at eskemerov@gmail.com. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html [homepage]: https://www.contributor-covenant.org For answers to common questions about this code of conduct, see https://www.contributor-covenant.org/faq ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2019 Evgeny Kemerov Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # This project is no longer under development. # A NEW project is being developed here: [sudoguy/tiktokpy](https://github.com/sudoguy/tiktokpy/) ---

tiktok-bot

The most intelligent TikTok bot for Python.

Build Status Downloads Package version

**Note**: *This project should be considered as an **"alpha"** release.* --- ## Quickstart ```python from tiktok_bot import TikTokBot bot = TikTokBot() # getting your feed (list of posts) my_feed = bot.list_for_you_feed(count=25) popular_posts = [post for post in my_feed if post.statistics.play_count > 1_000_000] # extract video urls without watermark (every post has helpers) urls = [post.video_url_without_watermark for post in popular_posts] # searching videos by hashtag name posts = bot.search_posts_by_hashtag("cat", count=50) ``` ## Installation Install with pip: ```shell pip install tiktok-bot ``` tiktok-bot requires Python 3.6+ ================================================ FILE: docs/css/custom.css ================================================ div.autodoc-docstring { padding-left: 20px; margin-bottom: 30px; border-left: 5px solid rgba(230, 230, 230); } div.autodoc-members { padding-left: 20px; margin-bottom: 15px; } ================================================ FILE: docs/index.md ================================================ # Welcome to MkDocs For full documentation visit [mkdocs.org](https://mkdocs.org). ## Commands * `mkdocs new [dir-name]` - Create a new project. * `mkdocs serve` - Start the live-reloading docs server. * `mkdocs build` - Build the documentation site. * `mkdocs help` - Print this help message. ## Project layout mkdocs.yml # The configuration file. docs/ index.md # The documentation homepage. ... # Other markdown pages, images and other files. ================================================ FILE: mkdocs.yml ================================================ site_name: TikTok Bot site_description: Free TikTok bot for Python. theme: name: "material" repo_name: sudoguy/tiktok_bot repo_url: https://github.com/sudoguy/tiktok-bot edit_uri: "" nav: - Introduction: 'index.md' markdown_extensions: - admonition - codehilite - mkautodoc extra_css: - css/custom.css ================================================ FILE: pyproject.toml ================================================ [tool.poetry] name = "tiktok_bot" version = "0.6.4" description = "Tik Tok API" license = "MIT" authors = ["Evgeny Kemerov "] readme = "README.md" include = ["CHANGELOG.md", "LICENSE"] repository = "https://github.com/sudoguy/tiktok_bot/" homepage = "https://github.com/sudoguy/tiktok_bot/" keywords = ["tiktok", "bot", "api", "wrapper", "tiktokbot"] classifiers = [ "Development Status :: 3 - Alpha", "Environment :: Console", "Intended Audience :: Science/Research", "Intended Audience :: Information Technology", "Intended Audience :: End Users/Desktop", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Topic :: Software Development :: Libraries :: Python Modules", ] [tool.poetry.dependencies] python = "^3.6" httpx = "^0.7.4" pydantic = "^0.32.2" loguru = "^0.3.2" typing-extensions = "^3.7.4" tqdm = "^4.38.0" [tool.poetry.dev-dependencies] pytest = "^5.1.2" pytest-cov = "^2.7.1" pytest-mock = "^1.10.4" pre-commit = "^1.18.3" pytest-sugar = "^0.9.2" black = {version = "^19.3b0", allow-prereleases = true} ipython = "^7.8.0" flake8 = "^3.7.8" flake8-blind-except = "^0.1.1" flake8-commas = "^2.0.0" flake8-comprehensions = "^2.2.0" flake8-deprecated = "^1.3" flake8-mutable = "^1.2.0" flake8-tidy-imports = "^2.0.0" flake8-print = "^3.1.0" isort = "^4.3.21" mypy = "^0.740" mkdocs = "^1.0.4" mkdocs-material = "^4.4.3" mkautodoc = "^0.1.0" bump2version = "^0.5.11" [tool.black] line-length = 100 target-version = ['py36', 'py37', 'py38'] include = '\.pyi?$' exclude = ''' /( \.eggs | \.git | \.hg | \.mypy_cache | \.tox | \.venv | _build | buck-out | build | dist )/ ''' [build-system] requires = ["poetry>=0.12"] build-backend = "poetry.masonry.api" ================================================ FILE: tests/api/test_api.py ================================================ from tiktok_bot.api import TikTokAPI def test_encrypt_with_xor(api: TikTokAPI): assert api.encrypt_with_XOR("user@example.com") == "7076607745607d64687569602b666a68" assert api.encrypt_with_XOR("password") == "75647676726a7761" ================================================ FILE: tests/conftest.py ================================================ import pytest from tiktok_bot.api import TikTokAPI @pytest.fixture() def api(): return TikTokAPI() ================================================ FILE: tests/test_tiktok-bot.py ================================================ from tiktok_bot import __version__ def test_version(): assert __version__ == "0.6.4" ================================================ FILE: tiktok_bot/__init__.py ================================================ from .bot import TikTokBot from .client import client __all__ = ["TikTokBot", "client"] __version__ = "0.6.4" ================================================ FILE: tiktok_bot/api/__init__.py ================================================ from .api import TikTokAPI __all__ = ["TikTokAPI"] ================================================ FILE: tiktok_bot/api/api.py ================================================ import itertools from typing import List from loguru import logger from tqdm import tqdm from tiktok_bot.client import HTTPClient from tiktok_bot.models.category import ListCategoriesRequest, ListCategoriesResponse from tiktok_bot.models.feed import ListFeedRequest, ListFeedResponse, ListForYouFeedResponse from tiktok_bot.models.feed_enums import FeedType, PullType from tiktok_bot.models.follow import FollowRequest, FollowResponse from tiktok_bot.models.hashtag import ListPostsInHashtagRequest, ListPostsInHashtagResponse from tiktok_bot.models.login import LoginRequest, LoginResponse from tiktok_bot.models.post import Post from tiktok_bot.models.search import ( ChallengeInfo, HashtagSearchResponse, HashtagSearchResult, SearchRequest, UserSearchRequest, UserSearchResponse, UserSearchResult, ) from tiktok_bot.models.user import UserProfileResponse from .config import DEFAULT_HEADERS, DEFAULT_PARAMS class TikTokAPI: def __init__(self): self.client = HTTPClient( base_url="https://api2.musical.ly/", default_headers=DEFAULT_HEADERS, default_params=DEFAULT_PARAMS, ) @staticmethod def encrypt_with_XOR(value: str, key=5) -> str: return "".join([hex(int(x ^ key))[2:] for x in value.encode("utf-8")]) def login(self, login_request: LoginRequest) -> LoginResponse: """ FIXME: Under development """ url = "passport/user/login/" response = self.client.post(url=url, data=login_request.dict()) login = LoginResponse(**response.json()) return login def login_with_email(self, email: str, password: str, captcha: str = ""): """ FIXME: Under development """ email = self.encrypt_with_XOR(email) password = self.encrypt_with_XOR(password) request = LoginRequest(email=email, password=password, captcha=captcha) return self.login(request) def _list_for_you_feed(self, list_feed_request: ListFeedRequest) -> ListForYouFeedResponse: "Lists posts in the For You feed." url = "aweme/v1/feed/" response = self.client.get(url=url, params=list_feed_request.dict()) feed = ListForYouFeedResponse(**response.json()) return feed def list_for_you_feed(self, count: int) -> List[Post]: "Lists posts in the For You feed with paginate." feed: List[Post] = [] logger.info(f"Getting {count} posts from your feed") with tqdm(total=count, desc="List for you feed") as pbar: for cursor in itertools.count(start=0, step=6): request = ListFeedRequest( count=count, max_cursor=cursor, pull_type=PullType.LoadMore, type=FeedType.ForYou, is_cold_start=1, ) response = self._list_for_you_feed(list_feed_request=request) feed += response.aweme_list pbar.update(len(response.aweme_list)) if not response.has_more or len(feed) >= count: feed = feed[:count] logger.info(f"Found {len(feed)} results") break return feed def list_following_feed(self, list_feed_request: ListFeedRequest) -> ListFeedResponse: "Lists posts in the Following feed." url = "aweme/v1/feed/" response = self.client.get(url=url, params=list_feed_request.dict()) feed = ListFeedResponse(**response.json()) return feed def list_categories( self, list_categories_request: ListCategoriesRequest ) -> ListCategoriesResponse: "Lists popular categories/hashtags." url = "aweme/v1/category/list/" response = self.client.get(url=url, params=list_categories_request.dict()) categories = ListCategoriesResponse(**response.json()) return categories def get_user(self, user_id: str) -> UserProfileResponse: "Gets a user's profile." url = "aweme/v1/user/" response = self.client.get(url=url, params={"user_id": user_id}) user = UserProfileResponse(**response.json()) return user def _search_users(self, user_search_request: UserSearchRequest) -> UserSearchResponse: "Searches for users." url = "aweme/v1/discover/search/" response = self.client.get(url=url, params=user_search_request.dict()) user_search = UserSearchResponse(**response.json()) return user_search def search_users(self, keyword: str, count: int) -> List[UserSearchResult]: "Searches for users with paginate." results: List[UserSearchResult] = [] logger.info(f'Search {count} users with keyword: "{keyword}"') with tqdm(total=count, desc="Searching users") as pbar: for cursor in itertools.count(start=0, step=10): user_search_request = UserSearchRequest(keyword=keyword, cursor=cursor) response = self._search_users(user_search_request=user_search_request) results += response.user_list pbar.update(len(response.user_list)) if not response.has_more or len(results) >= count: results = results[:count] logger.info(f"Found {len(results)} results") break return results def _search_posts_by_hashtag( self, search_request: ListPostsInHashtagRequest ) -> ListPostsInHashtagResponse: "Search posts by hashtag id." url = "aweme/v1/challenge/aweme/" response = self.client.get(url=url, params=search_request.dict()) search = ListPostsInHashtagResponse(**response.json()) return search def search_posts_by_hashtag(self, hashtag: ChallengeInfo, count: int) -> List[Post]: "Search posts by hashtag with paginate." results: List[Post] = [] logger.info(f'Search {count} posts with hashtag: "{hashtag.cha_name}"') with tqdm(total=count, desc="Searching posts") as pbar: for cursor in itertools.count(start=0, step=10): search_request = ListPostsInHashtagRequest(ch_id=hashtag.cid, cursor=cursor) response = self._search_posts_by_hashtag(search_request) results += response.aweme_list pbar.update(len(response.aweme_list)) if not response.has_more or len(results) >= count: results = results[:count] logger.info(f"Found {len(results)} results") break return results def _search_hashtags(self, hashtag_search_request: SearchRequest) -> HashtagSearchResponse: "Searches for hashtags." url = "aweme/v1/challenge/search/" response = self.client.get(url=url, params=hashtag_search_request.dict()) hashtag_search = HashtagSearchResponse(**response.json()) return hashtag_search def _follow(self, request: FollowRequest) -> FollowResponse: "Send follow request." url = "aweme/v1/commit/follow/user/" response = self.client.get(url=url, params=request.dict()) follow = FollowResponse(**response.json()) return follow def search_hashtags(self, keyword: str, count: int) -> List[HashtagSearchResult]: "Searches for hashtags with paginate." results: List[HashtagSearchResult] = [] logger.info(f'Search {count} hashtags with keyword: "{keyword}"') with tqdm(total=count, desc="Searching hashtags") as pbar: for cursor in itertools.count(start=0, step=10): search_request = SearchRequest(keyword=keyword, cursor=cursor) response = self._search_hashtags(search_request) results += response.challenge_list pbar.update(len(response.challenge_list)) if not response.has_more or len(results) >= count: results = results[:count] logger.info(f"Found {len(results)} results") break return results ================================================ FILE: tiktok_bot/api/config.py ================================================ # ToDo: Make it configurable DEFAULT_PARAMS = { "os_api": "23", "device_type": "Pixel", "ssmix": "a", "manifest_version_code": "2018111632", "dpi": 420, "app_name": "musical_ly", "version_name": "9.1.0", "is_my_cn": 0, "ac": "wifi", "update_version_code": "2018111632", "channel": "googleplay", "device_platform": "android", "build_number": "9.9.0", "version_code": 910, "timezone_name": "America/New_York", "timezone_offset": 36000, "resolution": "1080*1920", "os_version": "7.1.2", "device_brand": "Google", "mcc_mnc": "23001", "is_my_cn": 0, "fp": "", "app_language": "en", "language": "en", "region": "US", "sys_region": "US", "account_region": "US", "carrier_region": "US", "carrier_region_v2": "505", "aid": "1233", "pass-region": 1, "pass-route": 1, "app_type": "normal", # "iid": "6742828344465966597", # "device_id": "6746627788566021893", # ToDo: Make it dynamic "iid": "6749111388298184454", "device_id": "6662384847253865990", } DEFAULT_HEADERS = { "Host": "api2.musical.ly", "X-SS-TC": "0", "User-Agent": f"com.zhiliaoapp.musically/{DEFAULT_PARAMS['manifest_version_code']}" + f" (Linux; U; Android {DEFAULT_PARAMS['os_version']};" + f" {DEFAULT_PARAMS['language']}_{DEFAULT_PARAMS['region']};" + f" {DEFAULT_PARAMS['device_type']};" + " Build/NHG47Q; Cronet/58.0.2991.0)", "Accept-Encoding": "gzip", "Accept": "*/*", "Connection": "keep-alive", "X-Tt-Token": "", "sdk-version": "1", "Cookie": "null = 1;", } ================================================ FILE: tiktok_bot/bot/__init__.py ================================================ from .bot import TikTokBot __all__ = ["TikTokBot"] ================================================ FILE: tiktok_bot/bot/bot.py ================================================ import sys from typing import List from loguru import logger from typing_extensions import Literal from tiktok_bot.api import TikTokAPI from tiktok_bot.models.category import Category, ListCategoriesRequest from tiktok_bot.models.feed import ListFeedRequest from tiktok_bot.models.feed_enums import FeedType, PullType from tiktok_bot.models.post import Post from tiktok_bot.models.search import ChallengeInfo from tiktok_bot.models.user import CommonUserDetails, UserProfile class TikTokBot: def __init__(self, log_level: Literal["INFO", "DEBUG"] = "INFO"): self.api = TikTokAPI() logger.remove() logger.add(sys.stderr, level=log_level) def list_categories(self, count: int = 10, cursor: int = 0) -> List[Category]: request = ListCategoriesRequest(count=count, cursor=cursor) categories = self.api.list_categories(request) return categories.category_list def get_user_by_id(self, user_id: str) -> UserProfile: user_response = self.api.get_user(user_id=user_id) return user_response.user def search_users(self, keyword: str, count: int = 6) -> List[CommonUserDetails]: users_search = self.api.search_users(keyword=keyword, count=count) users = [user.user_info for user in users_search] return users def search_hashtags(self, keyword: str, count: int = 6) -> List[ChallengeInfo]: hashtags_search = self.api.search_hashtags(keyword=keyword, count=count) hashtags = [tag.challenge_info for tag in hashtags_search] return hashtags def search_posts_by_hashtag(self, hashtag_name: str, count: int = 6) -> List[Post]: tags = self.search_hashtags(keyword=hashtag_name, count=1) if not tags: logger.info(f'Tag "{hashtag_name}" not found') return [] posts = self.api.search_posts_by_hashtag(hashtag=tags[0], count=count) return posts def list_for_you_feed(self, count: int = 6) -> List[Post]: feed = self.api.list_for_you_feed(count=count) return feed def list_following_feed(self, count: int = 6, cursor: int = 0) -> List[Post]: """ Lists posts in the Following feed. * Login required """ request = ListFeedRequest( count=count, max_cursor=cursor, pull_type=PullType.LoadMore, type=FeedType.Following, is_cold_start=1, ) feed = self.api.list_following_feed(request) return feed.aweme_list ================================================ FILE: tiktok_bot/client/__init__.py ================================================ from .client import HTTPClient __all__ = ["HTTPClient"] ================================================ FILE: tiktok_bot/client/client.py ================================================ from collections import deque from time import time from typing import Deque, Optional from uuid import uuid4 from httpx import Client, Response from loguru import logger from .utils import generate_as, generate_cp, generate_mas class HTTPClient: def __init__( self, base_url: str, default_headers: Optional[dict] = None, default_params: Optional[dict] = None, history_len: int = 30, ): self.base_url = base_url self.default_headers = default_headers or {} self.default_params = default_params or {} self.history: Deque[Response] = deque(maxlen=history_len) self.http_client = Client( base_url=self.base_url, headers=default_headers, params=self.default_params ) def get(self, url: str, params: dict, headers: Optional[dict] = None): custom_headers = headers or {} all_params = {**self._generate_params(), **params} logger.debug(f"Sending request to {url}", params=all_params, custom_headers=custom_headers) response = self.http_client.get(url=url, params=all_params, headers=custom_headers) self.history.append(response) body = response.text or "is empty!" logger.debug(f"Response return status_code: {response.status_code}, body: {body}") for cookie_name, cookie_data in response.cookies.items(): self.http_client.cookies.set(cookie_name, cookie_data) logger.debug(f"New cookies: {dict(response.cookies)}") return response def post( self, url: str, data: dict, headers: Optional[dict] = None, params: Optional[dict] = None ): custom_headers = headers or {} custom_params = params or {} # merge parameters all_params = {**self._generate_params(), **custom_params} logger.debug( f"Sending request to {url}", params=all_params, custom_headers=custom_headers, data=data ) response = self.http_client.post( url=url, params=all_params, data=data, headers=custom_headers, ) self.history.append(response) body = response.text or "is empty!" logger.debug(f"Response return status_code: {response.status_code}, body: {body}") for cookie_name, cookie_data in response.cookies.items(): self.http_client.cookies.set(cookie_name, cookie_data) logger.debug(f"New cookies: {dict(response.cookies)}") return response def _generate_params(self): now = str(int(round(time() * 1000))) params = { "_rticket": now, "ts": now, "mas": generate_mas(now), "as": generate_as(now), "cp": generate_cp(now), "idfa": str(uuid4()).upper(), } return params ================================================ FILE: tiktok_bot/client/utils.py ================================================ from hashlib import md5, sha1 from time import time def generate_as(now: str) -> str: as_md5 = md5(now.encode()).hexdigest() return as_md5 def generate_cp(now: str) -> str: now += str(time()) cp_md5 = md5(now.encode()).hexdigest() return cp_md5 def generate_mas(now: str) -> str: mas_sha = sha1(now.encode()).hexdigest() mas_md5 = md5(mas_sha.encode()).hexdigest() return mas_md5 ================================================ FILE: tiktok_bot/models/__init__.py ================================================ ================================================ FILE: tiktok_bot/models/category.py ================================================ from typing import List, Union from pydantic import BaseModel, Schema from .post import Post from .request import CountOffsetParams, ListRequestParams, ListResponseData from .user import CommonUserDetails class ChallengeInfo(BaseModel): # The user who created the challenge, or an empty object author: Union[CommonUserDetails, BaseModel] # The name of the challenge cha_name: str # The ID of the challenge cid: str # A description of the challenge desc: str # ??? is_pgcshow: bool # An in-app link to the challenge schema_: str = Schema(default=..., alias="schema") # The type of challenge - 0 for hashtag? type: int # The number of users who have uploaded a video for the challenge user_count: int class Config: fields = {"schema_": "schema"} class Category(BaseModel): # A list of posts in the category aweme_list: List[Post] # The type of category - 0 for hashtag? category_type: int # Information about the category challenge_info: ChallengeInfo # A description of the category type, e.g. "Trending Hashtag" desc: str class ListCategoriesRequest(ListRequestParams, CountOffsetParams): pass class ListCategoriesResponse(ListResponseData, CountOffsetParams): # A list of categories category_list: List[Category] ================================================ FILE: tiktok_bot/models/comment.py ================================================ from typing import List, Optional from pydantic import BaseModel from typing_extensions import Literal from .request import BaseResponseData, CountOffsetParams, ListRequestParams, ListResponseData from .tag import Tag from .user import CommonUserDetails class Comment(BaseModel): # The ID of the post aweme_id: str # The ID of the comment cid: str # The timestamp in seconds when the comment was posted create_time: int # The number of times the comment has been liked digg_count: int # If this comment is replying to a comment, this array contains the original comment reply_comment: Optional[List["Comment"]] = None # If this comment is replying to a comment, the ID of that comment - "0" if not a reply reply_id: str # The status of the comment - 1 = published, 4 = published by you? status: int # The comment text text: str # Details about any tags in the comment text_extra: List[Tag] # Details about the author user: CommonUserDetails # 1 if the user likes the comment user_digged: Literal[0, 1] class ListCommentsRequest(ListRequestParams, CountOffsetParams): # The ID of the post to list comments for aweme_id: str # ??? - default is 2 comment_style: Optional[int] = None # ??? digged_cid = None # ??? insert_cids = None class ListCommentsResponse(ListResponseData, CountOffsetParams): comments: List[Comment] class PostCommentRequest(BaseModel): # The ID of the post to comment on aweme_id: str # The comment text text: str # The ID of the comment that is being replied to reply_id: Optional[str] = None # Details about any tags in the comment text_extra: List[Tag] # ??? is_self_see: Literal[0, 1] class PostCommentResponse(BaseResponseData): # The comment that was posted comment: Comment ================================================ FILE: tiktok_bot/models/feed.py ================================================ from typing import List, Optional from .feed_enums import FeedType, PullType from .post import Post from .request import ( CursorOffsetRequestParams, CursorOffsetResponseParams, ListRequestParams, ListResponseData, ) class ListFeedRequest(ListRequestParams, CursorOffsetRequestParams): # The type of feed to load type: FeedType # Your device's current volume level on a scale of 0 to 1, e.g. 0.5 volume: float = 0.5 # How the feed was requested pull_type: PullType # ??? - empty req_from: Optional[str] = None # ??? - 0 is_cold_start: Optional[int] = None # ??? gaid: Optional[str] = None # A user agent for your device ad_user_agent: Optional[str] = None class Config: use_enum_values = True class ListFeedResponse(ListResponseData, CursorOffsetResponseParams): # A list of posts in the feed aweme_list: List[Post] class ListForYouFeedResponse(ListFeedResponse): # ??? - 1 home_model: int # ??? - 1 refresh_clear: int ================================================ FILE: tiktok_bot/models/feed_enums.py ================================================ from enum import IntEnum class FeedType(IntEnum): ForYou = 0 Following = 1 class PullType(IntEnum): # The feed was loaded by default, e.g. by clicking the tab or loading the app Default = 0 # The feed was explicitly refreshed by the user, e.g. by swiping down Refresh = 1 # More posts were requested by the user, e.g. by swiping up LoadMore = 2 ================================================ FILE: tiktok_bot/models/follow.py ================================================ from typing import List from pydantic import BaseModel from typing_extensions import Literal from .request import ( BaseResponseData, ListRequestParams, ListResponseData, TimeOffsetRequestParams, TimeOffsetResponseParams, ) from .user import CommonUserDetails class FollowRequest(BaseModel): # The id of the user to follow user_id: str # 0 to unfollow, 1 to follow type: Literal[0, 1] class FollowResponse(BaseResponseData): # 0 if not following, 1 if following follow_status: Literal[0, 1] # 0 if not watching, 1 if watching watch_status: Literal[0, 1] class ListReceivedFollowRequestsRequest(ListRequestParams, TimeOffsetRequestParams): pass class ListReceivedFollowRequestsResponse(ListResponseData, TimeOffsetResponseParams): # A list of users who have requested to follow you request_users: List[CommonUserDetails] class ApproveFollowRequest(BaseModel): # The id of the user to approve from_user_id: str class ApproveFollowResponse(BaseResponseData): # 0 if the user was successfully approved approve_status: int class RejectFollowRequest(BaseModel): # The id of the user to reject from_user_id: str class RejectFollowResponse(BaseResponseData): # 0 if the user was successfully rejected reject_status: int ================================================ FILE: tiktok_bot/models/follower.py ================================================ from typing import List from .request import ( ListRequestParams, ListResponseData, TimeOffsetRequestParams, TimeOffsetResponseParams, ) from .user import CommonUserDetails class ListFollowersRequest(ListRequestParams, TimeOffsetRequestParams): # The id of the user whose followers to retrieve user_id: str class ListFollowersResponse(ListResponseData, TimeOffsetResponseParams): # A list of the user's followers followers: List[CommonUserDetails] class ListFollowingRequest(ListRequestParams, TimeOffsetRequestParams): # The id of the user whose followers to retrieve user_id: str class ListFollowingResponse(ListResponseData, TimeOffsetResponseParams): # A list of users the user is following followings: List[CommonUserDetails] ================================================ FILE: tiktok_bot/models/hashtag.py ================================================ from typing import List from .post import Post from .request import CountOffsetParams, ListRequestParams, ListResponseData class ListPostsInHashtagRequest(ListRequestParams, CountOffsetParams): # The ID of the hashtag ch_id: str # ??? - set to 0 query_type: int = 0 # ??? - set to 5 type: int = 5 class ListPostsInHashtagResponse(ListResponseData, CountOffsetParams): # A list of posts containing the hashtag aweme_list: List[Post] ================================================ FILE: tiktok_bot/models/like.py ================================================ from typing_extensions import Literal from .request import BaseResponseData class LikePostRequest(BaseResponseData): # The id of the post to like aweme_id: str # 0 to unlike, 1 to like type: Literal[0, 1] class LikePostResponse(BaseResponseData): # # 0 if liked, 1 if not liked # # Note: for some reason, this value is the opposite of what you would expect is_digg: Literal[0, 1] ================================================ FILE: tiktok_bot/models/login.py ================================================ from typing import List, Union from pydantic import BaseModel class LoginRequest(BaseModel): # Unsure, but looks to be hard-coded to 1 mix_mode: int = 1 # The unique username ("username") of the user username: str = "" # The email address associated with the user account email: str = "" # The mobile number associated with the user account mobile: str = "" # ??? account: str = "" # The password to the user account password: str = "" # The captcha answer - only required if a captcha was shown captcha: str = "" class LoginSuccessData(BaseModel): # ??? area: str # The URL of the user's avatar avatar_url: str # ??? bg_img_url: str # The user's birthday birthday: str # If the user allows people to find them by their phone number can_be_found_by_phone: int # ??? connects: List[BaseModel] # ??? description: str # The email address associated with the account email: str # The number of users that follow the user followers_count: int # The number of users the user is following followings_count: int # An integer representing the gender of the user gender: int # ??? industry: str # Indicates if the user account is blocked is_blocked: int # ??? is_blocking: int # ??? is_recommend_allowed: int # ??? media_id: int # The mobile number of the user mobile: str # The name of the user - does not appear to be used name: str # Indicates if the user is new or not new_user: int # A Chinese character hint recommend_hint_message: str # The screen name of the user - does not appear to be used screen_name: str # The session ID used to authenticate subsequent requests in the sessionid cookie session_key: str # ??? skip_edit_profile: int # ??? user_auth_info: str # The ID of the user user_id: str # If the user is verified or not user_verified: bool # ??? verified_agency: str # ??? verified_content: str # The number of users that have visited the user's profile recently visit_count_recent: int class LoginErrorData(BaseModel): # If required, the captcha that must solved captcha: str # A message explaining why the request failed description: str # An error code error_code: int class LoginResponse(BaseModel): data: Union[LoginSuccessData, LoginErrorData] # A message indicating whether the request was successful or not message: str ================================================ FILE: tiktok_bot/models/music.py ================================================ from typing import Optional from pydantic import BaseModel from .request import Media class MusicTrack(BaseModel): # The name of the musician author: str # A HD version of the music's cover art cover_hd: Optional[Media] # A large version of the music's cover art cover_large: Optional[Media] # A medium version of the music's cover art cover_medium: Optional[Media] # A thumbnail version of the music's cover art cover_thumb: Media # The duration of the track duration: int # The ID of the track id: str # The handle of the owner of the track owner_handle: Optional[str] # The ID of the owner of the track owner_id: Optional[str] # The nickname of the owner of the track owner_nickname: Optional[str] # The link to play this track play_url: Media # The title of this track title: str # The number of posts that use this track user_count: int ================================================ FILE: tiktok_bot/models/post.py ================================================ from typing import List, Optional from pydantic import BaseModel from .music import MusicTrack from .request import ( BaseResponseData, CursorOffsetRequestParams, CursorOffsetResponseParams, ListRequestParams, ListResponseData, ) from .user import CommonUserDetails from .video import Video class PostStatistics(BaseModel): # The ID of the post aweme_id: str # The number of comments on the post comment_count: int # The number of times the post has been liked digg_count: int # The number of times the post has been forwarded (looks unused?) forward_count: Optional[int] # The number of times the post has been viewed - doesn't appear to be public, so always 0 play_count: int # The number of times the post has been shared share_count: int class PostStatus(BaseModel): # True if the post allows comments allow_comment: bool # True if the post allows sharing allow_share: bool # 0 if the post can be downloaded download_status: int # True if the post is currently being reviewed in_reviewing: Optional[bool] # True if the post has been deleted is_delete: bool # True if the post is private is_private: bool # True if the post contains content that is not allowed on the platform is_prohibited: Optional[bool] # 0 if the post is public private_status: Optional[int] # 1 if the post has been reviewed reviewed: Optional[int] class PostTags(BaseModel): # 0 if the tag is for a user; 1 if the tag is for a hashtag type: int # The name of the hashtag hashtag_name: Optional[str] # The ID of the tagged user user_id: Optional[str] class RiskInfo(BaseModel): # The text shown if the post has been flagged content: str # ??? risk_sink: bool = False # The risk type associated with the post - 0 if no risk; 1 if low; 2 if high type: int # ??? - only present if the post has been flagged vote: Optional[bool] # True if a warning should be shown to the user warn: bool class ShareInfo(BaseModel): # ??? bool_persist: Optional[int] # The description used when sharing (if set) share_desc: str # The description used when sharing a link only (if set) share_link_desc: Optional[str] # The quote used when sharing (if set) share_quote: Optional[str] # The signature used when sharing (if set) share_signature_desc: Optional[str] # The signature URL used when sharing (if set) share_signature_url: Optional[str] # The title used when sharing share_title: str # The link to share share_url: str # The description used when sharing on Weibo share_weibo_desc: str class StickerInfo(BaseModel): # The ID of the sticker, e.g. 22094 id: str # The display name of the sticker, e.g. Long Face name: str class Post(BaseModel): # Details about the author author: Optional[CommonUserDetails] # The ID of the author author_user_id: str # The ID of the post aweme_id: str # The type of post - 0 for a musical.ly aweme_type: int # The timestamp in seconds when the post was created create_time: int # A description of the post desc: str # Details about the music used in the post music: Optional[MusicTrack] # True if the end user should not be provided the option to download the video prevent_download: Optional[bool] # An age rating for the post, e.g. 12 rate: int # The 2-letter region the post was created in, e.g. US region: str # Risk information about the post risk_infos: Optional[RiskInfo] # Information used when sharing the post share_info: Optional[ShareInfo] # A link to the video on the musical.ly website that is used when sharing share_url: str # Statistics about the post statistics: PostStatistics # Status information about the post status: PostStatus # Information about the sticker used in the post sticker_detail: Optional[StickerInfo] # The ID of the sticker used in the post (looks to be deprecated by sticker_detail) stickers: Optional[str] # Tagged users and hashtags used in the description text_extra: List[PostTags] # 1 if the logged in user has liked this post user_digged: int # Details about the video in the post video: Video @property def video_url(self): url = filter(lambda url: "watermark" in url, self.video.download_addr.url_list) return next(url) @property def video_url_without_watermark(self): return self.video_url.replace("watermark=1", "watermark=0") class GetPostResponse(BaseResponseData): aweme_detail: Post class ListPostsRequest(ListRequestParams, CursorOffsetRequestParams): # The id of the user whose posts to retrieve user_id: str class ListPostsResponse(ListResponseData, CursorOffsetResponseParams): aweme_list: List[Post] ================================================ FILE: tiktok_bot/models/qr-code.py ================================================ from typing import List from pydantic import BaseModel from .request import BaseResponseData class QRCodeRequest(BaseModel): # The internal version to use; currently 4 schema_type: int # The ID of the user to get a QR code for object_id: str class QRCodeUrl(BaseModel): # An in-app link to the QR code uri: str # Contains a public link to the QR code image (first element in array) url_list: List[str] class QRCodeResponse(BaseResponseData): # Contains a link to the QR code qrcode_url: QRCodeUrl ================================================ FILE: tiktok_bot/models/request.py ================================================ import abc from typing import List, Optional, Union from pydantic import BaseModel class RequiredUserDefinedRequestParams(BaseModel, abc.ABC): # The 16-character ID of your installation, e.g. 4549764744226841084 iid: str # A 16-character hexadecimal identifier associated with your device, e.g. 4b903fbb9d457937 openudid: str # The ID of your device that has already been registered with musical.ly device_id: str # An anti-fraud fingerprint of your device requested from a different API fp: str class StaticRequestParams(RequiredUserDefinedRequestParams): # Your Android version, e.g. 23 os_api: str # Your device model, e.g. Pixel 2 device_type: str # ??? - set to "a" ssmix: str # The SS_VERSION_CODE metadata value from the AndroidManifest.xml file, e.g. 2018060103 manifest_version_code: str # Your device's pixel density, e.g. 480 dpi: int # The application name - hard-coded to "musical_ly" app_name: str # The SS_VERSION_NAME metadata value from the AndroidManifest.xml file, e.g. 7.2.0 version_name: str # The UTC offset in seconds of your timezone, e.g. 37800 for Australia/Lord_Howe timezone_offset: int # ??? - are we in China / using the Chinese version? Set to 0 is_my_cn: int # Network connection type, e.g. "wifi" ac: str # The UPDATE_VERSION_CODE metadata value from the AndroidManifest.xml file, e.g. 2018060103 update_version_code: str # The channel you downloaded the app through, e.g. googleplay channel: str # Your device's platform, e.g. android device_platform: str # The build int of the application, e.g. 7.2.0 build_number: str # A numeric version of the version_name metadata value, e.g. 720 version_code: int # The name of your timezone as per the tz database, e.g. Australia/Sydney timezone_name: str # The region of the account you are logging into, e.g. AU. # This field is only present if you are logging in from a device that hasn't had a # user logged in before. account_region: Optional[str] = None # Your Optional[str]ice's resolution, e.g. 1080*1920 resolution: str # Your device's operating system version, e.g. 8.0.0 os_version: str # Your device's brand, e.g. Google device_brand: str # ??? - empty mcc_mnc: str # The application's two-letter language code, e.g. en app_language: str # Your i18n language, e.g. en language: str # Your region's i18n locale, e.g. US region: str # Your device's i18n locale, e.g. US sys_region: str # Your carrier's region (a two-letter country code), e.g. AU carrier_region: str # You carrer's mobile country code (MCC), e.g. 505 carrier_region_v2: str # A hard-coded i18n constant set to "1233" aid: str # ??? - set to 1 pass_region: int # ??? - set to 1 pass_route: int class Config: fields = {"pass_region": "pass-region", "pass_route": "pass-route"} class AntiSpamParams(BaseModel): # A 20-character anti-spam parameter as_: str # A 20-character anti-spam parameter cp: str # An encoded version of the 'as' anti-spam parameter mas: str class Config: fields = {"as_": "as"} class BaseRequestParams(StaticRequestParams, AntiSpamParams): # The current timestamp in seconds since UNIX epoch ts: int # The current timestamp in milliseconds since UNIX epoch _rticket: str class ListRequestParams(BaseModel): # The number of results to return count: int = 10 # How the request will be retried on failure - defaults to "no_retry" retry_type: Optional[str] = None class TimeOffsetRequestParams(BaseModel): """ A timestamp in seconds - the most recent results before this time will be listed. Use min_time from the response data here for pagination. """ max_time: int class TimeOffsetResponseParams(BaseModel): # The timestamp in seconds associated with the first result max_time: int # The timestamp in seconds associated with the last result - use as max_time for pagination min_time: int class CursorOffsetRequestParams(BaseModel): """ A timestamp in milliseconds - the most recent results before this time will be listed. Use max_cursor from the response data here for pagination. Use 0 for the most recent. """ max_cursor: int class CursorOffsetResponseParams(BaseModel): # The timestamp in milliseconds associated with the first result min_cursor: int # The timestamp in milliseconds associated with the last result - use for pagination max_cursor: int class CountOffsetParams(BaseModel): # The number of results to skip cursor: int = 0 class ExtraResponseData(BaseModel): # ??? fatal_item_ids: Optional[List[int]] = None # A log ID for this request logid: Optional[str] = None # The current timestamp in milliseconds now: int class BaseResponseData(BaseModel, abc.ABC): # 0 if the request was successful status_code: int extra: ExtraResponseData class ListResponseData(BaseResponseData): # Whether there are more results that can be requested has_more: Union[bool, int] # The total number of results returned - not present in all list requests total: Optional[int] = None class Media(BaseModel): # A list of HTTP URLs to this media url_list: List[str] ================================================ FILE: tiktok_bot/models/search.py ================================================ from typing import List, Optional from pydantic import BaseModel from typing_extensions import Literal from .category import ChallengeInfo from .request import CountOffsetParams, ListRequestParams, ListResponseData from .user import CommonUserDetails class SearchRequest(ListRequestParams, CountOffsetParams): # The term to search for keyword: str class UserSearchRequest(SearchRequest): # Required - the scope of the search - users = 1. type: int = 1 class SubstringPosition(BaseModel): """ Represents the location of a substring in a string. e.g. For the string "The quick brown fox", the substring "quick" would be: { begin: 4, end: 8 } """ # The start index of the substring begin: int # The end index of the substring end: int class UserSearchResult(BaseModel): # If the user's nickname contains the search term, this array contains the location of the term position: Optional[List[SubstringPosition]] = None # If the user's username (unique_id) contains the search term, # this array contains the location of the term uniqid_position: Optional[List[SubstringPosition]] = None # Information about the user user_info: CommonUserDetails class UserSearchResponse(ListResponseData, CountOffsetParams): # A list of users that match the search term user_list: List[UserSearchResult] # The scope of the search - users = 1 type: int class HashtagSearchResult(BaseModel): # Information about the hashtag challenge_info: ChallengeInfo # If the hashtag contains the search term, this array contains the location of the term position: Optional[List[SubstringPosition]] = None class HashtagSearchResponse(ListResponseData, CountOffsetParams): # A list of hashtags that match the search term challenge_list: List[HashtagSearchResult] # True if a challenge matches the search term is_match: bool # 1 if the search term is disabled keyword_disabled: Literal[0, 1] ================================================ FILE: tiktok_bot/models/sticker.py ================================================ from typing import Any, List from pydantic import BaseModel from .post import Post from .request import BaseResponseData, CountOffsetParams, ListRequestParams, ListResponseData, Media class Sticker(BaseModel): # ??? children: Any # A description of the sticker desc: str # The ID of the sticker effect_id: str # The icon associated with the sticker icon_url: Media # The ID of the sticker id: str # True if the current user has favorited the sticker is_favorite: bool # The name of the sticker name: str # The ID the user that owns the sticker (empty if owned by the Effect Assistant) owner_id: str # The nickname of the owner, e.g. "Effect Assistant" owner_nickname: str # ??? tags: List[Any] # The total number of posts using this sticker user_count: int class ListPostsByStickerRequest(ListRequestParams, CountOffsetParams): # The ID of the sticker sticker_id: str class ListPostsByStickerResponse(ListResponseData, CountOffsetParams): # A list of posts using the sticker aweme_list: List[Post] # Currently empty stickers: List[Any] class GetStickersRequest(BaseModel): # A list of sticker ids to get information about sticker_ids: str class GetStickersResponse(BaseResponseData): sticker_infos: List[Sticker] ================================================ FILE: tiktok_bot/models/tag.py ================================================ """ Represents a text link to a user, e.g. "@username" in a comment """ from pydantic import BaseModel class Tag(BaseModel): # The type of user being tagged? at_user_type: str # The zero-based index in the text where the tag starts end: int # The zero-based index in the text where the tag ends start: int # The type of tag? type: int # The ID of the user being tagged user_id: str ================================================ FILE: tiktok_bot/models/user.py ================================================ from typing import Optional, Union from pydantic import BaseModel from .request import BaseResponseData, Media class CommonUserDetails(BaseModel): # A large version of the user's avatar avatar_larger: Media # A medium version of the user's avatar avatar_medium: Media # A thumbnail version of the user's avatar avatar_thumb: Media # The timestamp in seconds when the user's account was created create_time: Optional[int] = None # The badge name with a verified user (e.g. comedian, style guru) custom_verify: str # 1 if you follow this user follow_status: int # 1 if this user follows you follower_status: int # The user's Instagram handle ins_id: str # Indicates if the user has been crowned is_verified: bool # The user's profile name nickname: str # A 2-letter country code representing the user's region, e.g. US region: str # If the user is live, a str ID used to join their stream, else 0 room_id: Optional[Union[str, int]] = None # 1 if the user's profile is set to private secret: int # The user's profile signature signature: str # The user's Twitter handle twitter_id: str # The user's ID uid: str # The user's musername unique_id: str # 1 if the user has been crowned verification_type: int # The user's YouTube channel ID youtube_channel_id: str class UserProfile(CommonUserDetails): # The number of videos the user has uploaded aweme_count: int # The number of videos the user has liked favoriting_count: int # The number of users who follow this user follower_count: int # The number of users this user follows following_count: int # The total number of likes the user has received total_favorited: int class UserProfileResponse(BaseResponseData): user: UserProfile ================================================ FILE: tiktok_bot/models/video.py ================================================ from pydantic import BaseModel from .request import Media class Video(BaseModel): # A medium version of the video's cover image cover: Media # A high-quality link to download the video download_addr: Media # The video's duration in milliseconds duration: int # Whether the download link has a watermark has_watermark: bool # The video's height, e.g. 960 height: int # A high-quality version of the video's cover image origin_cover: Media # The quality of the video, e.g. 720p ratio: str # The video's width, e.g. 540 width: int