├── .gitignore ├── CredentialManager.py ├── Experimenting.py ├── Instagram.py ├── InstagramAPI.py ├── InstagramAnalyzer.py ├── Modifier.py ├── PostAggregator.py ├── README.md ├── Users.py └── sample_constants.json /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.swp 3 | .DS_Store 4 | 5 | Constants.json 6 | Requests.pkl 7 | 8 | Database/ 9 | Temp/ 10 | -------------------------------------------------------------------------------- /CredentialManager.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | import base64 5 | import json 6 | import getpass 7 | import os 8 | import sys 9 | 10 | 11 | ###################################################################################### 12 | # Written by Ryan D'souza in 2015 13 | # A password manager written in Python with only native dependencies 14 | # Note: this sacrifies most security at the expense of convenience and speed 15 | # 16 | # For run instructions, see 17 | # https://github.com/dsouzarc/dotfiles/tree/master/Python#credential-manager 18 | ###################################################################################### 19 | 20 | 21 | class CredentialManager(object): 22 | """Main class that manages credentials""" 23 | 24 | 25 | directory_path = None 26 | file_path = None 27 | credentials = None 28 | 29 | 30 | def __init__(self): 31 | """Constructor that just loads the prior credentials to a JSON object""" 32 | 33 | #Store data in hidden file in hidden folder in home directory 34 | self.directory_path = '{home}/.my_credential_manager/'.format(home=os.path.expanduser('~')) 35 | self.file_path = '{directory}.credential_files.json'.format(directory=self.directory_path) 36 | 37 | #Create path if needed 38 | if not os.path.exists(self.directory_path): 39 | os.makedirs(self.directory_path) 40 | print('Initialized credential manager: %s' % self.file_path) 41 | 42 | #Load data from file 43 | try: 44 | with open(self.file_path) as credential_file: 45 | self.credentials = json.load(credential_file) 46 | except ValueError: 47 | print 'Error when loading existing credentials. Building new file' 48 | 49 | if self.credentials is None: 50 | self.credentials = dict() 51 | 52 | 53 | def save_credentials(self): 54 | """Convenience method to centrally handle saving the credentials""" 55 | 56 | with open(self.file_path, 'w') as credential_file: 57 | json.dump(self.credentials, credential_file, sort_keys=True, indent=4) 58 | 59 | 60 | def encrypt_value(self, value): 61 | """Convenience method to centrally handle encryption/encoding 62 | 63 | Args: 64 | value (str): Value to encrypt 65 | 66 | Returns: 67 | encoded value 68 | """ 69 | 70 | return base64.b64encode(value) 71 | 72 | 73 | def decrypt_value(self, encrypted_value): 74 | """Convenience method to centrally handle decryption/decoding 75 | 76 | Args: 77 | encrypted_value (str): Value to decrypt 78 | 79 | Returns: 80 | decrypted value 81 | """ 82 | 83 | return base64.b64decode(encrypted_value) 84 | 85 | 86 | def get_value(self, key): 87 | """Given the key, returns the value 88 | 89 | Args: 90 | key (str): Key to find 91 | 92 | Returns: 93 | (str): Decrypted value 94 | """ 95 | 96 | encrypted_key = self.encrypt_value(key) 97 | 98 | if encrypted_key not in self.credentials: 99 | raise KeyError('No value found for: %s' % key) 100 | 101 | return self.decrypt_value(self.credentials[encrypted_key]) 102 | 103 | 104 | def delete_value(self, key, save_credentials=True): 105 | """Given the key, removes it from the dictionary and saves the file 106 | 107 | Args: 108 | key (str): Key to delete 109 | save_credentials (bool): Whether or not to save the dictionary to file 110 | """ 111 | 112 | encrypted_key = self.encrypt_value(key) 113 | 114 | if encrypted_key in self.credentials: 115 | del self.credentials[encrypted_key] 116 | 117 | if save_credentials: 118 | self.save_credentials() 119 | 120 | 121 | def get_values(self, *keys): 122 | """Given the keys, returns the values 123 | 124 | Args: 125 | key (list(str)): Keys to find 126 | 127 | Returns: 128 | (list(str)): Decrypted values 129 | """ 130 | 131 | values = list() 132 | 133 | for key in keys: 134 | values.append(self.get_value(key)) 135 | 136 | return values 137 | 138 | 139 | def set_value(self, key, value, save_credentials=True): 140 | """Given the key and the value, update the credentials dictionary and file with it 141 | 142 | Args: 143 | key (str): Unencrypted key to save 144 | value (str): Unencrypted value to save 145 | save_credentials (bool): Whether or not to save the dictionary to file 146 | """ 147 | 148 | encrypted_key = self.encrypt_value(key) 149 | encrypted_value = self.encrypt_value(value) 150 | 151 | self.credentials[encrypted_key] = encrypted_value 152 | 153 | if save_credentials: 154 | self.save_credentials() 155 | 156 | 157 | def save_account(self, account_name, username, password): 158 | """Given the account information, update the credentials dictionary and file with it 159 | 160 | Args: 161 | account_name (str): Name of the account 162 | username (str): Raw username to save 163 | password (str): Raw password to save 164 | """ 165 | 166 | username_key = '{account_name}_Username'.format(account_name=account_name) 167 | password_key = '{account_name}_Password'.format(account_name=account_name) 168 | 169 | self.set_value(username_key, username, save_credentials=False) 170 | self.set_value(password_key, password, save_credentials=False) 171 | self.save_credentials() 172 | 173 | 174 | def get_account(self, account_name): 175 | """Given the account name, return the username and password for the account 176 | 177 | Args: 178 | account_name (str): Name of the account 179 | 180 | Returns: 181 | (list(str)): Username and password for the account 182 | """ 183 | 184 | username_key = '{account_name}_Username'.format(account_name=account_name) 185 | password_key = '{account_name}_Password'.format(account_name=account_name) 186 | 187 | return self.get_values(username_key, password_key) 188 | 189 | 190 | def delete_account(self, account_name): 191 | """Given the account name, deletes the username and password for the account 192 | 193 | Args: 194 | account_name (str): Name of the account 195 | """ 196 | 197 | username_key = '{account_name}_Username'.format(account_name=account_name) 198 | password_key = '{account_name}_Password'.format(account_name=account_name) 199 | 200 | self.delete_value(username_key, save_credentials=False) 201 | self.delete_value(password_key, save_credentials=False) 202 | self.save_credentials() 203 | 204 | 205 | def save_account_prompt(self): 206 | """Convenience method to prompt and save credentials for an account""" 207 | 208 | account_name = raw_input("Enter name of account: ") 209 | username = raw_input("Enter account username: ") 210 | password = getpass.getpass("Enter account password: ") 211 | 212 | self.save_account(account_name, username, password) 213 | 214 | 215 | def get_account_prompt(self): 216 | """Convenience method to return credentials for an account""" 217 | 218 | account_name = raw_input("Enter name of account: ") 219 | 220 | credentials = self.get_account(account_name) 221 | for credential in credentials: 222 | print(credential) 223 | 224 | 225 | def save_value_prompt(self): 226 | """Convenience method to prompt to save a key and value""" 227 | 228 | key = raw_input("Enter key: ") 229 | value = raw_input("Enter value: ") 230 | 231 | self.set_value(key, value) 232 | 233 | 234 | def get_value_prompt(self): 235 | """Convenience method to prompt to get a key and value""" 236 | 237 | key = raw_input("Enter key: ") 238 | print(self.get_value(key)) 239 | 240 | 241 | def delete_value_prompt(self): 242 | """Convenience method to delete a key and value""" 243 | 244 | key = raw_input("enter key to delete: ") 245 | self.delete_value(key) 246 | 247 | 248 | def delete_account_prompt(self): 249 | """Convenience method to delete credentials for an account""" 250 | 251 | account_name = raw_input("Enter name of account to delete: ") 252 | self.delete_account(account_name) 253 | 254 | 255 | if __name__ == "__main__": 256 | 257 | manager = CredentialManager() 258 | 259 | if len(sys.argv) == 1: 260 | manager.save_account_prompt() 261 | 262 | elif len(sys.argv) == 2: 263 | if sys.argv[1] == "save_account": 264 | manager.save_account_prompt() 265 | elif sys.argv[1] == "get_account": 266 | manager.get_account_prompt() 267 | elif sys.argv[1] == "save_value": 268 | manager.save_value_prompt() 269 | elif sys.argv[1] == "get_value": 270 | manager.get_value_prompt() 271 | elif sys.argv[1] == "delete_account": 272 | manager.delete_account_prompt() 273 | elif sys.argv[1] == "delete_value": 274 | manager.delete_value_prompt() 275 | 276 | -------------------------------------------------------------------------------- /Experimenting.py: -------------------------------------------------------------------------------- 1 | """Experimenting.py: Temporary Python file for testing various experiments before being integrated""" 2 | 3 | from collections import Counter 4 | 5 | import json 6 | import subprocess 7 | import random 8 | import time 9 | 10 | from CredentialManager import CredentialManager 11 | from InstagramAPI import InstagramAPI 12 | from Users import InstagramUser 13 | 14 | import pymongo 15 | import requests 16 | 17 | 18 | def unfollow_user(instagram_api, user_pk, unlike_posts=True, min_wait=2, max_wait=10): 19 | """Convenience method for unfollowing someone and unliking posts 20 | 21 | Args: 22 | instagram_api (InstagramAPI.InstagramAPI): instagram api object 23 | user_pk (str): the user ID/pk 24 | unlike_posts (bool): whether to unlike their posts 25 | """ 26 | 27 | if not unlike_posts: 28 | unfollow_result = instagram_api.unfollow(user_pk) 29 | print("Unfollowed %s" % unfollow_result) 30 | return 31 | 32 | try: 33 | user_posts = instagram_api.getTotalUserFeed(user_pk) 34 | except KeyError: 35 | print('Cannot access posts as account could not be found') 36 | else: 37 | unlike_count = 0 38 | 39 | for user_post in user_posts: 40 | if user_post['has_liked']: 41 | media_id = user_post['id'] 42 | unlike_result = instagram_api.unlike(media_id) 43 | time.sleep(random.randint(min_wait, max_wait)) 44 | 45 | if not unlike_result: 46 | print("Error unliking post: %s\t %s" % (unlike_result, 47 | json.dumps(instagram_api.LastJson, indent=4))) 48 | else: 49 | unlike_count += 1 50 | 51 | unfollow_result = instagram_api.unfollow(user_pk) 52 | print("Unliked %s\tOut of %s. \tUnfollowed: %s" % (unlike_count, len(user_posts), unfollow_result)) 53 | 54 | 55 | #Interactive way to unfollow those who don't follow back 56 | def following_follower_diff(instagram_api): 57 | """Interactive method for unfollowing people 58 | 59 | Args: 60 | instagram_api (InstagramAPI.InstagramAPI): instagram api object 61 | """ 62 | 63 | all_followers = instagram_api.getTotalSelfFollowers() 64 | all_following = instagram_api.getTotalSelfFollowings() 65 | 66 | all_followers_dict = {} 67 | 68 | for follower in all_followers: 69 | user_id = follower['pk'] 70 | all_followers_dict[user_id] = follower 71 | 72 | for following in all_following: 73 | user_id = following['pk'] 74 | 75 | #The person does not follow us back 76 | if user_id not in all_followers_dict: 77 | 78 | full_name = following['full_name'].encode('utf-8') 79 | user_name = following['username'].encode('utf-8') 80 | profile_link = "https://instagram.com/" + user_name 81 | 82 | #Prompt for unfollowing/other actions 83 | print(full_name + " not following you: \t " + profile_link) 84 | 85 | command = raw_input("Type 'O' to open in brower, 'U' to unfollow, " + 86 | "'UL' to unfollow and unlike, " + 87 | "or any other key to do nothing: ").lower() 88 | 89 | if command == 'o': 90 | subprocess.Popen(['open', profile_link]) 91 | 92 | command = raw_input("\nEnter 'U' to unfollow " + full_name + 93 | " or any other key to do nothing: ").lower() 94 | 95 | if command == 'u': 96 | unfollow_user(instagram_api, user_id, unlike_posts=False) 97 | 98 | elif command == 'ul': 99 | unfollow_user(instagram_api, user_id, unlike_posts=True) 100 | #TODO: Add some sort of Queue for this 101 | 102 | 103 | def get_my_post_likers(instagram_api, save_to_file=True, read_from_file=False): 104 | """Goes through all my posts, and for reach post, gets the list of likers 105 | 106 | Returns: 107 | dict(str, list(dict)): Dictionary with Key: post_id. Value list of likers 108 | """ 109 | 110 | save_file_name = 'results_my_posts_likes.json' 111 | results = dict() 112 | 113 | 114 | if read_from_file: 115 | results = json.load(open(save_file_name, 'r')) 116 | return results 117 | 118 | 119 | my_posts = instagram_api.getTotalSelfUserFeed() 120 | 121 | for post_counter, post in enumerate(my_posts): 122 | 123 | post_id = post['pk'] 124 | 125 | instagram_api.getMediaLikers(post_id) 126 | post_likers_info = instagram_api.LastJson 127 | post_likers = post_likers_info['users'] 128 | 129 | results[str(post_id)] = list() 130 | 131 | for post_liker in post_likers: 132 | liker_pk = str(post_liker['pk']) 133 | 134 | results[str(post_id)].append(post_liker) 135 | 136 | post_url = 'https://www.instagram.com/p/{code}'.format(code=post.get('code')) 137 | print("Caption: %s\t# of likes: %s\nLink: %s" % (len(post_likers), 138 | post['caption'].get('text'), post_url)) 139 | 140 | time.sleep(random.randint(3, 15)) 141 | 142 | 143 | if save_to_file: 144 | with open(save_file_name, 'w') as save_file: 145 | json.dump(results, save_file, indent=4) 146 | 147 | 148 | return results 149 | 150 | 151 | def get_unique_likers(posts_likers): 152 | """Given a dictionary of post_ids and a list of their likers, 153 | Returns a dictionary with the unique likers 154 | 155 | Args: 156 | post_likers (dict, list((dict)): Results from get_my_post_likers 157 | 158 | Returns: 159 | dict(str, dict): Dictionary with key: user_id, value: liker 160 | """ 161 | 162 | results = dict() 163 | 164 | for post_id, post_likers in posts_likers.items(): 165 | for post_liker in post_likers: 166 | results[str(post_liker['pk'])] = post_liker 167 | 168 | return results 169 | 170 | 171 | def get_liker_frequencies(posts_likers): 172 | """Given a dictionary of post_ids and a list of their likers, 173 | Returns a Counter object with user_id and like frequency 174 | 175 | Args: 176 | posts_likers (dict, list(dict)): Results from get_my_post_likers 177 | 178 | Returns: 179 | Counter: python Counter object with user_id: frequency 180 | """ 181 | 182 | liker_counter = Counter() 183 | 184 | for post_id, post_likers in posts_likers.items(): 185 | for post_liker in post_likers: 186 | liker_pk = str(post_liker['pk']) 187 | liker_counter.update({liker_pk: 1}) 188 | 189 | 190 | return liker_counter 191 | 192 | 193 | def get_all_followers(instagram_api, save_to_file=True, read_from_file=False): 194 | """Convenience method to return a dict with key: username, value: user object 195 | 196 | Returns: 197 | dict(str, dict): key: username, value: user object 198 | """ 199 | 200 | save_file_name = 'my_followers.json' 201 | 202 | if save_to_file: 203 | results = json.load(open(save_file_name, 'r')) 204 | return results 205 | 206 | 207 | all_followers_map = dict() 208 | 209 | all_followers_raw = instagram_api.getTotalSelfFollowers() 210 | for raw_follower in all_followers_raw: 211 | all_followers_map[str(raw_follower['pk'])] = raw_follower 212 | 213 | if save_to_file: 214 | with open(save_file_name, 'w') as save_file: 215 | json.dump(all_followers_map, save_file, indent=4) 216 | 217 | 218 | return all_followers_map 219 | 220 | 221 | 222 | if __name__ == "__main__": 223 | """Main method - run from here""" 224 | 225 | #TODO: Use .map instead of for for 226 | 227 | credential_manager = CredentialManager() 228 | insta_username, insta_password = credential_manager.get_account('Instagram') 229 | 230 | instagram_api = InstagramAPI(insta_username, insta_password) 231 | 232 | following_follower_diff(instagram_api) 233 | exit(0) 234 | 235 | #user_info = instagram_api.getUsernameInfo(1458052235) 236 | #user_info = instagram_api.getUserTags(1458052235) 237 | #user_info = instagram_api.getUserFeed(1458052235) #GOOD 238 | #geo_info = instagram_api.getLikedMedia() #Just things that I've liked 239 | 240 | 241 | 242 | all_followers_map = get_all_followers(instagram_api, save_to_file=True, read_from_file=True) 243 | 244 | my_posts_likers = get_my_post_likers(instagram_api, save_to_file=True, read_from_file=True) 245 | unique_likers = get_unique_likers(my_posts_likers) 246 | liker_frequency = get_liker_frequencies(my_posts_likers) 247 | 248 | 249 | for liker_pk, count in liker_frequency.most_common(): 250 | user = unique_likers[str(liker_pk)] 251 | print('%s \t\t %s \t\t %s' % (user['username'], user.get('full_name', ''), count)).expandtabs(20) 252 | 253 | 254 | for user_pk, user in all_followers_map.items(): 255 | 256 | if user_pk not in liker_counter: 257 | 258 | print('%s \t\t %s' % (user['username'], user.get('full_name', ''))).expandtabs(20) 259 | command = raw_input("Would you like to unfollow and unlike? Enter 'u': ").lower() 260 | if command == 'u': 261 | unfollow_user(instagram_api, user_pk, unlike_posts=True) 262 | 263 | 264 | 265 | exit(0) 266 | 267 | 268 | 269 | 270 | """ 271 | 272 | for liker_pk, count in liker_counter.most_common(): 273 | 274 | user = all_followers_map.get(liker_pk, all_likers_map.get(liker_pk)) 275 | print('%s \t\t %s \t\t %s' % (user['username'], user.get('full_name', ''), count)).expandtabs(20) 276 | 277 | print("\n\nNON-LIKERS \n\n") 278 | 279 | count = 0 280 | 281 | for user_pk, user in all_followers_map.items(): 282 | count += 1 283 | 284 | if user_pk not in all_likers_map: 285 | print('%s \t\t %s' % (user['username'], user.get('full_name', ''))).expandtabs(20) 286 | 287 | command = raw_input("Would you like to unfollow and unlike? Enter 'u': ").lower() 288 | if command == 'u': 289 | results = instagram_api.getTotalUserFeed(user_pk) 290 | print("RESULTS: %s\n\n" % (json.dumps(results, indent=4))) 291 | user_posts_raw = instagram_api.LastJson 292 | print("RAW DATA: %s\n\n" % json.dumps(user_posts_raw, indent=4)) 293 | user_posts = user_posts_raw.get('items') 294 | for user_post in user_posts: 295 | 296 | print(user_post['image_versions2']['candidates'][0]) 297 | print("HERE: %s\n" % (len(user_posts))) 298 | if count > 2: 299 | exit(0) 300 | exit(0) 301 | """ 302 | 303 | 304 | 305 | 306 | """ 307 | my_post = my_posts[0] 308 | 309 | print(json.dumps(my_post, indent=4)) 310 | 311 | results = instagram_api.getMediaLikers('785079885619196477') 312 | print("LIKES\n\n") 313 | print(json.dumps(instagram_api.LastJson, indent=4)) 314 | print(json.dumps(results, indent=4)) 315 | """ 316 | 317 | 318 | 319 | 320 | 321 | 322 | -------------------------------------------------------------------------------- /Instagram.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Instagram.py: Handles interacting with Instagram API, adds additional functionality""" 3 | 4 | import inspect 5 | import json 6 | import os 7 | import random 8 | import subprocess 9 | import sys 10 | import time 11 | 12 | from bson import json_util 13 | from CredentialManager import CredentialManager 14 | from InstagramAPI import InstagramAPI 15 | from pymongo import MongoClient 16 | from Users import InstagramUser 17 | 18 | import pymongo 19 | import requests 20 | 21 | 22 | #################################################################### 23 | # Written by Ryan D'souza 24 | # Main class for interacting with Instagram 25 | #################################################################### 26 | 27 | 28 | 29 | class Instagram(InstagramAPI): 30 | """Main class that handles all interactions""" 31 | 32 | 33 | def __init__(self, insta_username, insta_password, save_file_path=None): 34 | """Constructor 35 | 36 | Args: 37 | insta_username (str): string version of username i.e.: 'dsouzarc' 38 | insta_password (str): string version of password 39 | save_file_path (str): string path to where we should save the credentials 40 | """ 41 | 42 | InstagramAPI.__init__(self, username=insta_username, password=insta_password, 43 | save_file_path=save_file_path) 44 | 45 | 46 | def get_messages(self): 47 | """Prints a list of messages in the inbox""" 48 | 49 | request_response = self.getv2Inbox() 50 | actual_responses = self.LastJson 51 | 52 | inbox = actual_responses["inbox"] 53 | threads = inbox["threads"] 54 | 55 | for thread in threads: 56 | thread_users = thread["users"] 57 | usernames = list() 58 | for thread_user in thread_users: 59 | usernames.append(thread_user["username"]) 60 | usernames.append(thread_user["full_name"]) 61 | 62 | print("Group chat with: " + ", ".join(usernames)) 63 | 64 | sender = thread["inviter"] 65 | print("From: " + sender["username"] + " " + sender["full_name"]) 66 | 67 | messages = thread["items"] 68 | for message in messages: 69 | if "text" in message: 70 | print(message["text"]) 71 | 72 | elif "reel_share" in message: 73 | print(message["reel_share"]["text"]) 74 | 75 | elif "items" in thread: 76 | descriptions = list() 77 | for item in thread["items"]: 78 | if "action_log" in item: 79 | descriptions.append(item["action_log"]) 80 | elif "media_share" in item: 81 | media_url = 'https://instagram.com/p/' + item['media_share']['code'] 82 | descriptions.append(media_url) 83 | 84 | print(' \t'.join(str(description) for description in descriptions)) 85 | 86 | else: 87 | print("Unable to find: " + json.dumps(thread, indent=4)) 88 | 89 | 90 | @staticmethod 91 | def default_client(): 92 | """Convenience method to return an Instagram object with the default credentials/set-up 93 | 94 | Returns: 95 | (:obj: `Instagram.Instagram`): Initialized version of our Instagram client 96 | """ 97 | 98 | credential_manager = CredentialManager() 99 | 100 | current_directory = os.path.abspath(inspect.getfile(inspect.currentframe())) 101 | save_file_path = os.path.dirname(current_directory) 102 | 103 | insta_username, insta_password = credential_manager.get_account('Instagram') 104 | 105 | 106 | client = Instagram(insta_username, insta_password, save_file_path) 107 | 108 | return client 109 | 110 | 111 | if __name__ == "__main__": 112 | """ Main method - run from here """ 113 | 114 | client = Instagram.default_client() 115 | 116 | client.get_messages() 117 | exit(0) 118 | 119 | # https://github.com/billcccheng/instagram-terminal-news-feed/blob/master/start.py 120 | 121 | 122 | all_followers = client.get_followers(from_mongo=False, from_file=True) 123 | all_following = client.get_following(from_mongo=False, from_file=True) 124 | 125 | for follower_pk in all_followers: 126 | if follower_pk in all_following: 127 | all_following[follower_pk]["is_follower"] = True 128 | all_followers[follower_pk]["am_following"] = True 129 | print("Following and followed by: " + all_followers[follower_pk].get("username")) 130 | 131 | 132 | exit(0) 133 | 134 | 135 | raw_media_info = json.loads(open("media_info.json").read()) 136 | raw_media_info = raw_media_info["items"][0] 137 | 138 | raw_tagged_users = raw_media_info["usertags"] 139 | tagged_users = [] 140 | print(raw_tagged_users) 141 | for tag_key, users in raw_tagged_users.items(): 142 | for user_dict in users: 143 | if "user" in user_dict and "username" in user_dict["user"]: 144 | user = user_dict["user"] 145 | tagged_user = { 146 | "full_name": user["full_name"], 147 | "profile_picture_link": user["profile_pic_url"], 148 | "pk": user["pk"], 149 | "is_private": user["is_private"], 150 | "username": user["username"] 151 | } 152 | tagged_users.append(tagged_user) 153 | 154 | 155 | 156 | 157 | """ 158 | 159 | client.api.timelineFeed() 160 | timeline_feed = client.api.LastJson 161 | 162 | for mediaItem in timeline_feed["items"]: 163 | 164 | media_id = mediaItem["id"] 165 | client.api.mediaInfo(media_id) 166 | print(json.dumps(client.api.LastJson, indent=4)) 167 | break 168 | 169 | """ 170 | -------------------------------------------------------------------------------- /InstagramAPI.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import copy 4 | import datetime 5 | import hashlib 6 | import hmac 7 | import json 8 | import math 9 | import os.path 10 | import pickle 11 | import random 12 | import requests 13 | import sys 14 | import time 15 | import urllib 16 | import uuid 17 | 18 | #Urllib split from Python 2 to Python 3 19 | if sys.version_info.major == 3: 20 | import urllib.parse 21 | 22 | from requests_toolbelt import MultipartEncoder 23 | 24 | 25 | #################################################################### 26 | # Modified by Ryan D'souza 27 | # Handles interactions with Instagram's API 28 | # https://github.com/LevPasha/Instagram-API-python 29 | #################################################################### 30 | 31 | 32 | class InstagramAPI(object): 33 | 34 | 35 | #################################################################### 36 | """ Class that acts as an Instagram API Client 37 | See Instagram.py for running example """ 38 | #################################################################### 39 | 40 | #Preferences file 41 | CONSTANTS_FILE_NAME = "Constants.json" 42 | REQUESTS_FILE_NAME = "Requests.pkl" 43 | 44 | #Other constants 45 | API_URL = 'https://i.instagram.com/api/v1/' 46 | USER_AGENT = None 47 | IG_SIG_KEY = None 48 | EXPERIMENTS = None 49 | SIG_KEY_VERSION = None 50 | 51 | username = None 52 | password = None 53 | username_id = None 54 | device_id = None 55 | uuid = None 56 | is_logged_in = False 57 | last_response = None 58 | session = None 59 | rank_token = None 60 | token = None 61 | 62 | last_login_datetime = None 63 | 64 | LastJson = None 65 | 66 | 67 | def __init__(self, username, password, save_file_path=None, debug=False): 68 | """Constructor 69 | 70 | Args: 71 | username (str): Instagram username 72 | password (str): Instagram password 73 | save_file_path (str): Path to where we should save credentials 74 | debug (bool): For printing log messages 75 | """ 76 | 77 | if save_file_path is not None: 78 | self.CONSTANTS_FILE_NAME = save_file_path + '/' + self.CONSTANTS_FILE_NAME 79 | self.REQUESTS_FILE_NAME = save_file_path + '/' + self.REQUESTS_FILE_NAME 80 | 81 | constants_file = open(self.CONSTANTS_FILE_NAME) 82 | constants = json.load(constants_file) 83 | constants_file.close() 84 | 85 | self.DEVICE_SETTINGS = constants["DEVICE_SETTINGS"] 86 | self.USER_AGENT = str(constants["USER_AGENT"]).format(**self.DEVICE_SETTINGS) 87 | self.IG_SIG_KEY = constants["IG_SIG_KEY"] 88 | self.EXPERIMENTS = constants["EXPERIMENTS"] 89 | self.SIG_KEY_VERSION = constants["SIG_KEY_VERSION"] 90 | 91 | self.username = username 92 | self.password = password 93 | self.is_logged_in = False 94 | self.last_response = None 95 | self.session = requests.session() 96 | 97 | datetime_format = "%Y-%m-%d %H:%M:%S" 98 | 99 | #Get the last time we logged in 100 | try: 101 | datetime_string = constants["last_login_time"] 102 | self.last_login_datetime = datetime.datetime.strptime(datetime_string, datetime_format) 103 | 104 | except Exception as date_cast_exception: 105 | print("No old date found: %s" % date_cast_exception) 106 | self.last_login_datetime = None 107 | 108 | 109 | #If we logged in under a week ago and we have the requests object saved 110 | if (self.last_login_datetime and 111 | (datetime.datetime.now() - self.last_login_datetime).days < 7 and 112 | os.path.isfile(self.REQUESTS_FILE_NAME)): 113 | 114 | print("No login needed") 115 | self.is_logged_in = True 116 | self.username_id = constants["username_id"] 117 | self.uuid = constants["uuid"] 118 | self.device_id = constants["device_id"] 119 | self.rank_token = constants["rank_token"] 120 | self.token = constants["csrftoken"] 121 | 122 | with open(self.REQUESTS_FILE_NAME, "r") as requests_file: 123 | self.session = pickle.load(requests_file) 124 | 125 | 126 | #If we never logged in or logged in over a week ago 127 | else: 128 | print("Login needed") 129 | m = hashlib.md5() 130 | m.update(username.encode('utf-8') + password.encode('utf-8')) 131 | self.device_id = self.generateDeviceId(m.hexdigest()) 132 | self.uuid = self.generateUUID(False) 133 | 134 | send_url = 'si/fetch_headers/?challenge_type=signup&guid=' + self.uuid 135 | if (self.SendRequest(send_url, None, True)): 136 | 137 | self.token = self.last_response.cookies['csrftoken'] 138 | 139 | data = { 140 | 'phone_id' : self.device_id, 141 | '_csrftoken' : self.token, 142 | 'username' : self.username, 143 | 'guid' : self.uuid, 144 | 'device_id' : self.device_id, 145 | 'password' : self.password, 146 | 'login_attempt_count' : '0' 147 | } 148 | 149 | post_data = self.generateSignature(json.dumps(data)) 150 | if (self.SendRequest('accounts/login/', post_data, True)): 151 | 152 | self.is_logged_in = True 153 | self.username_id = self.LastJson["logged_in_user"]["pk"] 154 | self.rank_token = "%s_%s" % (self.username_id, self.uuid) 155 | self.token = self.last_response.cookies["csrftoken"] 156 | self.last_login_datetime = datetime.datetime.now() 157 | 158 | constants["username_id"] = self.username_id 159 | constants["uuid"] = self.uuid 160 | constants["device_id"] = self.device_id 161 | constants["rank_token"] = self.rank_token 162 | constants["csrftoken"] = self.token 163 | constants["last_login_time"] = self.last_login_datetime.strftime(datetime_format) 164 | 165 | 166 | #Save our data so that we don't have to login again 167 | with open(self.CONSTANTS_FILE_NAME, "w") as constants_file: 168 | json.dump(constants, constants_file, ensure_ascii=True, indent=4) 169 | 170 | with open(self.REQUESTS_FILE_NAME, "wb") as requests_file: 171 | pickle.dump(self.session, requests_file, pickle.HIGHEST_PROTOCOL) 172 | 173 | #Default methods --> make us seem normal 174 | self.syncFeatures() 175 | self.autoCompleteUserList() 176 | self.timelineFeed() 177 | self.getv2Inbox() 178 | self.getRecentActivity() 179 | print ("Login success! Saved login information \n") 180 | 181 | 182 | def syncFeatures(self): 183 | """ Syncs features - default code """ 184 | 185 | data = json.dumps({ 186 | '_uuid' : self.uuid, 187 | '_uid' : self.username_id, 188 | 'id' : self.username_id, 189 | '_csrftoken' : self.token, 190 | 'experiments' : self.EXPERIMENTS 191 | }) 192 | return self.SendRequest('qe/sync/', self.generateSignature(data)) 193 | 194 | 195 | def autoCompleteUserList(self): 196 | return self.SendRequest('friendships/autocomplete_user_list/') 197 | 198 | 199 | def timelineFeed(self): 200 | return self.SendRequest('feed/timeline/') 201 | 202 | 203 | def megaphoneLog(self): 204 | return self.SendRequest('megaphone/log/') 205 | 206 | 207 | def expose(self): 208 | data = json.dumps({ 209 | '_uuid' : self.uuid, 210 | '_uid' : self.username_id, 211 | 'id' : self.username_id, 212 | '_csrftoken' : self.token, 213 | 'experiment' : 'ig_android_profile_contextual_feed' 214 | }) 215 | return self.SendRequest('qe/expose/', self.generateSignature(data)) 216 | 217 | 218 | def logout(self): 219 | logout = self.SendRequest('accounts/logout/') 220 | 221 | 222 | def uploadPhoto(self, photo, caption = None, upload_id = None): 223 | if upload_id is None: 224 | upload_id = str(int(time.time() * 1000)) 225 | data = { 226 | 'upload_id' : upload_id, 227 | '_uuid' : self.uuid, 228 | '_csrftoken' : self.token, 229 | 'image_compression' : '{"lib_name":"jt","lib_version":"1.3.0","quality":"87"}', 230 | 'photo' : ('pending_media_%s.jpg'%upload_id, open(photo, 'rb'), 'application/octet-stream', {'Content-Transfer-Encoding':'binary'}) 231 | } 232 | m = MultipartEncoder(data, boundary=self.uuid) 233 | self.session.headers.update ({'X-IG-Capabilities' : '3Q4=', 234 | 'X-IG-Connection-Type' : 'WIFI', 235 | 'Cookie2' : '$Version=1', 236 | 'Accept-Language' : 'en-US', 237 | 'Accept-Encoding' : 'gzip, deflate', 238 | 'Content-type': m.content_type, 239 | 'Connection' : 'close', 240 | 'User-Agent' : self.USER_AGENT}) 241 | response = self.session.post(self.API_URL + "upload/photo/", data=m.to_string()) 242 | if response.status_code == 200: 243 | if self.configure(upload_id, photo, caption): 244 | self.expose() 245 | return False 246 | 247 | def uploadVideo(self, video, thumbnail, caption = None, upload_id = None): 248 | if upload_id is None: 249 | upload_id = str(int(time.time() * 1000)) 250 | data = { 251 | 'upload_id': upload_id, 252 | '_csrftoken': self.token, 253 | 'media_type': '2', 254 | '_uuid': self.uuid, 255 | } 256 | m = MultipartEncoder(data, boundary=self.uuid) 257 | self.session.headers.update({'X-IG-Capabilities': '3Q4=', 258 | 'X-IG-Connection-Type': 'WIFI', 259 | 'Host': 'i.instagram.com', 260 | 'Cookie2': '$Version=1', 261 | 'Accept-Language': 'en-US', 262 | 'Accept-Encoding': 'gzip, deflate', 263 | 'Content-type': m.content_type, 264 | 'Connection': 'keep-alive', 265 | 'User-Agent': self.USER_AGENT}) 266 | response = self.session.post(self.API_URL + "upload/video/", data=m.to_string()) 267 | if response.status_code == 200: 268 | body = json.loads(response.text) 269 | upload_url = body['video_upload_urls'][3]['url'] 270 | upload_job = body['video_upload_urls'][3]['job'] 271 | 272 | videoData = open(video, 'rb').read() 273 | #solve issue #85 TypeError: slice indices must be integers or None or have an __index__ method 274 | request_size = int(math.floor(len(videoData) / 4)) 275 | lastRequestExtra = (len(videoData) - (request_size * 3)) 276 | 277 | headers = copy.deepcopy(self.session.headers) 278 | self.session.headers.update({'X-IG-Capabilities': '3Q4=', 279 | 'X-IG-Connection-Type': 'WIFI', 280 | 'Cookie2': '$Version=1', 281 | 'Accept-Language': 'en-US', 282 | 'Accept-Encoding': 'gzip, deflate', 283 | 'Content-type': 'application/octet-stream', 284 | 'Session-ID': upload_id, 285 | 'Connection': 'keep-alive', 286 | 'Content-Disposition': 'attachment; filename="video.mov"', 287 | 'job': upload_job, 288 | 'Host': 'upload.instagram.com', 289 | 'User-Agent': self.USER_AGENT}) 290 | for i in range(0, 4): 291 | start = i * request_size 292 | if i == 3: 293 | end = i * request_size + lastRequestExtra 294 | else: 295 | end = (i + 1) * request_size 296 | length = lastRequestExtra if i == 3 else request_size 297 | content_range = "bytes {start}-{end}/{lenVideo}".format(start=start, end=(end - 1), 298 | lenVideo=len(videoData)).encode('utf-8') 299 | 300 | self.session.headers.update({'Content-Length': str(end - start), 'Content-Range': content_range, }) 301 | response = self.session.post(upload_url, data=videoData[start:start + length]) 302 | self.session.headers = headers 303 | 304 | if response.status_code == 200: 305 | if self.configureVideo(upload_id, video, thumbnail, caption): 306 | self.expose() 307 | return False 308 | 309 | def direct_share(self, media_id, recipients, text = None): 310 | # TODO Instagram.php 420-490 311 | return False 312 | 313 | def configureVideo(self, upload_id, video, thumbnail, caption = ''): 314 | clip = VideoFileClip(video) 315 | self.uploadPhoto(photo=thumbnail, caption=caption, upload_id=upload_id) 316 | data = json.dumps({ 317 | 'upload_id': upload_id, 318 | 'source_type': 3, 319 | 'poster_frame_index': 0, 320 | 'length': 0.00, 321 | 'audio_muted': False, 322 | 'filter_type': 0, 323 | 'video_result': 'deprecated', 324 | 'clips': { 325 | 'length': clip.duration, 326 | 'source_type': '3', 327 | 'camera_position': 'back', 328 | }, 329 | 'extra': { 330 | 'source_width': clip.size[0], 331 | 'source_height': clip.size[1], 332 | }, 333 | 'device': self.DEVICE_SETTINGS, 334 | '_csrftoken': self.token, 335 | '_uuid': self.uuid, 336 | '_uid': self.username_id, 337 | 'caption': caption, 338 | }) 339 | return self.SendRequest('media/configure/?video=1', self.generateSignature(data)) 340 | 341 | def configure(self, upload_id, photo, caption = ''): 342 | #(w,h) = getImageSize(photo) 343 | (w, h) = (500, 500) 344 | data = json.dumps({ 345 | '_csrftoken' : self.token, 346 | 'media_folder' : 'Instagram', 347 | 'source_type' : 4, 348 | '_uid' : self.username_id, 349 | '_uuid' : self.uuid, 350 | 'caption' : caption, 351 | 'upload_id' : upload_id, 352 | 'device' : self.DEVICE_SETTINGS, 353 | 'edits' : { 354 | 'crop_original_size': [w * 1.0, h * 1.0], 355 | 'crop_center' : [0.0, 0.0], 356 | 'crop_zoom' : 1.0 357 | }, 358 | 'extra' : { 359 | 'source_width' : w, 360 | 'source_height' : h, 361 | }}) 362 | return self.SendRequest('media/configure/?', self.generateSignature(data)) 363 | 364 | def editMedia(self, mediaId, captionText = ''): 365 | data = json.dumps({ 366 | '_uuid' : self.uuid, 367 | '_uid' : self.username_id, 368 | '_csrftoken' : self.token, 369 | 'caption_text' : captionText 370 | }) 371 | return self.SendRequest('media/'+ str(mediaId) +'/edit_media/', self.generateSignature(data)) 372 | 373 | def removeSelftag(self, mediaId): 374 | data = json.dumps({ 375 | '_uuid' : self.uuid, 376 | '_uid' : self.username_id, 377 | '_csrftoken' : self.token 378 | }) 379 | return self.SendRequest('media/'+ str(mediaId) +'/remove/', self.generateSignature(data)) 380 | 381 | def mediaInfo(self, mediaId): 382 | data = json.dumps({ 383 | '_uuid' : self.uuid, 384 | '_uid' : self.username_id, 385 | '_csrftoken' : self.token, 386 | 'media_id' : mediaId 387 | }) 388 | return self.SendRequest('media/'+ str(mediaId) +'/info/', self.generateSignature(data)) 389 | 390 | def deleteMedia(self, mediaId): 391 | data = json.dumps({ 392 | '_uuid' : self.uuid, 393 | '_uid' : self.username_id, 394 | '_csrftoken' : self.token, 395 | 'media_id' : mediaId 396 | }) 397 | return self.SendRequest('media/'+ str(mediaId) +'/delete/', self.generateSignature(data)) 398 | 399 | def changePassword(self, newPassword): 400 | data = json.dumps({ 401 | '_uuid' : self.uuid, 402 | '_uid' : self.username_id, 403 | '_csrftoken' : self.token, 404 | 'old_password' : self.password, 405 | 'new_password1' : newPassword, 406 | 'new_password2' : newPassword 407 | }) 408 | return self.SendRequest('accounts/change_password/', self.generateSignature(data)) 409 | 410 | def explore(self): 411 | return self.SendRequest('discover/explore/') 412 | 413 | def comment(self, mediaId, commentText): 414 | data = json.dumps({ 415 | '_uuid' : self.uuid, 416 | '_uid' : self.username_id, 417 | '_csrftoken' : self.token, 418 | 'comment_text' : commentText 419 | }) 420 | return self.SendRequest('media/'+ str(mediaId) +'/comment/', self.generateSignature(data)) 421 | 422 | def deleteComment(self, mediaId, commentId): 423 | data = json.dumps({ 424 | '_uuid' : self.uuid, 425 | '_uid' : self.username_id, 426 | '_csrftoken' : self.token 427 | }) 428 | return self.SendRequest('media/'+ str(mediaId) +'/comment/'+ str(commentId) +'/delete/', self.generateSignature(data)) 429 | 430 | def changeProfilePicture(self, photo): 431 | # TODO Instagram.php 705-775 432 | return False 433 | 434 | def removeProfilePicture(self): 435 | data = json.dumps({ 436 | '_uuid' : self.uuid, 437 | '_uid' : self.username_id, 438 | '_csrftoken' : self.token 439 | }) 440 | return self.SendRequest('accounts/remove_profile_picture/', self.generateSignature(data)) 441 | 442 | def setPrivateAccount(self): 443 | data = json.dumps({ 444 | '_uuid' : self.uuid, 445 | '_uid' : self.username_id, 446 | '_csrftoken' : self.token 447 | }) 448 | return self.SendRequest('accounts/set_private/', self.generateSignature(data)) 449 | 450 | def setPublicAccount(self): 451 | data = json.dumps({ 452 | '_uuid' : self.uuid, 453 | '_uid' : self.username_id, 454 | '_csrftoken' : self.token 455 | }) 456 | return self.SendRequest('accounts/set_public/', self.generateSignature(data)) 457 | 458 | def getProfileData(self): 459 | data = json.dumps({ 460 | '_uuid' : self.uuid, 461 | '_uid' : self.username_id, 462 | '_csrftoken' : self.token 463 | }) 464 | return self.SendRequest('accounts/current_user/?edit=true', self.generateSignature(data)) 465 | 466 | def editProfile(self, url, phone, first_name, biography, email, gender): 467 | data = json.dumps({ 468 | '_uuid' : self.uuid, 469 | '_uid' : self.username_id, 470 | '_csrftoken' : self.token, 471 | 'external_url' : url, 472 | 'phone_number' : phone, 473 | 'username' : self.username, 474 | 'full_name' : first_name, 475 | 'biography' : biography, 476 | 'email' : email, 477 | 'gender' : gender, 478 | }) 479 | return self.SendRequest('accounts/edit_profile/', self.generateSignature(data)) 480 | 481 | def getUsernameInfo(self, usernameId): 482 | return self.SendRequest('users/'+ str(usernameId) +'/info/') 483 | 484 | def getSelfUsernameInfo(self): 485 | return self.getUsernameInfo(self.username_id) 486 | 487 | def getRecentActivity(self): 488 | activity = self.SendRequest('news/inbox/?') 489 | return activity 490 | 491 | def getFollowingRecentActivity(self): 492 | activity = self.SendRequest('news/?') 493 | return activity 494 | 495 | def getv2Inbox(self): 496 | inbox = self.SendRequest('direct_v2/inbox/?') 497 | return inbox 498 | 499 | def getUserTags(self, usernameId): 500 | tags = self.SendRequest('usertags/'+ str(usernameId) +'/feed/?rank_token='+ str(self.rank_token) +'&ranked_content=true&') 501 | return tags 502 | 503 | def getSelfUserTags(self): 504 | return self.getUserTags(self.username_id) 505 | 506 | def tagFeed(self, tag): 507 | userFeed = self.SendRequest('feed/tag/'+ str(tag) +'/?rank_token=' + str(self.rank_token) + '&ranked_content=true&') 508 | return userFeed 509 | 510 | def getMediaLikers(self, mediaId): 511 | likers = self.SendRequest('media/'+ str(mediaId) +'/likers/?') 512 | return likers 513 | 514 | def getGeoMedia(self, usernameId): 515 | locations = self.SendRequest('maps/user/'+ str(usernameId) +'/') 516 | return locations 517 | 518 | def getSelfGeoMedia(self): 519 | return self.getGeoMedia(self.username_id) 520 | 521 | def fbUserSearch(self, query): 522 | query = self.SendRequest('fbsearch/topsearch/?context=blended&query='+ str(query) +'&rank_token='+ str(self.rank_token)) 523 | return query 524 | 525 | def searchUsers(self, query): 526 | query = self.SendRequest('users/search/?ig_sig_key_version='+ str(self.SIG_KEY_VERSION) 527 | +'&is_typeahead=true&query='+ str(query) +'&rank_token='+ str(self.rank_token)) 528 | return query 529 | 530 | def searchUsername(self, usernameName): 531 | query = self.SendRequest('users/'+ str(usernameName) +'/usernameinfo/') 532 | return query 533 | 534 | def syncFromAdressBook(self, contacts): 535 | return self.SendRequest('address_book/link/?include=extra_display_name,thumbnails', "contacts=" + json.dumps(contacts)) 536 | 537 | def searchTags(self, query): 538 | query = self.SendRequest('tags/search/?is_typeahead=true&q='+ str(query) +'&rank_token='+ str(self.rank_token)) 539 | return query 540 | 541 | def getTimeline(self): 542 | query = self.SendRequest('feed/timeline/?rank_token='+ str(self.rank_token) +'&ranked_content=true&') 543 | return query 544 | 545 | def getUserFeed(self, usernameId, maxid = '', minTimestamp = None): 546 | query = self.SendRequest('feed/user/' + str(usernameId) + '/?max_id=' + str(maxid) + '&min_timestamp=' + str(minTimestamp) 547 | + '&rank_token='+ str(self.rank_token) +'&ranked_content=true') 548 | return query 549 | 550 | def getSelfUserFeed(self, maxid = '', minTimestamp = None): 551 | return self.getUserFeed(self.username_id, maxid, minTimestamp) 552 | 553 | def getHashtagFeed(self, hashtagString, maxid = ''): 554 | return self.SendRequest('feed/tag/'+hashtagString+'/?max_id='+str(maxid)+'&rank_token='+self.rank_token+'&ranked_content=true&') 555 | 556 | def searchLocation(self, query): 557 | locationFeed = self.SendRequest('fbsearch/places/?rank_token='+ str(self.rank_token) +'&query=' + str(query)) 558 | return locationFeed 559 | 560 | def getLocationFeed(self, locationId, maxid = ''): 561 | return self.SendRequest('feed/location/'+str(locationId)+'/?max_id='+maxid+'&rank_token='+self.rank_token+'&ranked_content=true&') 562 | 563 | def getPopularFeed(self): 564 | popularFeed = self.SendRequest('feed/popular/?people_teaser_supported=1&rank_token='+ str(self.rank_token) +'&ranked_content=true&') 565 | return popularFeed 566 | 567 | def getUserFollowings(self, usernameId, maxid = ''): 568 | return self.SendRequest('friendships/'+ str(usernameId) +'/following/?max_id='+ str(maxid) 569 | +'&ig_sig_key_version='+ self.SIG_KEY_VERSION +'&rank_token='+ self.rank_token) 570 | 571 | def getSelfUsersFollowing(self): 572 | return self.getUserFollowings(self.username_id) 573 | 574 | def getUserFollowers(self, usernameId, maxid = ''): 575 | if maxid == '': 576 | return self.SendRequest('friendships/'+ str(usernameId) +'/followers/?rank_token='+ self.rank_token) 577 | else: 578 | return self.SendRequest('friendships/'+ str(usernameId) +'/followers/?rank_token='+ self.rank_token + '&max_id='+ str(maxid)) 579 | 580 | def getSelfUserFollowers(self): 581 | return self.getUserFollowers(self.username_id) 582 | 583 | def like(self, mediaId): 584 | data = json.dumps({ 585 | '_uuid' : self.uuid, 586 | '_uid' : self.username_id, 587 | '_csrftoken' : self.token, 588 | 'media_id' : mediaId 589 | }) 590 | return self.SendRequest('media/'+ str(mediaId) +'/like/', self.generateSignature(data)) 591 | 592 | def unlike(self, mediaId): 593 | data = json.dumps({ 594 | '_uuid' : self.uuid, 595 | '_uid' : self.username_id, 596 | '_csrftoken' : self.token, 597 | 'media_id' : mediaId 598 | }) 599 | return self.SendRequest('media/'+ str(mediaId) +'/unlike/', self.generateSignature(data)) 600 | 601 | def getMediaComments(self, mediaId): 602 | return self.SendRequest('media/'+ mediaId +'/comments/?') 603 | 604 | def setNameAndPhone(self, name = '', phone = ''): 605 | data = json.dumps({ 606 | '_uuid' : self.uuid, 607 | '_uid' : self.username_id, 608 | 'first_name' : name, 609 | 'phone_number' : phone, 610 | '_csrftoken' : self.token 611 | }) 612 | return self.SendRequest('accounts/set_phone_and_name/', self.generateSignature(data)) 613 | 614 | def getDirectShare(self): 615 | return self.SendRequest('direct_share/inbox/?') 616 | 617 | def backup(self): 618 | # TODO Instagram.php 1470-1485 619 | return False 620 | 621 | def follow(self, userId): 622 | data = json.dumps({ 623 | '_uuid' : self.uuid, 624 | '_uid' : self.username_id, 625 | 'user_id' : userId, 626 | '_csrftoken' : self.token 627 | }) 628 | return self.SendRequest('friendships/create/'+ str(userId) +'/', self.generateSignature(data)) 629 | 630 | def unfollow(self, userId): 631 | data = json.dumps({ 632 | '_uuid' : self.uuid, 633 | '_uid' : self.username_id, 634 | 'user_id' : userId, 635 | '_csrftoken' : self.token 636 | }) 637 | return self.SendRequest('friendships/destroy/'+ str(userId) +'/', self.generateSignature(data)) 638 | 639 | def block(self, userId): 640 | data = json.dumps({ 641 | '_uuid' : self.uuid, 642 | '_uid' : self.username_id, 643 | 'user_id' : userId, 644 | '_csrftoken' : self.token 645 | }) 646 | return self.SendRequest('friendships/block/'+ str(userId) +'/', self.generateSignature(data)) 647 | 648 | def unblock(self, userId): 649 | data = json.dumps({ 650 | '_uuid' : self.uuid, 651 | '_uid' : self.username_id, 652 | 'user_id' : userId, 653 | '_csrftoken' : self.token 654 | }) 655 | return self.SendRequest('friendships/unblock/'+ str(userId) +'/', self.generateSignature(data)) 656 | 657 | def userFriendship(self, userId): 658 | data = json.dumps({ 659 | '_uuid' : self.uuid, 660 | '_uid' : self.username_id, 661 | 'user_id' : userId, 662 | '_csrftoken' : self.token 663 | }) 664 | return self.SendRequest('friendships/show/'+ str(userId) +'/', self.generateSignature(data)) 665 | 666 | def getLikedMedia(self,maxid=''): 667 | return self.SendRequest('feed/liked/?max_id='+str(maxid)) 668 | 669 | def generateSignature(self, data): 670 | try: 671 | parsedData = urllib.parse.quote(data) 672 | except AttributeError: 673 | parsedData = urllib.quote(data) 674 | 675 | return 'ig_sig_key_version=' + self.SIG_KEY_VERSION + '&signed_body=' + hmac.new(self.IG_SIG_KEY.encode('utf-8'), data.encode('utf-8'), hashlib.sha256).hexdigest() + '.' + parsedData 676 | 677 | def generateDeviceId(self, seed): 678 | volatile_seed = "12345" 679 | m = hashlib.md5() 680 | m.update(seed.encode('utf-8') + volatile_seed.encode('utf-8')) 681 | return 'android-' + m.hexdigest()[:16] 682 | 683 | def generateUUID(self, type): 684 | #according to https://github.com/LevPasha/Instagram-API-python/pull/16/files#r77118894 685 | #uuid = '%04x%04x-%04x-%04x-%04x-%04x%04x%04x' % (random.randint(0, 0xffff), 686 | # random.randint(0, 0xffff), random.randint(0, 0xffff), 687 | # random.randint(0, 0x0fff) | 0x4000, 688 | # random.randint(0, 0x3fff) | 0x8000, 689 | # random.randint(0, 0xffff), random.randint(0, 0xffff), 690 | # random.randint(0, 0xffff)) 691 | generated_uuid = str(uuid.uuid4()) 692 | if (type): 693 | return generated_uuid 694 | else: 695 | return generated_uuid.replace('-', '') 696 | 697 | def buildBody(bodies, boundary): 698 | # TODO Instagram.php 1620-1645 699 | return False 700 | 701 | 702 | def getTotalSelfUserFeed(self, minTimestamp = None): 703 | return self.getTotalUserFeed(self.username_id, minTimestamp) 704 | 705 | 706 | def getTotalSelfFollowers(self): 707 | return self.getTotalFollowers(self.username_id) 708 | 709 | 710 | def getTotalSelfFollowings(self): 711 | return self.getTotalFollowings(self.username_id) 712 | 713 | 714 | def getTotalLikedMedia(self,scan_rate = 1): 715 | next_id = '' 716 | liked_items = [] 717 | for x in range(0,scan_rate): 718 | temp = self.getLikedMedia(next_id) 719 | temp = self.LastJson 720 | next_id = temp["next_max_id"] 721 | for item in temp["items"]: 722 | liked_items.append(item) 723 | return liked_items 724 | 725 | 726 | def SendRequest(self, endpoint, post = None, login = False): 727 | if (not self.is_logged_in and not login): 728 | raise Exception("Not logged in!\n") 729 | return; 730 | 731 | self.session.headers.update ({'Connection' : 'close', 732 | 'Accept' : '*/*', 733 | 'Content-type' : 'application/x-www-form-urlencoded; charset=UTF-8', 734 | 'Cookie2' : '$Version=1', 735 | 'Accept-Language' : 'en-US', 736 | 'User-Agent' : self.USER_AGENT}) 737 | 738 | 739 | #Handle Post requests 740 | if (post != None): 741 | response = self.session.post(self.API_URL + endpoint, data=post) 742 | 743 | #Handle GET requests 744 | else: 745 | response = self.session.get(self.API_URL + endpoint) # , verify=False 746 | 747 | if response.status_code == 200: 748 | self.last_response = response 749 | self.LastJson = json.loads(response.text) 750 | return True 751 | 752 | else: 753 | print ("Request return " + str(response.status_code) + " error! " + response.text) 754 | # for debugging 755 | try: 756 | self.last_response = response 757 | self.LastJson = json.loads(response.text) 758 | except: 759 | pass 760 | return False 761 | 762 | 763 | def getTotalFollowers(self,usernameId): 764 | followers = [] 765 | next_max_id = '' 766 | while 1: 767 | self.getUserFollowers(usernameId,next_max_id) 768 | temp = self.LastJson 769 | 770 | for item in temp["users"]: 771 | followers.append(item) 772 | 773 | if temp["big_list"] == False: 774 | return followers 775 | next_max_id = temp["next_max_id"] 776 | 777 | 778 | def getTotalFollowings(self,usernameId): 779 | followers = [] 780 | next_max_id = '' 781 | while 1: 782 | self.getUserFollowings(usernameId,next_max_id) 783 | temp = self.LastJson 784 | 785 | for item in temp["users"]: 786 | followers.append(item) 787 | 788 | if temp["big_list"] == False: 789 | return followers 790 | next_max_id = temp["next_max_id"] 791 | 792 | 793 | def user_feed_generator(self, username_id, max_pages=sys.maxint, min_timestamp=None): 794 | """Generator function to return a user's posts. Yields a LIST of posts 795 | 796 | Args: 797 | username_id (int): The user's ID 798 | max_pages (int): Maximum number of pages to get data from 799 | min_timestamp (int): Timestamp for earliest post to get 800 | 801 | Returns: 802 | (list(dict)): A list of posts (each post is a dictionary) a user has made 803 | """ 804 | 805 | next_max_id = '' 806 | counter = 0 807 | 808 | while 1 and counter < max_pages: 809 | self.getUserFeed(username_id, next_max_id, min_timestamp) 810 | temp = self.LastJson 811 | 812 | if not temp["more_available"]: 813 | counter = max_pages 814 | else: 815 | counter += 1 816 | next_max_id = temp["next_max_id"] 817 | 818 | yield temp.get("items", list()) 819 | 820 | 821 | @staticmethod 822 | def get_error_sleep_time(): 823 | """Convenience method to return a random time to sleep for when we encounter an error 824 | 825 | Returns: 826 | (int): Randomly generated integer for how long to sleep for 827 | """ 828 | 829 | return random.randint(random.randint(90, 120), random.randint(150, 240)) 830 | 831 | 832 | def getTotalUserFeed(self, username_id, max_pages=sys.maxint, min_timestamp=None): 833 | """Aggregate's a user's posts and returns it 834 | 835 | Args: 836 | username_id (int): The user's ID 837 | max_pages (int): Maximum number of pages to get data from 838 | min_timestamp (int) Timestamp for earliest post to get 839 | 840 | Returns: 841 | (list(dict)): A list of posts (each post is a dictionary) a user has made 842 | """ 843 | 844 | total_user_feed = list() 845 | next_max_id = '' 846 | counter = 0 847 | 848 | while 1 and counter < max_pages: 849 | 850 | try: 851 | self.getUserFeed(username_id, next_max_id, min_timestamp) 852 | temp = self.LastJson 853 | 854 | except Exception as e: 855 | print("ERROR GETTING USER FEED: %s" % e) 856 | time.sleep(get_error_sleep_time()) 857 | 858 | else: 859 | for item in temp.get("items", list()): 860 | total_user_feed.append(item) 861 | 862 | if not temp["more_available"]: 863 | counter = max_pages 864 | else: 865 | counter += 1 866 | next_max_id = temp["next_max_id"] 867 | 868 | 869 | return total_user_feed 870 | 871 | 872 | -------------------------------------------------------------------------------- /InstagramAnalyzer.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """InstagramAnalyzer.py: Interacts with Instagram.py and MongoDB for analysis""" 3 | 4 | import inspect 5 | import json 6 | import os 7 | import random 8 | import subprocess 9 | import sys 10 | import time 11 | 12 | from bson import json_util 13 | from CredentialManager import CredentialManager 14 | from Instagram import Instagram 15 | from InstagramAPI import InstagramAPI 16 | from pymongo import MongoClient 17 | from Users import InstagramUser 18 | 19 | import pymongo 20 | import requests 21 | 22 | 23 | #################################################################### 24 | # Written by Ryan D'souza 25 | # Main class for analyzing and storing Instagram Data 26 | #################################################################### 27 | 28 | 29 | 30 | 31 | class InstagramAnalyzer(Instagram): 32 | """Main class that analyzes Instagram and stores the data in MongoDB""" 33 | 34 | IS_DEVELOPMENT = True 35 | 36 | _database = None 37 | _users_collection = None 38 | 39 | 40 | def __init__(self, insta_username, insta_password, mongoclient_host, save_file_path=None): 41 | """Constructor 42 | 43 | Args: 44 | insta_username (str): string version of the username i.e.: 'dsouzarc' 45 | insta_password (str): string version of the password 46 | mongoclient_host (str): string host for MongoDB instance i.e.: "localhost://27017" 47 | """ 48 | 49 | 50 | Instagram.__init__(self, insta_username=insta_username, 51 | insta_password=insta_password, 52 | save_file_path=save_file_path) 53 | 54 | database_client = MongoClient(mongoclient_host) 55 | self._database = database_client["Instagram"] 56 | self._users_collection = self._database["users"] 57 | 58 | self._users_collection.create_index("pk", unique=True) 59 | 60 | 61 | #Convenience method 62 | def get_following(self, from_mongo=True, from_file=False): 63 | """Gets a list of all the users we are following on Instagram. 64 | 65 | Args: 66 | from_mongo (bool): bool for whether we should get saved Mongo data or live data 67 | from_file (bool): bool for whether we should get the data from a saved file 68 | 69 | Returns: 70 | dict(pk, dict): dict of who we are following. key: user PK, value: user 71 | """ 72 | 73 | all_following = dict() 74 | following = list() 75 | 76 | #Get the data from Mongo 77 | if from_mongo: 78 | following = self._users_collection.find({'am_following': True}) 79 | 80 | #Get the live data 81 | elif not from_mongo and not from_file: 82 | following = self.getTotalSelfFollowers() 83 | json.dump(following, open('all_following.json', 'w'), indent=4) 84 | 85 | #Get the data from a saved file 86 | else: 87 | saved_following = json.load(open('all_following.json', 'r')) 88 | 89 | for saved_following in following: 90 | saved_following['am_following'] = True 91 | all_following[saved_following['pk']] = saved_following 92 | 93 | return all_following 94 | 95 | 96 | #Convenience method 97 | def get_followers(self, from_mongo=True, from_file=False): 98 | """Gets a list of all the users that follow us on Instagram 99 | 100 | Args: 101 | from_mongo (bool): bool for whether we should get saved Mongo data or live data 102 | from_file (bool): bool for whether we should get the data from a saved file 103 | 104 | Returns: 105 | dict(pk, dict): dict of who follows us. key: user PK, value: user 106 | """ 107 | 108 | all_followers = dict() 109 | followers = list() 110 | 111 | #Get the data from Mongo 112 | if from_mongo: 113 | followers = self._users_collection.find({'is_follower': True}) 114 | 115 | #Get the live data 116 | elif not from_mongo and not from_file: 117 | followers = self.getTotalSelfFollowers() 118 | json.dump(followers, open('all_followers.json', 'w'), indent=4) 119 | 120 | #Get the data from a saved file 121 | else: 122 | followers = json.load(open('all_followers.json', 'r')) 123 | 124 | for follower in followers: 125 | follower['is_follower'] = True 126 | all_followers[follower['pk']] = follower 127 | 128 | return all_followers 129 | 130 | 131 | #For adding user information to Mongo 132 | def add_users_to_db(self, user_pks, skip_saved=True, is_follower=False, am_following=False): 133 | """Given a list of users, for each user, gets their information (1 API call) 134 | and saves it to MongoDB 135 | 136 | Args: 137 | user_pks (list(int)): list(int) of users' PKs 138 | skip_saved (bool): bool indicating if we should replace the data if it is in Mongo 139 | is_follower (bool): bool indicating if the user follows us 140 | am_following (bool): bool indicating if we follow the user 141 | """ 142 | 143 | skip_user_pks = set() 144 | 145 | #Add the saved user PKs from MongoDB to the Set 146 | if skip_saved: 147 | saved_user_pks = self._users_collection.find({}, {'pk': 1, '_id': 0}) 148 | for saved_user_pk in saved_user_pks: 149 | skip_user_pks.add(saved_user_pk['pk']) 150 | 151 | 152 | for user_pk in user_pks: 153 | if user_pk in skip_user_pks: 154 | print("Skipping: " + str(user_pk)) 155 | continue 156 | 157 | #New user, get their information 158 | try: 159 | raw_user_result = self.getUsernameInfo(user_pk) 160 | raw_user = self.LastJson["user"] 161 | 162 | #Error getting user from Instagram API - sleep then try again 163 | except requests.exceptions.RequestException as e: 164 | print("Requests exception: %s" % (e)) 165 | all_followers.append(follower) 166 | time.sleep(random.randint(180, 10 * 180)) 167 | 168 | #No error - let's insert the user into Mongo 169 | else: 170 | user = InstagramUser(raw_user, 171 | is_follower=is_follower, 172 | am_following=am_following) 173 | user.add_update("inserted") 174 | 175 | try: 176 | inserted_result = self._users_collection.insert_one(user.storage_dict()) 177 | 178 | #User already exists in MongoDB - let's replace 179 | except pymongo.errors.DuplicateKeyError: 180 | self._users_collection.delete_one({"pk": user.pk}) 181 | inserted_result = self._users_collection.insert_one(user.storage_dict()) 182 | 183 | finally: 184 | if inserted_result.acknowledged: 185 | print("Upserted: %s\t%s\t%s" % (user.full_name, user.username, 186 | inserted_result.inserted_id)) 187 | else: 188 | print("ERROR UPSERTING: %s", user_info) 189 | 190 | 191 | #Sleep for a bit before getting the next user 192 | sleep_delay = random.randint(0, 10) # 180)) 193 | time.sleep(sleep_delay) 194 | 195 | 196 | @staticmethod 197 | def default_mongodb(credential_manager=CredentialManager()): 198 | """Convenience method to return the MongoDB URL (ip, port, username, password) 199 | 200 | Args: 201 | credential_manager (CredentialManager): CredentialManager object; used for default info 202 | 203 | Returns: 204 | (str): Mongo Client host (mongo_url://username:password:port) 205 | """ 206 | 207 | mongodb_username, mongodb_password = credential_manager.get_account('InstagramMongoDB') 208 | mongodb_ip_address, mongodb_port = (credential_manager. 209 | get_values('InstagramMongoDBIPAddress', 210 | 'InstagramMongoDBPort')) 211 | 212 | 213 | mongo_client_host = ("mongodb://{username}:{password}@{ip_address}:{port}/" 214 | .format(username=mongodb_username, password=mongodb_password, 215 | ip_address=mongodb_ip_address, port=mongodb_port)) 216 | 217 | return mongo_client_host 218 | 219 | 220 | @staticmethod 221 | def default_client(): 222 | """Convenience method to return an Instagram object with the default credentials/set-up 223 | 224 | Returns: 225 | (:obj: `Instagram.Instagram`): Initialized version of our Instagram client 226 | """ 227 | 228 | credential_manager = CredentialManager() 229 | 230 | current_directory = os.path.abspath(inspect.getfile(inspect.currentframe())) 231 | save_file_path = os.path.dirname(current_directory) 232 | 233 | insta_username, insta_password = credential_manager.get_account('Instagram') 234 | 235 | mongo_client_host = InstagramAnalyzer.default_mongodb(credential_manager) 236 | 237 | 238 | client = InstagramAnalyzer(insta_username, insta_password, 239 | mongo_client_host, save_file_path) 240 | 241 | return client 242 | 243 | 244 | if __name__ == "__main__": 245 | """ Main method - run from here """ 246 | 247 | client = InstagramAnalyzer.default_client() 248 | 249 | client.get_messages() 250 | exit(0) 251 | 252 | # https://github.com/billcccheng/instagram-terminal-news-feed/blob/master/start.py 253 | 254 | 255 | all_followers = client.get_followers(from_mongo=False, from_file=True) 256 | all_following = client.get_following(from_mongo=False, from_file=True) 257 | 258 | for follower_pk in all_followers: 259 | if follower_pk in all_following: 260 | all_following[follower_pk]["is_follower"] = True 261 | all_followers[follower_pk]["am_following"] = True 262 | print("Following and followed by: " + all_followers[follower_pk].get("username")) 263 | 264 | client.add_users_to_db(all_followers.keys(), skip_saved=True, is_follower=True, am_following=False) 265 | 266 | exit(0) 267 | 268 | 269 | 270 | raw_media_info = json.loads(open("media_info.json").read()) 271 | raw_media_info = raw_media_info["items"][0] 272 | 273 | raw_tagged_users = raw_media_info["usertags"] 274 | tagged_users = [] 275 | print(raw_tagged_users) 276 | for tag_key, users in raw_tagged_users.items(): 277 | for user_dict in users: 278 | if "user" in user_dict and "username" in user_dict["user"]: 279 | user = user_dict["user"] 280 | tagged_user = { 281 | "full_name": user["full_name"], 282 | "profile_picture_link": user["profile_pic_url"], 283 | "pk": user["pk"], 284 | "is_private": user["is_private"], 285 | "username": user["username"] 286 | } 287 | tagged_users.append(tagged_user) 288 | 289 | 290 | 291 | 292 | 293 | 294 | """ 295 | 296 | client.api.timelineFeed() 297 | timeline_feed = client.api.LastJson 298 | 299 | for mediaItem in timeline_feed["items"]: 300 | 301 | media_id = mediaItem["id"] 302 | client.api.mediaInfo(media_id) 303 | print(json.dumps(client.api.LastJson, indent=4)) 304 | break 305 | 306 | """ 307 | -------------------------------------------------------------------------------- /Modifier.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import json 4 | import random 5 | import subprocess 6 | import time 7 | 8 | from bson import json_util 9 | from CredentialManager import CredentialManager 10 | from pymongo import MongoClient 11 | from InstagramAPI import InstagramAPI 12 | from Instagram import Instagram 13 | from Users import InstagramUser 14 | 15 | import pymongo 16 | import requests 17 | 18 | 19 | credentials = CredentialManager() 20 | 21 | insta_user, insta_pass = credentials.get_account('Instagram') 22 | mongo_user, mongo_pass = credentials.get_account('InstagramMongoDB') 23 | mongo_ip, mongo_port = credentials.get_values('InstagramMongoDBIPAddress', 'InstagramMongoDBPort') 24 | 25 | 26 | mongo_client_host = ("mongodb://{username}:{password}@{ip_address}:{port}/" 27 | .format(username=mongo_user, 28 | password=mongo_pass, 29 | ip_address=mongo_ip, 30 | port=mongo_port)) 31 | 32 | print(mongo_client_host) 33 | exit(0) 34 | 35 | client = Instagram(insta_user, insta_pass, mongo_client_host) 36 | 37 | api = None 38 | database = None 39 | users_collection = None 40 | database_client = MongoClient(mongo_client_host) 41 | database = database_client["Instagram"] 42 | users_collection = database["users"] 43 | 44 | client.following_follower_diff() 45 | 46 | #Add the most recent followers to the database 47 | """client.IS_DEVELOPMENT = False 48 | client.add_followers_to_db(skip_saved=True) 49 | """ 50 | 51 | #users_collection.update_many({}, { '$set': {'is_follower': True}}) 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /PostAggregator.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """PostAggregator.py: Handles getting newsfeed and storing post information to Mongo""" 3 | 4 | import json 5 | import random 6 | import subprocess 7 | import sys 8 | import time 9 | 10 | from bson import json_util 11 | from CredentialManager import CredentialManager 12 | from InstagramAPI import InstagramAPI 13 | from Instagram import Instagram 14 | from pymongo import MongoClient 15 | from Users import InstagramUser 16 | 17 | import pymongo 18 | import requests 19 | 20 | 21 | class PostAggregator(object): 22 | 23 | _instagram = None #Instagram wrapper 24 | _database = None 25 | 26 | def __init__(self, instagram): 27 | """ Constructor 28 | 29 | Args: 30 | instagram (`obj`: Instagram): Instagram object from Instagram.py 31 | """ 32 | 33 | self._instagram = instagram 34 | self._database = instagram._database 35 | 36 | 37 | def store_posts_for_user(self, username_id, table_name, max_posts=sys.maxint): 38 | """Gets the posts for a user and stores them in MongoDB 39 | 40 | Args: 41 | username_id (int): The user's ID 42 | table_name (str): String for the table name in Mongo 43 | max_posts (int): Maximum number of posts to store 44 | """ 45 | 46 | counter = 0 47 | total_posts = list() 48 | mongo_table = self._database[table_name] 49 | 50 | userfeed_generator = aggregator._instagram.user_feed_generator(username_id, 51 | max_pages=max_posts) 52 | 53 | for posts_list in userfeed_generator: 54 | for post in posts_list: 55 | mongo_table.insert_one(post) 56 | total_posts.append(post) 57 | counter += 1 58 | print("# of posts: %s\t # of API requests: %s" % (len(total_posts), counter)) 59 | 60 | if counter % 4 == 0: 61 | time.sleep(random.randint(random.randint(20, 110), random.randint(111, 180))) 62 | else: 63 | time.sleep(random.randint(random.randint(10, 30), random.randint(31, 45))) 64 | 65 | 66 | 67 | if __name__ == "__main__": 68 | 69 | client = Instagram.default_client() 70 | aggregator = PostAggregator(client) 71 | 72 | aggregator.store_posts_for_user(1405454497, 'tfm_girls_2', 5) 73 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Written by Ryan D'souza 2 | 3 | ## Instagram Analyzer - Python script 4 | 5 | Following my passion for Data Science, this repository hosts code that will be used to analyze Instagram. 6 | Data is stored in a (private) MongoDB instance for analysis. 7 | 8 | **Ideas include:** 9 | - Prime time to post 10 | 11 | - Measuring how interactions and relationships change over time 12 | 13 | - Caption generation 14 | 15 | - Heat maps that show where friends post the most number of photos 16 | 17 | - Search for specific criteria (i.e.: "who went to Oktoberfest in Munich") 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /Users.py: -------------------------------------------------------------------------------- 1 | from bson import json_util 2 | from datetime import datetime 3 | 4 | import json 5 | 6 | 7 | #################################################################### 8 | # Written by Ryan D'souza 9 | # Represents an Instagram User stored in MongoDB 10 | #################################################################### 11 | 12 | 13 | def get_datetime(): 14 | """Convenience method for current date as a string 15 | 16 | Returns: 17 | (str): string for today in YYYY-MM-dd 18 | """ 19 | return datetime.now().strftime('%Y-%m-%d') 20 | 21 | 22 | class InstagramUser(object): 23 | """Represents an InstagramUser stored in MongoDB 24 | Object-Oriented design for convenience and encapsulation when manipulating 25 | more complicated data which might be missing 26 | """ 27 | 28 | full_name = None 29 | profile_picture_link = None 30 | following_count = None 31 | follower_count = None 32 | biography = None 33 | pk = None 34 | username = None 35 | is_private = False 36 | is_business = False 37 | is_follower = False 38 | am_following = False 39 | 40 | last_updated = None 41 | updates = None 42 | states = None 43 | 44 | 45 | def __init__(self, raw_user_info, last_updated=None, updates=None, 46 | states=None, is_follower=False, am_following=False): 47 | """Constructor 48 | Args: 49 | raw_user_info (dict): dict JSON response from Instagram getUsernameInfo API call 50 | last_updated (str): string YYY-MM-dd for when this user was last updated 51 | updates (dict): dict{string date: list(operation)} for when/what we did 52 | states (dict): dict{string date: json difference} 53 | is_follower (bool): bool indicating whether or not they follow us 54 | am_following (bool): bool indicating whether or not we follow them 55 | """ 56 | 57 | self.full_name = raw_user_info["full_name"] 58 | self.profile_picture_link = raw_user_info["hd_profile_pic_url_info"]["url"] 59 | self.following_count = raw_user_info["following_count"] 60 | self.follower_count = raw_user_info["follower_count"] 61 | self.biography = raw_user_info["biography"] 62 | self.pk = raw_user_info["pk"] 63 | self.username = raw_user_info["username"] 64 | self.is_private = raw_user_info["is_private"] 65 | self.is_business = raw_user_info["is_business"] 66 | 67 | self.is_follower = is_follower 68 | self.am_following = am_following 69 | 70 | 71 | if last_updated is None: 72 | self.last_updated = raw_user_info.get("last_updated", get_datetime()) 73 | else: 74 | self.last_updated = get_datetime() 75 | 76 | if updates is None: 77 | self.updates = raw_user_info.get("updates", dict()) 78 | else: 79 | self.updates = {} 80 | 81 | if states is None: 82 | self.states = raw_user_info.get("states", dict()) 83 | else: 84 | self.states = {} 85 | 86 | 87 | def add_update(self, update, current_date=get_datetime()): 88 | """Adds an update to this user with the current date as a key 89 | 90 | Args: 91 | update (str): string description of the update 92 | """ 93 | 94 | if current_date not in self.updates: 95 | self.updates[current_date] = list() 96 | 97 | self.updates[current_date].append(update) 98 | self.last_updated = current_date 99 | 100 | 101 | def add_state(self, state, current_date=get_datetime()): 102 | """Adds the old state of this user with the current date as a key 103 | 104 | Args: 105 | state (dict): Dictionary of the user 106 | """ 107 | 108 | self.states[current_date] = state 109 | self.last_updated = current_date 110 | 111 | 112 | def storage_dict(self): 113 | """Convenience method to return this object as a storable dictionary 114 | 115 | Returns: 116 | (dict): Dictionary representation of this object and its values 117 | """ 118 | return vars(self) 119 | 120 | -------------------------------------------------------------------------------- /sample_constants.json: -------------------------------------------------------------------------------- 1 | { 2 | "uuid": "[Will be filled in]", 3 | "SIG_KEY_VERSION": "4", 4 | "rank_token": "[Will be filled in]", 5 | "last_login_time": "[Will be filled in]", 6 | "EXPERIMENTS": "ig_android_progressive_jpeg,ig_creation_growth_holdout,ig_android_report_and_hide,ig_android_new_browser,ig_android_enable_share_to_whatsapp,ig_android_direct_drawing_in_quick_cam_universe,ig_android_huawei_app_badging,ig_android_universe_video_production,ig_android_asus_app_badging,ig_android_direct_plus_button,ig_android_ads_heatmap_overlay_universe,ig_android_http_stack_experiment_2016,ig_android_infinite_scrolling,ig_fbns_blocked,ig_android_white_out_universe,ig_android_full_people_card_in_user_list,ig_android_post_auto_retry_v7_21,ig_fbns_push,ig_android_feed_pill,ig_android_profile_link_iab,ig_explore_v3_us_holdout,ig_android_histogram_reporter,ig_android_anrwatchdog,ig_android_search_client_matching,ig_android_high_res_upload_2,ig_android_new_browser_pre_kitkat,ig_android_2fac,ig_android_grid_video_icon,ig_android_white_camera_universe,ig_android_disable_chroma_subsampling,ig_android_share_spinner,ig_android_explore_people_feed_icon,ig_explore_v3_android_universe,ig_android_media_favorites,ig_android_nux_holdout,ig_android_search_null_state,ig_android_react_native_notification_setting,ig_android_ads_indicator_change_universe,ig_android_video_loading_behavior,ig_android_black_camera_tab,liger_instagram_android_univ,ig_explore_v3_internal,ig_android_direct_emoji_picker,ig_android_prefetch_explore_delay_time,ig_android_business_insights_qe,ig_android_direct_media_size,ig_android_enable_client_share,ig_android_promoted_posts,ig_android_app_badging_holdout,ig_android_ads_cta_universe,ig_android_mini_inbox_2,ig_android_feed_reshare_button_nux,ig_android_boomerang_feed_attribution,ig_android_fbinvite_qe,ig_fbns_shared,ig_android_direct_full_width_media,ig_android_hscroll_profile_chaining,ig_android_feed_unit_footer,ig_android_media_tighten_space,ig_android_private_follow_request,ig_android_inline_gallery_backoff_hours_universe,ig_android_direct_thread_ui_rewrite,ig_android_rendering_controls,ig_android_ads_full_width_cta_universe,ig_video_max_duration_qe_preuniverse,ig_android_prefetch_explore_expire_time,ig_timestamp_public_test,ig_android_profile,ig_android_dv2_consistent_http_realtime_response,ig_android_enable_share_to_messenger,ig_explore_v3,ig_ranking_following,ig_android_pending_request_search_bar,ig_android_feed_ufi_redesign,ig_android_video_pause_logging_fix,ig_android_default_folder_to_camera,ig_android_video_stitching_7_23,ig_android_profanity_filter,ig_android_business_profile_qe,ig_android_search,ig_android_boomerang_entry,ig_android_inline_gallery_universe,ig_android_ads_overlay_design_universe,ig_android_options_app_invite,ig_android_view_count_decouple_likes_universe,ig_android_periodic_analytics_upload_v2,ig_android_feed_unit_hscroll_auto_advance,ig_peek_profile_photo_universe,ig_android_ads_holdout_universe,ig_android_prefetch_explore,ig_android_direct_bubble_icon,ig_video_use_sve_universe,ig_android_inline_gallery_no_backoff_on_launch_universe,ig_android_image_cache_multi_queue,ig_android_camera_nux,ig_android_immersive_viewer,ig_android_dense_feed_unit_cards,ig_android_sqlite_dev,ig_android_exoplayer,ig_android_add_to_last_post,ig_android_direct_public_threads,ig_android_prefetch_venue_in_composer,ig_android_bigger_share_button,ig_android_dv2_realtime_private_share,ig_android_non_square_first,ig_android_video_interleaved_v2,ig_android_follow_search_bar,ig_android_last_edits,ig_android_video_download_logging,ig_android_ads_loop_count_universe,ig_android_swipeable_filters_blacklist,ig_android_boomerang_layout_white_out_universe,ig_android_ads_carousel_multi_row_universe,ig_android_mentions_invite_v2,ig_android_direct_mention_qe,ig_android_following_follower_social_context", 7 | "IG_SIG_KEY": "012a54f51c49aa8c5c322416ab1410909add32c966bbaa0fe3dc58ac43fd7ede", 8 | "USER_AGENT": "Instagram 50.0.0 Android ({android_version}/{android_release}; 320dpi; 720x1280; {manufacturer}; {model}; armani; qcom; en_US)", 9 | "csrftoken": "[Will be filled in as an integer]", 10 | "DEVICE_SETTINGS": { 11 | "android_release": "4.3", 12 | "model": "HM 1SW", 13 | "android_version": 18, 14 | "manufacturer": "Xiaomi" 15 | }, 16 | "username_id": "[Will be filled in]", 17 | "device_id": "[Will be filled in]" 18 | } 19 | --------------------------------------------------------------------------------