From 151c1644b23d42cf43dc0ae428e2c580a6776fbe Mon Sep 17 00:00:00 2001 From: Fytex Date: Wed, 30 Sep 2020 19:58:53 +0100 Subject: [PATCH] Patches, code and config's structure reformatted and added new features Code structure reformatted Config.ini structure reformatted Added force_search and specific_file as parameters to config Patch - Clicking on a random user while scrolling down Patch - Scrolling up a bit while scrolling down Added 'ctrl+c' (SIGINT) to stop execution and save all work done until the moment New informative messages in CLI Waiting time more efficient and cleaner README.md edited Solved patch by executing JavaScript to scroll down --- README.md | 57 ++--- config.ini | 49 +++-- modules/__init__.py | 2 + modules/browser.py | 74 +++++++ modules/comments.py | 35 +++ modules/implicitly_wait.py | 41 ++++ modules/instagram_bot.py | 440 +++++++++++++++++++------------------ script.py | 151 ++++++++++--- 8 files changed, 559 insertions(+), 290 deletions(-) create mode 100644 modules/__init__.py create mode 100644 modules/browser.py create mode 100644 modules/comments.py create mode 100644 modules/implicitly_wait.py diff --git a/README.md b/README.md index 9103ea7..52f8b54 100644 --- a/README.md +++ b/README.md @@ -11,10 +11,10 @@ ## How does this bot work? It works as a browser simulator using selenium. -There are four steps: +At the beginning there are four steps (It will save data in order to don't waste your time): 1. Log in -2. Find user from post +2. Find post's owner 3. Find followers/followings 4. Start spamming mentions in the post @@ -34,13 +34,13 @@ But don't worry... if you feel unsafe you can install these files by yourself (j ### Setup -1. Install Google Chrome (Check `config.ini` Chrome's category if you want a different path) -> if you guys start complaining about this specific step I'll make some updates to have wider options +1. Install Google Chrome (Check Browser's category in `config.ini` if you want a different path) -> if you guys start complaining about this specific step I'll make some updates to have wider options 2. Install Python 3.6+ (Don't forget to add in system variable `PATH`) 3. Open terminal, change directory to Instagram-Giveaways-Winner's folder and type: `pip install -r requirements.txt` -4. Edit config.ini (See next category) +4. Edit `config.ini` (See next category) 5. In the same terminal type: `py script.py` -Warning: Avoid resizing or touching the Browser oppened. You can minimize if you want or if you want to get rid of it just change `Window` to `False` in `config.ini` +Warning: Avoid resizing or touching the Browser oppened. You can minimize if you want or if you want to get rid of it just change Window at Browser's category to `False` in `config.ini` These commands can change depending on your configuration. Such as python/py/python3... or pip/pip3... @@ -51,22 +51,30 @@ If you need help add me on discord or join the server and ask me (links in my pr Browser's window can be invisible (in background) if Window's option is deactivated. -- Step 1 requires both username/email and password. +- **Login** requires both username/email and password. + - A cookie's data will be saved to login automatically in order to prevent repetitve actions. -- Step 2 will only occur if no User Target is specified in the file. - - User Target is the user where the bot will get the followers/followings to mention. +- **Find post's owner** will only occur if no User Target is specified in the file. + - User Target is the user where the bot will search for followers/followings to mention. - The timeout is a way to prevent blocking while trying to log in by stopping the program. -- Step 3 is the search and find part where the bot saves all users. - - You can specify the limit of users in the file (it will pick the lowest one between the limit and the number of followers/followings from the User Target, the higher the number is, more time will take). - - In the file you can either choose followers or followings. - - The timeout is a way to prevent blocking while searching for followers/followings in case it gets stuck by stopping the program. - -- Step 4 is where all the fun begins. It starts spamming mentions in the post. - - By enabling Save Only's option this step won't run. This option is used in case you only want to save the followers/followings and use them later. +- **Find followers/followings** is where the bot searches and saves all followers/followings. + - You can specify the limit of followers/followings (it will pick the lowest one between the limit and the number of followers/followings from the User Target). + - First it will search if there are the limit number of followers/followings inside a file in a specific file in records (database) + - If Force Search is disabled and there is no limit and a file was found then it uses that amount of followers/followings to mention. + - If Force Search is enabled or there aren't enough followers/followers in the file or file doesn't exist it will search on Instagram (User Target's followers/followings) until it meets the limit. If no limit specified then searches for all of them. + - In `config.ini` you can either choose to search for followers or followings. + - The timeout is a way to prevent blocking while searching for followers/followings in case it gets stuck. + - If you raise SIGNINT by pressing 'ctrl + c' in your keyboard. It will stop searching and will immediately save all followers/followings found into a file in records. + - If you have a custom/specific file which you want to use just set the relative path at 'Specific File' in `config.ini`. + +- **Start spamming mentions in the post** is where all the fun begins. It starts spamming mentions in the post. + - By enabling SaveOnly's option this step won't run. This option is used in case you only want to save the followers/followings and use them later. - You can edit the message and add as many mentions as you want (mentions will never be repeated). - - Interval Category in `config` file lets you choose how much time it waits before each comment. You have to find out by yourself the best interval that fits your account. It has a min, max and weight so the number isn't always the same preventing Instagram finding out it is a bot. (Later I will try to create kind of an A.I. to do this for you) - - In case you have a char that doesn't belong to BMP (such as some emojis) it will appear a different message. But your message will still be sent. (Refresh when finished or open in another browser to check everything is fine) + - Interval's category in `config.ini` file lets you choose how much time it waits before each comment. You have to find out by yourself the best interval that fits into your account. It has a min, max and weight so the number isn't always the same preventing Instagram finding out it is a bot. (The smaller the interval the higher the chance of having to wait to comment again because of Instagram's A.I.) (You have to play with these numbers until you find out the best interval range for you) + - In case you have a char that doesn't belong to BMP (such as some emojis) it will appear a different message. But your message will still be sent. (Refresh when finished or open in another browser to check everything is fine) + - By pressing 'ctrl+c' it raises SIGINT in order to stop the execution of the program. + ### How do the records (database) work? @@ -75,9 +83,8 @@ There are two sections: - followers - followings -Depending on what you choose it will save in the respective directory. Then it will choose the file's name using the following format `{User Target}_{total}.json` where `User Target` is the user where we got the followers/followings and `total` is the number of users we got. -When searching for a `User Target` if the lowest one between the `limit` (in the config file) and the number of followers/followings from the `User Target` is already met in a file then it will skip Step 2 and use that file automatically. - +Depending on what you choose it will save in the respective directory. Then it will choose the file's name using the following format `{User Target}.txt` where `User Target` is the user where we got the followers/followings. +By following the same pattern you see on the other records (`@user` in each line) you can create a custom file (E.g. `custom.txt`) and enable Specific File in `config.ini` (removing `#`) and changing the value to the path (E.g. `records\custom.txt`) ### How do the cookies work (auto-login)? @@ -89,14 +96,12 @@ When the program logs into your account it saves in a separated folder called `c I would recommend using an alternative account which you aren't afraid of losing because it could go wrong in the worst case. However I've never experienced any bad situations using this script. -Instagram has a comment's request rate-limit to avoid spamming. From my research it has an algorithm which varies from user to user. Since Instagram doesn't provide a time for reset we have to try every x seconds. We chose it to be every 10 seconds. If a message pops up saying that it couldn't post the comment its because you hit that rate-limit so you just have to wait. (You don't have to do anything its all automatic) - +Instagram has a comment's request rate-limit to avoid spamming. From my research it has an algorithm which varies from user to user. Since Instagram doesn't provide a time for reset we have to try every x seconds. In `config.ini` at Interval's category you can choose the range of the intervale by setting the minimum, maximum and the most probable number for the interval between comments (Better a range of numbers instead of always the same number in order to avoid being rate-limited). If a message pops up saying that it couldn't post the comment its because you hit that rate-limit so you just have to wait. (You don't have to do anything its all automatic but if this happens you should consider raising those intervals) +If you want to be able to send more comments you have to make a good use of your account (following, commenting, liking, etc.). That's how Instagram's A.I.'s works. #### Future Updates: - [ ] Add a followers/followings tracker so it won't repeat the count if we restart the bot - - [ ] Add a way to find followers/followings some at a time until it reaches the limit/maximum. This way we can find followers/followings and post comments in a cycle. - - [ ] Set a specific file from records (database) to use as users to mention - - [ ] Find out the best interval between each comment for your account + - [ ] Find out the best interval between each comment for your account (This requires researching and planning by using a lot of my time. Don't see when this would release because I'm not being financially paid for this) - [ ] Compatibility with more browsers @@ -105,4 +110,4 @@ Instagram has a comment's request rate-limit to avoid spamming. From my research raise exception_class(message, screen, stacktrace) selenium.common.exceptions.WebDriverException: Message: unknown error: cannot find Chrome binary ``` - - Since Chrome has updated their files' location, Selenium hasn't fixed it yet. Check `config.ini` Chrome's Category to fix it. + - Since Chrome has updated their files' location, Selenium hasn't fixed it yet. Check `config.ini` Browser's Category to fix it. diff --git a/config.ini b/config.ini index 7b48031..24b3680 100644 --- a/config.ini +++ b/config.ini @@ -1,14 +1,13 @@ [Required] - Post Link = url + Post Link = https://www.instagram.com/p/code/ # Post Link can be disabled (writing # before Post Link) only in case if Save Only is enabled and a User Target specified - Expression = I want to win this giveaway! @, @ + Expression = \@StaticUser, I'm here to mention two of my friends. @ and @ # Use @ to choose where to write each mention - # in case you want to write a specific @ use the - # following escape character '\' like this \@fytex - # You can edit this message as much as you want - # and add as many mentions as you want + # In case you want to write a specific @ use the following escape character '\' like this \@fytex + # You can edit this message as much as you want and add as many mentions as you want even special characters and emojis + # If no mentions then it doesn't search for followers/followings except if Save Only is enabled Username = username @@ -17,23 +16,26 @@ [Optional] # Remove '#' before each line to enable option - #Window = True - # Show Web Browser (if not then runs in background) - - #User Target = User - # User to pick followers/followings for mentions + #User Target = usertarget + # User to search for followers/followings - #Followers = True - # if True then uses UserTarget's followers - # if False then uses UserTarget's followings + #Followers = False + # if True then searches for User Target's followers + # if False then searches for User Target's followings - #Limit = 200 + #Limit = 200 # Limit of users to mention (This corresponds to the maximum number of users to fetch during followers/followings' search) + # Warning: Enable this and set a limit if User Target has too many followers/followings otherwise you will have to wait a long time or stop the operation + # You can stop the operation of searching by pressing 'ctrl + c' [SIGINT] and all the followers/followings searched will be saved + + #Specific File = records\custom.txt + # Relative path to the records' custom file - #Timeout = 30 - # Limit time to wait in case it gets stuck + #Force Search = True + # Forces searching for users to mention. This option adds new followers/followings to previous records + # You can't enable this option and have a limit at the same time - #Save Only = False + #Save Only = True # Don't spam (only save data) [Requires either Post Link or User Target] @@ -45,13 +47,17 @@ #Max = 120 #Weight = 90 + # Weight must be a number between Min and Max # Numbers tend to be close to Weight's number # if not specified it will use the midpoint between Min and Max -[Chrome] # Remove '#' before each line to enable option +[Browser] # Remove '#' before each line to enable option - #Default Lang = False + #Window = False + # Show Web Browser (if not then runs in background) + + #Default Lang = True # if False it uses English # if True it uses your Chrome's Language # Warning: Very low chance of crashing program if True @@ -64,3 +70,6 @@ # # enable this option because Chrome is changing paths and Selenium hasn't fixed yet. # if error still persists find where Chrome is located and put the path ending in the executable + + #Timeout = 30 + # Limit time to wait in case it gets stuck (don't change this except if your internet/computer is too slow and so raise the number) \ No newline at end of file diff --git a/modules/__init__.py b/modules/__init__.py new file mode 100644 index 0000000..638bffb --- /dev/null +++ b/modules/__init__.py @@ -0,0 +1,2 @@ +from .browser import Tab +from .instagram_bot import Bot diff --git a/modules/browser.py b/modules/browser.py new file mode 100644 index 0000000..300387d --- /dev/null +++ b/modules/browser.py @@ -0,0 +1,74 @@ +import os + +from selenium import webdriver # type: ignore +from typing import Optional +from sys import platform + +class Browser: + def __init__(self, window:bool=True, binary_location:Optional[str]=None, default_lang:bool=False, **kwargs): + + if platform == 'linux' or platform == 'linux2': + driver_file_name = 'chrome_linux' + elif platform == 'win32': + driver_file_name = 'chrome_windows.exe' + elif platform == 'darwin': + driver_file_name = 'chrome_mac' + + driver_path = os.path.join(os.getcwd() , f'drivers{os.path.sep}{driver_file_name}') + + os.chmod(driver_path , 0o755) + + options = webdriver.ChromeOptions() + + if not default_lang: + options.add_experimental_option('prefs', {'intl.accept_languages': 'en,en_US'}) + + options.headless = not window ; + + if binary_location: + options.binary_location = binary_location + + + self.driver = webdriver.Chrome(executable_path=driver_path, options=options) + + +class Tab: + + def __init__(self, driver:webdriver.Chrome, url:str): + self.driver = driver + self.url = url + + + def new_tab(self, url:str='https://www.google.com'): + + ''' + + Opens a new tab on Browser + + Args: + - url : to navigate after openning tab + + ''' + + self.driver.execute_script(f'window.open(\'{url}\');') + self.driver.switch_to.window(self.driver.window_handles[-1]) + + + def close_tab(self): + + ''' + + Close the last tab on Browser + + ''' + + self.driver.close() + self.driver.switch_to.window(self.driver.window_handles[-1]) # could write 'main' but later on could be modified + + + def __enter__(self): + self.new_tab(self.url) + + + def __exit__(self, *exc): + self.close_tab() diff --git a/modules/comments.py b/modules/comments.py new file mode 100644 index 0000000..b3544ae --- /dev/null +++ b/modules/comments.py @@ -0,0 +1,35 @@ +from typing import Iterator, List +from itertools import chain + +class Comments: + + def __init__(self, iter_connections:Iterator[str], parts_expr:List[str]): + self.iter_connections = iter_connections + self.parts_expr = parts_expr + + def generate(self) -> Iterator[str]: + + ''' + + Generates every comment from an expression and a list of connections + + ''' + + last_part = self.parts_expr[-1] + + while True: + + if len(self.parts_expr) == 1: + yield last_part + + else: + + try: + + users = next(self.iter_connections) + except StopIteration: + return + + comment = ''.join(chain.from_iterable(zip(self.parts_expr, users))) + + yield (comment + last_part).replace(r'\@', '@') diff --git a/modules/implicitly_wait.py b/modules/implicitly_wait.py new file mode 100644 index 0000000..436d15b --- /dev/null +++ b/modules/implicitly_wait.py @@ -0,0 +1,41 @@ +from contextlib import contextmanager +from selenium import webdriver # type: ignore + +class ImplicitlyWait: + def __init__(self, driver:webdriver, timeout:int): + self.driver = driver + self.timeout = timeout + + def enable(self): + + ''' + + Enable implicitly wait so it doesn't throw errors without waiting some time in order to let the element appear + + ''' + + self.driver.implicitly_wait(self.timeout) + + def disable(self): + + ''' + + Disable implicitly wait so it doesn't wait for the element to appear. This can cause errors if not handled with a 'Explicitly Wait' + + ''' + + self.driver.implicitly_wait(0) + + @contextmanager + def ignore(self): + + ''' + + Ingore implicitly wait in the current block of code by disabling and enabling again when finished + + ''' + + try: + yield self.disable() + finally: + self.enable() diff --git a/modules/instagram_bot.py b/modules/instagram_bot.py index 0744e8d..b612722 100644 --- a/modules/instagram_bot.py +++ b/modules/instagram_bot.py @@ -1,84 +1,55 @@ -from selenium import webdriver -from selenium.webdriver.common.keys import Keys -from selenium.webdriver.support.wait import WebDriverWait -from selenium.common.exceptions import WebDriverException -from typing import List, Iterator, Callable -from itertools import chain -from time import perf_counter, sleep import re -import sys -import os -from pathlib import Path import json -class Comments: - - def __init__(self, iter_connections:Iterator[str], parts_expr:List[str]): - self.iter_connections = iter_connections - self.parts_expr = parts_expr - - def generate(self) -> Iterator[str]: - - last_part = self.parts_expr[-1] - - while True: - - if len(self.parts_expr) == 1: - yield last_part +from pathlib import Path +from itertools import islice +from time import perf_counter, sleep +from selenium.webdriver.common.by import By # type: ignore +from selenium.webdriver.common.keys import Keys # type: ignore +from typing import List, Iterator, Callable, Optional +from selenium.webdriver.support.wait import WebDriverWait # type: ignore +from selenium.webdriver.support import expected_conditions as EC # type: ignore +from selenium.common.exceptions import WebDriverException # type: ignore - else: - - try: - - users = next(self.iter_connections) - except StopIteration: - return +from .browser import Browser +from .comments import Comments +from .implicitly_wait import ImplicitlyWait - comment = ''.join(chain.from_iterable(zip(self.parts_expr, users))) - yield (comment + last_part).replace(r'\@', '@') +class Bot(Browser): + __version__ = '2.0.0' -class Bot: - __version__ = '1.2.3' + def __init__(self, *args, **kwargs): - - def __init__(self, window:bool=True, timeout:int=30, binary_location:str=None, default_lang:bool=False): - - if sys.platform == 'linux' or sys.platform == 'linux2': - driver_file_name = 'chrome_linux' - elif sys.platform == 'win32': - driver_file_name = 'chrome_windows.exe' - elif sys.platform == 'darwin': - driver_file_name = 'chrome_mac' - - driver_path = os.path.join(os.getcwd() , f'drivers{os.path.sep}{driver_file_name}') - - os.chmod(driver_path , 0o755) + self.url_base = 'https://www.instagram.com/' + self.url_login = self.url_base + 'accounts/login' + self.timeout = kwargs.get('timeout', 30) + self.records_path = kwargs['records_path'] + self.connections = [] + self.num_comments = 0 # Have to save in the instance in order to display in terminal in case user raises KeyboardInterrupt - options = webdriver.ChromeOptions() + super().__init__(*args, **kwargs) - if not default_lang: - options.add_experimental_option('prefs', {'intl.accept_languages': 'en,en_US'}) + self.implicitly_wait = ImplicitlyWait(self.driver, self.timeout) + self.implicitly_wait.enable() - options.headless = not window ; - if binary_location: - options.binary_location = binary_location + def log_in(self, username:str, password:str): - - self.driver = webdriver.Chrome(executable_path=driver_path, options=options) + ''' + Logs into Instagram account using the given credentials - self.url_base = 'https://www.instagram.com/' - self.url_login = self.url_base + 'accounts/login' - self.timeout = timeout + Args: + - username : user's username + - password : user's password - def log_in(self, username:str, password:str): + ''' COOKIE_NAME = 'sessionid' - + self.driver.get(self.url_login) try: @@ -88,28 +59,20 @@ def log_in(self, username:str, password:str): except FileNotFoundError: pass - + else: + self.driver.add_cookie(cookie) # I find out that chrome sends a warning message after loading a cookie WebDriverWait(self.driver, self.timeout).until( - lambda x: self.driver.get_log('browser')) - - self.driver.refresh() - + lambda driver: driver.get_log('browser')) - # Waits for information about logged status - WebDriverWait(self.driver, self.timeout).until( - lambda x: x.find_elements_by_css_selector('form input')) + self.driver.refresh() if 'not-logged-in' in self.driver.find_element_by_tag_name('html').get_attribute('class'): - # Waits for form - WebDriverWait(self.driver, self.timeout).until( - lambda x: x.find_elements_by_css_selector('form input')) - username_input, password_input, *_ = self.driver.find_elements_by_css_selector('form input') username_input.send_keys(username) @@ -117,7 +80,7 @@ def log_in(self, username:str, password:str): WebDriverWait(self.driver, self.timeout).until( lambda x: 'js logged-in' in x.find_element_by_tag_name('html').get_attribute('class')) - + cookie = self.driver.get_cookie(COOKIE_NAME) Path('cookies/').mkdir(exist_ok=True) @@ -125,154 +88,159 @@ def log_in(self, username:str, password:str): with open(f'cookies/{username}.json', 'w') as file: json.dump(cookie, file) - - def new_tab(self, url:str='https://www.google.com'): - - self.driver.execute_script(f'window.open(\'{url}\');') - self.driver.switch_to.window(self.driver.window_handles[-1]) - - - def close_tab(self): - - self.driver.close() - self.driver.switch_to.window(self.driver.window_handles[-1]) # could write 'main' but later on could be modified - - - def get_user_connections(self, username:str, limit:int=None, followers=True) -> List[str]: + def get_user_connections_from_records(self, username:Optional[str]=None, specific_file:Optional[str]=None, limit:Optional[int]=None, followers:bool=True) -> bool: ''' Connections means followers or followings depending on the chosen data Args: - - username : target's username - - limit : limit number of connections to save - - followers: if True returns a list of user's followers, if False returns of user's followings + - username : target's username + - specific_file : file to open instead of searching for a {username}.txt + - limit : limit number of connections (followers/followings) to save + - followers : if True returns a list of user's followers, if False returns of user's followings Returns: - - list of usernames + - success: True if we could get the number of connections as the limit + ''' - - if limit == 0: - return [] - - path = 'records//' + ('followers' if followers else 'followings') - - if limit: - - try: - - with open(f'{path}//{username}_{limit}.json', 'r') as file: - return json.load(file) - - except FileNotFoundError: - pass - - self.new_tab(self.url_base + username) - - WebDriverWait(self.driver, self.timeout).until( - lambda x: x.find_element_by_css_selector('header h1')) - - if followers: - connections_link = self.driver.find_element_by_css_selector('ul li a span') - connections_limit = int(connections_link.get_attribute('title').replace(',', '').replace('.', '').replace(' ', '')) + + try: + + with open(specific_file or f'{self.records_path}//{username}.txt', 'r') as file: + + self.connections = list(map(lambda x: x.rstrip('\n'), islice(file, limit) if limit \ + else file.readlines())) + + except FileNotFoundError: + pass + else: - connections_link = self.driver.find_element_by_css_selector('ul li:nth-child(3) a span') - - try: - connections_limit = int(connections_link.text.replace(',', '')) - - except ValueError: - exit(''' - You must choose a UserTarget which following < 10,000 users - This happens because instagram doesn't provide by source the whole number, - and it would be a pain to translate every possible letter - ''') - - limit = min(connections_limit, limit) if limit else connections_limit - - if limit == connections_limit: - try: - - with open(f'{path}//{username}_{limit}.json', 'r') as file: - self.close_tab() - return json.load(file) - - except FileNotFoundError: - pass - + + if self.connections and not limit or len(self.connections) == limit: + return True + + return False + + + def save_connections(self, username:str, connections_ext:List[str]): + + ''' + + Saves connections_ext in a file called {username}.txt + + Args: + - username : target's username + - connections_ext : list of connections to be appended to the file + + ''' + + Path(self.records_path).mkdir(parents=True, exist_ok=True) + + with open(f'{self.records_path}//{username}.txt', 'a') as file: + file.writelines(line + '\n' for line in connections_ext) + + + def get_user_connections_from_web(self, limit:Optional[int]=None, followers:bool=True, force_search:bool=False): + + ''' + + Searches for connections from a specific user on the web + (Connections means followers or followings depending on the chosen option) + + Args: + - limit : limit number of connections (followers/followings) to save + - followers : if True returns a list of user's followers, if False returns of user's followings + - force_search : force searching connections + + ''' + + connections_link = self.driver.find_element_by_css_selector('ul li a span' if followers \ + else 'ul li:nth-child(3) a span') + + + try: + connections_limit = connections_link.get_attribute('title') if followers else connections_link.text + + connections_limit = int(connections_limit.replace(',', '').replace('.', '').replace(' ', '')) + + except ValueError: # This might only happen on followings + exit(''' + You must choose a UserTarget which following < 10,000 users + This happens because instagram doesn't provide by source the whole number, + and it would be a pain to translate every possible letter + ''') + + # There is no need to search for connections if the records have more or no limit is specified + # or user has less connections than records (otherwiser enable option -> Force Search) + if not force_search and self.connections and (not limit or connections_limit < limit): + return + connections_link.click() - WebDriverWait(self.driver, self.timeout).until( - lambda x: x.find_element_by_css_selector('div[role=dialog] ul')) - - connections_list = self.driver.find_element_by_css_selector('div[role=dialog] ul') - connections_count = len(connections_list.find_elements_by_css_selector('li')) - - last_count = connections_count + self.driver.execute_script(''' + elem = document.querySelector('div[role=dialog] > div > div:nth-of-type(2)'); + ''') + + # Have to click once in order to load connections + self.driver.find_element_by_css_selector('div[role=dialog] li > div > div:nth-of-type(2) > div:nth-of-type(2)').click() timestamp = perf_counter() - try: - if limit != connections_limit: - raise FileNotFoundError # Already searched and not found + limit = min(limit, connections_limit) if limit else connections_limit - file = open(f'{path}//{username}_{limit}.json', 'r') - - except FileNotFoundError: + connections_set = set(self.connections) # Cloned connections as a 'set' for search O(1) + # only need to search for a few more connections + limit -= len(self.connections) # type: ignore # limit appears to be 'Optional[int]' but in reality it is 'int' because of limit being defined as 'None' or 'int' as an argument + connections_added_count = 0 + total_connections_searched = 0 - self.driver.execute_script(''' - elem = document.querySelector('div[role=dialog] > div > div:nth-of-type(2)'); - ''') - - while connections_count < limit and perf_counter() - timestamp < self.timeout: + while perf_counter() - timestamp < self.timeout: # Timer to prevent being in a loop if semone unfollows while searching - self.driver.execute_script(''' - elem.scrollTo(0,elem.scrollHeight); - ''') + connections_list = self.driver.find_elements_by_css_selector('div[role=dialog] ul li span a') + diff_connections_count = len(connections_list) - total_connections_searched - if last_count != connections_count: - timestamp = perf_counter() - - last_count = connections_count + if diff_connections_count > 0: - connections_count = len(connections_list.find_elements_by_tag_name('li')) + total_connections_searched += diff_connections_count + timestamp = perf_counter() + count = 0 - connections = [] - - for connection_obj in connections_list.find_elements_by_tag_name('li'): - connection = connection_obj.find_element_by_tag_name('a') + for connection in connections_list[-diff_connections_count:]: - connection_name = connection.text + connection_username = '@' + connection.text - if not connection_name: - connection_name = connection.get_attribute('href').split('/')[-2] + if connection_username not in connections_set: - connections.append('@' + connection_name) - if (len(connections) == limit): - break + self.connections.append(connection_username) - Path(path).mkdir(parents=True, exist_ok=True) + connections_added_count += 1 + if not force_search and connections_added_count == limit: + return - with open(f'{path}//{username}_{len(connections)}.json', 'w') as file: - json.dump(connections, file) + if total_connections_searched >= connections_limit: # This might only trigger when force_search is enabled + break - else: - connections = json.load(file) - file.close() - self.close_tab() - - return connections + self.driver.execute_script(''' + elem.scrollTo(0, elem.scrollHeight); + ''') def get_user_from_post(self, url:str) -> str: + + ''' + + Find the owner of the url's post + + Args: + - url : post's link + + ''' + self.driver.get(url) - WebDriverWait(self.driver, self.timeout).until( - lambda x: x.find_element_by_css_selector('article[role=\'presentation\'] a')) - user = self.driver.find_element_by_css_selector('article[role=\'presentation\'] a') \ .get_attribute('href').split('/')[-2] return user @@ -280,6 +248,15 @@ def get_user_from_post(self, url:str) -> str: def write_comment(self, comment:str): + ''' + + Writes a comment in Instagram's comment box + + Args: + - comment : text to write + + ''' + # Click Comment's Box self.driver \ .find_element_by_css_selector('article[role=\'presentation\'] form > textarea') \ @@ -291,14 +268,19 @@ def write_comment(self, comment:str): .send_keys(comment) def override_post_requests_js(self, comment:str): + ''' + This method was created because ChromeDriver doesn't support characters outside of BMP. Executed javascript code in Chrome's Browser to be able to post those emojis and characters Explanation: It overrides HTTP POST requests method in order to change the body (in this case the comment) + + Args: + comment: text to write ''' - + self.driver.execute_script(''' XMLHttpRequest.prototype.realSend = XMLHttpRequest.prototype.send; let re = RegExp('comment_text=.*&replied_to_comment_id='); @@ -308,15 +290,22 @@ def override_post_requests_js(self, comment:str): vData = 'comment_text=''' + comment + '''&replied_to_comment_id='; } - this.realSend(vData); + this.realSend(vData); }; XMLHttpRequest.prototype.send = newSend; ''') - + def send_comment(self) -> bool: + + ''' + + Press 'Post' button in comment's box to send it + + ''' + try: - + # Click Post's Button to send Comment self.driver \ .find_element_by_css_selector('article[role=\'presentation\'] form > button') \ @@ -325,29 +314,40 @@ def send_comment(self) -> bool: except WebDriverException: sleep(60) # Couldn't comment error pop up. No specific css selector. (

was too risky because of pop up's warnings such as cookies one) - # Wait the loading icon disappear - WebDriverWait(self.driver, self.timeout).until_not( - lambda x: x.find_element_by_css_selector('article[role=\'presentation\'] form > div')) - + with self.implicitly_wait.ignore(): # remove Implicit Wait since we are going to check for possible non-existent element and we don't want any cooldown + + # Wait the loading icon disappear + WebDriverWait(self.driver, self.timeout).until_not( + EC.presence_of_element_located((By.CSS_SELECTOR, 'article[role=\'presentation\'] form > div'))) + # Text in Comment's Box return not self.driver.find_element_by_css_selector('article[role=\'presentation\'] form > textarea').text - - - def comment_post(self, url:str, expr:str, connections:List[str], get_interval:Callable[[], float]): + + + def comment_post(self, url:str, expr:str, get_interval:Callable[[], float]): + + ''' + + Generates the comments from an expression and a list of connections then writes and finally sends + + Args: + - url : post's url to comment + - expr : expression representing comments' pattern + - get_interval : generator which yields a waiting time + + ''' + + expr_parts = re.split(r'(? Iterator[str]: - for idx in range(0, (len(connections) // n) * n, n): - yield connections[idx:idx + n] + + def chunks() -> Iterator[str]: + for idx in range(0, (len(self.connections) // n) * n, n): + yield self.connections[idx:idx + n] comments = Comments(chunks(), expr_parts) @@ -357,11 +357,11 @@ def chunks() -> Iterator[str]: has_input = False while not success: - + if not has_input: try: self.write_comment(comment) - + except WebDriverException: # This would be pretty sure a char/emoji not in BMP because ChormeDriver doesn't support. self.override_post_requests_js(comment) comment = ''' @@ -371,13 +371,25 @@ def chunks() -> Iterator[str]: has_input = True - + success = self.send_comment() + + if success: + self.num_comments += 1 + sleep(get_interval()) - def close_driver(self): - self.driver.close() + def quit(self, message:str=None): + + ''' + + Close driver and quit program + + Args: + - message : quitting program's text + + ''' - def __exit__(self, exc_type, exc_value, traceback): - self.close_driver() + self.driver.quit() + exit(message) diff --git a/script.py b/script.py index b7b60d7..31d7645 100644 --- a/script.py +++ b/script.py @@ -1,13 +1,17 @@ -from configparser import ConfigParser -from modules.instagram_bot import Bot -from functools import partial -from random import triangular +import signal + from re import search +from typing import List +from modules import Bot, Tab +from random import triangular +from functools import partial +from configparser import ConfigParser + ASCII = ''' \n\n -\t\t\t .----------------. .----------------. .----------------. +\t\t\t .----------------. .----------------. .----------------. \t\t\t| .--------------. || .--------------. || .--------------. | \t\t\t| | ____ ____ | || | _ _ | || | ______ | | \t\t\t| | |_ || _| | || | | | | | | || | / ____ `. | | @@ -17,7 +21,7 @@ \t\t\t| | |____||____| | || | |_____| | || | \______.' | | \t\t\t| | | || | | || | | | \t\t\t| '--------------' || '--------------' || '--------------' | -\t\t\t '----------------' '----------------' '----------------' +\t\t\t '----------------' '----------------' '----------------' \n\n\n\t\t\t\t\t\t\t\t\tCreated by: Fytex\n\n\n ''' @@ -31,33 +35,61 @@ password = parser.get('Required', 'Password') -window = parser.getboolean('Optional', 'Window', fallback=True) user_target = parser.get('Optional', 'User Target', fallback=None) from_followers = parser.getboolean('Optional', 'Followers', fallback=True) limit = parser.getint('Optional', 'Limit', fallback=None) -timeout = parser.getint('Optional', 'Timeout', fallback=30) +specific_file = parser.get('Optional', 'Specific File', fallback=None) +force_search = parser.getboolean('Optional', 'Force Search', fallback=False) save_only = parser.getboolean('Optional', 'Save Only', fallback=False) low = parser.getint('Interval', 'Min', fallback=60) high = parser.getint('Interval', 'Max', fallback=120) -mode = parser.getint('Interval', 'Weight', fallback=None) # None goes for midpoint +weight = parser.getint('Interval', 'Weight', fallback=None) # None goes for midpoint + + +window = parser.getboolean('Browser', 'Window', fallback=True) +default_lang = parser.getboolean('Browser', 'Default Lang', fallback=False) +binary_location = parser.get('Browser', 'Location', fallback=None) +timeout = parser.getint('Browser', 'Timeout', fallback=30) + +if not post_link: + if not save_only: + exit('Post Link must be provided or enable Save Only') + + if save_only and not user_target: + exit('Must specify Post Link or User Target') +if specific_file: -default_lang = parser.getboolean('Chrome', 'Default Lang', fallback=False) -binary_location = parser.get('Chrome', 'Location', fallback=None) + if save_only: + exit('Either choose a Specifc File or Save Only') + if force_search: + exit('Either choose a Speciic File or Force Search') -if not post_link and not save_only: - exit('Post Link must be provided') +if limit: + if force_search: + exit('Force Search only works if limit is disabled. Otherwise it will always force search to the limit') -if save_only and not post_link and not user_target: - exit('Must specify Post Link or User Target') + + does_mention = bool(search(r'(? 0') + +if not save_only and not low <= weight <= high: + exit('Weight must be a number between Min and Max') print(ASCII) -bot = Bot(window, timeout, binary_location, default_lang) +CONNECTIONS_TYPE = "followers" if from_followers else "followings" + +records_path = 'records//' + CONNECTIONS_TYPE + +bot = Bot(window, binary_location, default_lang, timeout=timeout, records_path=records_path) + print('Logging in...') @@ -65,29 +97,88 @@ print('Logged in successfully!') -connections = [] -if limit and (save_only or search(r'(?