├── requirements.txt ├── tiktokpy ├── __init__.py ├── tiktokpy.py ├── settings.py └── api.py ├── .gitignore ├── example.py └── README.MD /requirements.txt: -------------------------------------------------------------------------------- 1 | requests -------------------------------------------------------------------------------- /tiktokpy/__init__.py: -------------------------------------------------------------------------------- 1 | from .tiktokpy import TikTok, need_to_login 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # 2 | venv/ 3 | 4 | templates/ 5 | .vscode/ 6 | .idea/ 7 | .DS_Store 8 | 9 | tiktokpy/__pycache__/ 10 | 11 | temp.py 12 | 13 | -------------------------------------------------------------------------------- /example.py: -------------------------------------------------------------------------------- 1 | import tiktokpy 2 | import os 3 | 4 | username = os.environ['username'] 5 | password = os.environ['password'] 6 | 7 | tiktok = tiktokpy.TikTok(username, password) 8 | 9 | # go and follow on 30 my followers 10 | tiktok.following_followers(30) 11 | 12 | # search posts with hashtag 'cats' and like it 13 | tiktok.like_search_results('cats') 14 | 15 | # like 20 (default) posts of my followers 16 | tiktok.like_followers_posts() 17 | -------------------------------------------------------------------------------- /README.MD: -------------------------------------------------------------------------------- 1 | # TikTokPy 2 | 3 | ***Disclaimer: this is a research project. Made for anylizing. Author do not reponsible if your account be blocked with that instrument*** 4 | 5 | Now it's a very small project with few functions, 6 | so I call it test project. 7 | 8 | And now available functions: 9 | ``` 10 | login 11 | 12 | get_followers_list 13 | 14 | search_hashtag 15 | 16 | like_followers_posts 17 | 18 | following_followers 19 | 20 | like_search_results 21 | 22 | ``` 23 | 24 | Small example you can find in example.py 25 | 26 | To bee continued 27 | -------------------------------------------------------------------------------- /tiktokpy/tiktokpy.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from . import api 4 | 5 | 6 | def need_to_login(func): 7 | """ 8 | Decorator for login function 9 | """ 10 | def wrapper(self, *args, **kwargs): 11 | if not self.cookies.get('user_id') or need_to_login: 12 | response = api.login(self.username, self.password) 13 | if response: 14 | self.cookies.update(response['data']) 15 | if self.cookies.get('user_id'): 16 | return func(self, *args, **kwargs) 17 | logging.debug('Wrong username or password') 18 | raise Exception('Wrong username or password') 19 | return wrapper 20 | 21 | 22 | class TikTok(): 23 | 24 | def __init__(self, username, password): 25 | self.cookies = {} 26 | self.username = username 27 | self.password = password 28 | 29 | def login(self): 30 | response = api.login(self.username, self.password) 31 | if response: 32 | self.cookies.update(response) 33 | 34 | def get_followers_list(self, user_id=None, count=20): 35 | user_id = user_id if user_id else self.cookies.get('user_id') 36 | return api.get_followers_list(user_id, count) 37 | 38 | def search_hashtag(self, text, count=20): 39 | return api.get_search_results('challenge', text, count) 40 | 41 | @need_to_login 42 | def like_followers_posts(self, count=20): 43 | followers = self.get_followers_list(count)['followers'] 44 | if not followers: 45 | return 46 | for follower in followers['followers']: 47 | posts = api.get_posts(follower.uid) 48 | if not posts: 49 | continue 50 | for post in posts['aweme_list']: 51 | api.like_post(post['aweme_id']) 52 | 53 | @need_to_login 54 | def following_followers(self, count): 55 | followers = self.get_followers_list(count) 56 | for follower in followers['followers']: 57 | api.follow(follower.uid) 58 | 59 | @need_to_login 60 | def like_search_results(self, text, count=20): 61 | search_results = self.search_hashtag(text, count) 62 | if not search_results: 63 | return 64 | for post in search_results['aweme_list']: 65 | api.like_post(post['aweme_id']) 66 | 67 | -------------------------------------------------------------------------------- /tiktokpy/settings.py: -------------------------------------------------------------------------------- 1 | class Settings: 2 | 3 | http_prefix = 'https' 4 | 5 | url_api = 'api2.musical.ly' 6 | url_api16 = 'api2-16-h2.musical.ly' 7 | 8 | urls = { 9 | 10 | 'login': (url_api, 'passport/user/login/'), 11 | 'user_info': (url_api16, '/aweme/v1/user/'), 12 | 13 | 'search_human': (url_api16, '/aweme/v1/discover/search/'), 14 | 'search_ht': (url_api16, '/aweme/v1/challenge/search/'), 15 | 16 | 'followers_list': (url_api, 'aweme/v1/user/follower/list/'), 17 | 'followings_list': (url_api16, 'aweme/v1/user/following/list/'), 18 | 19 | 'like': (url_api16, '/aweme/v1/commit/item/digg/'), 20 | 21 | 'follow': (url_api16, '/aweme/v1/commit/follow/user/'), 22 | 'posts': (url_api16, '/aweme/v1/aweme/post/'), 23 | 24 | 'comments_list': (url_api16, '/aweme/v1/comment/list') 25 | 26 | } 27 | 28 | base_headers = { 29 | #':method': 'GET', 30 | #':authority': 'api2-16-h2.musical.ly', 31 | #':scheme': 'https', 32 | #':path': '', 33 | 'Host': 'api2.musical.ly', 34 | 'User-Agent': 'TikTok 10.3.0 rv:103005 (iPhone; iOS 12.1.4; ru_RU) Cronet', 35 | 'Accept-Encoding': 'gzip, deflate', 36 | 'Connection': 'keep-alive', 37 | 'x-tt-token': '03a10395e47f188137e97a9938874fd779c376b666aea7d55b3e9c9d077f14982e3c21395181d3fcc9c5e0125b719ef6ed6', 38 | 'sdk-version': '1', 39 | 'X-Khronos': '', 40 | 'X-Pods': '', 41 | 'X-Gorgon': '', 42 | 'Cookie': '' 43 | } 44 | 45 | query_string = { 46 | 'version_code': '10.3.0', 47 | 'pass-region': '1', 48 | 'pass-route': '1', 49 | 'language': 'ru', 50 | 'app_name': 'musical_ly', 51 | 'vid': 'C6A5896B-9B12-42FA-B43C-123749A77B5F', 52 | 'app_version': '10.3.0', 53 | 'carrier_region': 'RU', 54 | 'is_my_cn': '0', 55 | 'channel': 'App Store', 56 | 'mcc_mnc': '25001', 57 | 'device_id': '6659342247706494469', 58 | 'tz_offset': '10800', 59 | 'account_region': 'RU', 60 | 'sys_region': 'RU', 61 | 'aid': '1233', 62 | 'screen_width': '1125', 63 | 'openudid': 'c8128daffab809a0b644b01ea05c56b31bd4c47e', 64 | 'os_api': '18', 65 | 'ac': 'WIFI', 66 | 'os_version': '12.1.4', 67 | 'app_language': 'ru', 68 | 'tz_name': 'Europe/Moscow', 69 | 'device_platform': 'iphone', 70 | 'build_number': '103005', 71 | 'device_type': 'iPhone9,4', 72 | 'iid': '6664943549341927173', 73 | 'idfa': 'FA987C16-742A-4D4C-9D6C-50DF6EC1003F', 74 | 'mix_mode': '1', 75 | 'ts': '', 76 | } 77 | 78 | login_data = { 79 | 'username': None, 80 | 'password': None, 81 | 'email': None, 82 | 'mobile': None, 83 | 'account': None, 84 | 'captcha': None 85 | } 86 | 87 | cookies = { 88 | 'uid_tt': '91c80eb90e69bbace74f3b5c9d107ee5bad984852311250976b23062e9e2ea41', 89 | 'sid_tt': 'a10395e47f188137e97a9938874fd779', 90 | 'sessionid': 'a10395e47f188137e97a9938874fd779', 91 | 'install_id': '6664943549341927173', 92 | 'odin_tt': 'ad759d0a3df0a77bc360d3dc3d5a2fca10ec5c1944bb4ac228c64702728500c755d6816b785b58ceb9eac6cd6b87046be5350c9263068ded493643985024e051' 93 | } 94 | 95 | 96 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /tiktokpy/api.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import time 3 | import urllib.parse 4 | import logging 5 | 6 | from .settings import Settings 7 | 8 | 9 | methods = { 10 | 'get': requests.get, 11 | 'post': requests.post 12 | } 13 | 14 | 15 | def xor(string, key=5): 16 | """ 17 | Decode string 18 | """ 19 | return ''.join([hex(int(x ^ key))[2:] for x in string.encode('utf-8')]) 20 | 21 | 22 | def prepare_url(path, query=''): 23 | """ 24 | Create url with parameters 25 | """ 26 | host, url = Settings.urls.get(path) 27 | full_path = url + '?' + query 28 | url = urllib.parse.urlunparse((Settings.http_prefix, host, url, None, query, '')) 29 | return url, host, full_path 30 | 31 | 32 | def _https_query(method, path, custom_query={}, custom_headers={}, data=None, ver=1): 33 | """ 34 | Base method for api 35 | 36 | :param method: method http, POST or GET required 37 | :param url: url 38 | :param custom_query: 39 | :param custom_headers: dict of header params 40 | :param session: dict of cookies parameters 41 | :param data: 42 | :return: result of requests get or post method 43 | """ 44 | 45 | headers = dict(Settings.base_headers) 46 | headers.update(custom_headers) 47 | cookies = '' 48 | for key, value in Settings.cookies.items(): 49 | cookies += '{}={};'.format(key, value) 50 | headers['Cookie'] = cookies 51 | 52 | time_for_query = str(round(time.time())) 53 | headers['X-Khronos'] = time_for_query 54 | 55 | query = Settings.query_string 56 | for key, value in custom_query.items(): 57 | if value is None: 58 | continue 59 | query[key] = value 60 | query['ts'] = time_for_query 61 | 62 | url, host, full_path = prepare_url(path, urllib.parse.urlencode(query)) 63 | headers['Host'] = host 64 | 65 | try: 66 | response = methods[method.lower()](url, headers=headers, data=data, verify=False) 67 | print(response.content) 68 | return response.json() 69 | except KeyError: 70 | logging.error('Wrong method: {}'.format(method)) 71 | except Exception as e: 72 | logging.exception('Unknown error: {}'.format(e)) 73 | 74 | 75 | # ACTIONS 76 | 77 | 78 | def login(username, password): 79 | """ 80 | Send username & password, take user info 81 | :param username: 82 | :param password: 83 | :return: 84 | """ 85 | 86 | method = 'get' 87 | query = dict(Settings.login_data) 88 | query['username'] = xor(username) 89 | query['password'] = xor(password) 90 | 91 | return _https_query(method, 'login', custom_query=query) 92 | 93 | 94 | def get_user_info(user_id): 95 | """ 96 | Get user info 97 | """ 98 | method = 'get' 99 | 100 | return _https_query(method, 'user_info', custom_query={'user_id': user_id}, ver=2) 101 | 102 | 103 | def get_followers_list(user_id, count=20, max_time=None): 104 | """ 105 | Get folowwers 106 | """ 107 | method = 'get' 108 | query = { 109 | 'user_id': user_id, 110 | 'count': count, 111 | 'max_time': max_time if max_time else 0, #str(round(time.time())), 112 | 'offset': 0, 113 | 'source_type': 1 114 | } 115 | 116 | return _https_query(method, 'followers_list', custom_query=query) 117 | 118 | 119 | def get_following_list(user_id, count=20, max_time=None): 120 | """ 121 | Get following 122 | """ 123 | method = 'get' 124 | query = { 125 | 'user_id': user_id, 126 | 'count': count, 127 | 'max_time': max_time if max_time else 0, 128 | 'offset': 0, 129 | 'source_type': 1 130 | } 131 | 132 | return _https_query(method, 'followings_list', custom_query=query) 133 | 134 | 135 | def get_search_results(source, text, count=20): 136 | """ 137 | Searching by source: discover / challenge / music 138 | """ 139 | method = 'get' 140 | query = { 141 | 'cursor': 0, 142 | 'keyword': text, 143 | 'count': count, 144 | 'type': 1, 145 | 'hot_search': 0, 146 | 'type': 1, 147 | 'is_pull_refresh': 0, 148 | 'search_source': source 149 | } 150 | 151 | return _https_query(method, 'search', custom_query=query) 152 | 153 | 154 | def like_post(post_id, like_type=1): 155 | """ 156 | Like / unlike post by id 157 | """ 158 | method = 'post' 159 | data = 'aweme_id={}&channel_id={}&type={}'.format(post_id, 3, like_type) 160 | 161 | return _https_query(method, 'like', custom_query=query, data=data) 162 | 163 | 164 | def get_posts(user_id, count=20): 165 | """ 166 | Get list of posts 167 | """ 168 | method = 'post' 169 | query = { 170 | 'min_cursor': 0, 171 | 'max_cursor': 0, 172 | 'user_id': user_id, 173 | 'count': count 174 | } 175 | return _https_query(method, 'posts', custom_query=query) 176 | 177 | 178 | def follow(user_id, unfollow=False): 179 | """ 180 | Follow on user by id 181 | """ 182 | method = 'get' 183 | query = { 184 | 'user_id': user_id, 185 | 'channel_id': 3, 186 | 'type': 0 if unfollow else 1 187 | } 188 | 189 | return _https_query(method, 'follow', custom_query=query) 190 | 191 | --------------------------------------------------------------------------------