diff --git a/insomniac/__init__.py b/insomniac/__init__.py index a63f9f2..cf78b38 100644 --- a/insomniac/__init__.py +++ b/insomniac/__init__.py @@ -5,7 +5,7 @@ from insomniac import network from insomniac.activation import activation_controller from insomniac.network import HTTP_OK -from insomniac.params import parse_arguments +from insomniac.params import parse_arguments, load_param from insomniac.utils import * @@ -17,7 +17,7 @@ def run(activation_code="", starter_conf_file_path=None): print_timeless(COLOR_OKGREEN + __version__.__logo__ + COLOR_ENDC) print_version() - activation_code_from_args = get_arg_value(ACTIVATION_CODE_ARG_NAME) + activation_code_from_args = get_arg_value(ACTIVATION_CODE_ARG_NAME, starter_conf_file_path) if activation_code_from_args is not None: activation_code = activation_code_from_args @@ -60,7 +60,16 @@ def _get_latest_version(package): return latest_version -def get_arg_value(arg_name): +def get_arg_value(arg_name, conf_path): + arg_from_cli = get_arg_value_from_cli(arg_name) + + if arg_from_cli is not None: + return arg_from_cli + + return get_arg_value_from_config_file(arg_name, conf_path) + + +def get_arg_value_from_cli(arg_name): parser = ArgumentParser(add_help=False) parser.add_argument(f'--{arg_name.replace("_", "-")}') try: @@ -70,6 +79,13 @@ def get_arg_value(arg_name): return getattr(args, arg_name) +def get_arg_value_from_config_file(arg_name, conf_file_path): + if conf_file_path is None: + return None + + return load_param(conf_file_path, arg_name) + + class ArgumentParser(argparse.ArgumentParser): def error(self, message): diff --git a/insomniac/__version__.py b/insomniac/__version__.py index e821c9b..9d5a082 100644 --- a/insomniac/__version__.py +++ b/insomniac/__version__.py @@ -13,7 +13,7 @@ __title__ = 'insomniac' __description__ = 'Simple Instagram bot for automated Instagram interaction using Android.' __url__ = 'https://github.com/alexal1/Insomniac/' -__version__ = '3.7.25' +__version__ = '3.7.26' __debug_mode__ = False __author__ = 'Insomniac Team' __author_email__ = 'info@insomniac-bot.com' diff --git a/insomniac/action_runners/__init__.py b/insomniac/action_runners/__init__.py index c4d66e8..5d49362 100644 --- a/insomniac/action_runners/__init__.py +++ b/insomniac/action_runners/__init__.py @@ -1,4 +1,4 @@ -from insomniac.action_runners.core import ActionsRunner, CoreActionsRunner, ActionStatus, ActionState +from insomniac.action_runners.core import ActionsRunner, InsomniacActionsRunner, CoreActionsRunner, ActionStatus, ActionState from insomniac.action_runners.interact import InteractBySourceActionRunner, InteractByTargetsActionRunner from insomniac.action_runners.unfollow import UnfollowActionRunner diff --git a/insomniac/action_runners/core.py b/insomniac/action_runners/core.py index 1b4eaa5..5373fab 100644 --- a/insomniac/action_runners/core.py +++ b/insomniac/action_runners/core.py @@ -29,7 +29,7 @@ def get_limit(self): return self.limit_state -class ActionsRunner(object): +class ActionsRunner(ABC): """An interface for actions-runner object""" ACTION_ID = "OVERRIDE" @@ -46,9 +46,13 @@ def set_params(self, args): def reset_params(self): raise NotImplementedError() + +class InsomniacActionsRunner(ActionsRunner, ABC): + """An interface for extra-actions-runner object""" + def run(self, device_wrapper, storage, session_state, on_action, is_limit_reached, is_passed_filters=None): raise NotImplementedError() -class CoreActionsRunner(ActionsRunner, ABC): - """An interface for extra-actions-runner object""" +class CoreActionsRunner(InsomniacActionsRunner, ABC): + """An interface for core-actions-runner object""" diff --git a/insomniac/action_runners/interact/action_handle_target.py b/insomniac/action_runners/interact/action_handle_target.py index 3591f70..4132f93 100644 --- a/insomniac/action_runners/interact/action_handle_target.py +++ b/insomniac/action_runners/interact/action_handle_target.py @@ -6,7 +6,7 @@ CommentAction, TargetType, FilterAction from insomniac.limits import process_limits from insomniac.report import print_short_report, print_interaction_types -from insomniac.session_state import SessionState +from insomniac.session_state import InsomniacSessionState from insomniac.sleeper import sleeper from insomniac.storage import FollowingStatus from insomniac.utils import * @@ -134,7 +134,7 @@ def interact_with_username_target(target_name, target_name_view): is_liked, is_followed, is_watch, is_commented = interaction(username=target_name, interaction_strategy=interaction_strategy) if is_liked or is_followed or is_watch or is_commented: on_action(InteractAction(source_name=target, source_type=source_type, user=target_name, succeed=True)) - print_short_report(SessionState.SOURCE_NAME_TARGETS, session_state) + print_short_report(InsomniacSessionState.SOURCE_NAME_TARGETS, session_state) else: on_action(InteractAction(source_name=target, source_type=source_type, user=target_name, succeed=False)) @@ -181,7 +181,7 @@ def interact_with_post_id_target(target_post, target_username, target_name_view) print(COLOR_OKGREEN + f"@{target_username} - {target_post} - photo been liked." + COLOR_ENDC) on_action(LikeAction(source_name=target_username, source_type=source_type, user=target_username)) on_action(InteractAction(source_name=target_username, source_type=source_type, user=target_username, succeed=True)) - print_short_report(SessionState.SOURCE_NAME_TARGETS, session_state) + print_short_report(InsomniacSessionState.SOURCE_NAME_TARGETS, session_state) if is_like_limit_reached: # If one of the limits reached for source-limit, move to next source diff --git a/insomniac/apk/ADBKeyboard.apk b/insomniac/assets/ADBKeyboard.apk similarity index 100% rename from insomniac/apk/ADBKeyboard.apk rename to insomniac/assets/ADBKeyboard.apk diff --git a/insomniac/assets/aapt b/insomniac/assets/aapt new file mode 100644 index 0000000..f04c43e Binary files /dev/null and b/insomniac/assets/aapt differ diff --git a/insomniac/db_models.py b/insomniac/db_models.py index 73c74e0..11c4424 100644 --- a/insomniac/db_models.py +++ b/insomniac/db_models.py @@ -9,7 +9,7 @@ from insomniac.globals import executable_name DATABASE_NAME = f'{executable_name}.db' -DATABASE_VERSION = 3 +DATABASE_VERSION = 4 db = SqliteDatabase(DATABASE_NAME, autoconnect=False) @@ -72,16 +72,25 @@ def add_session(self, app_id, app_version, args, profile_status, followers_count def update_profile_info(self, profile_status, followers_count, following_count): """ Create a new InstagramProfileInfo record - - Can't see any usage for that right now, maybe we will use it later just in order to update profile info without - running an insomniac-session. """ with db.connection_context(): InstagramProfileInfo.create(profile=self, - status=profile_status, + status=profile_status.value, followers=followers_count, following=following_count) + def get_latsest_profile_info(self) -> Optional['InstagramProfileInfo']: + with db.connection_context(): + query = InstagramProfileInfo.select() \ + .where(InstagramProfileInfo.profile == self) \ + .group_by(InstagramProfileInfo.profile) \ + .having(InstagramProfileInfo.timestamp == fn.MAX(InstagramProfileInfo.timestamp)) + + for obj in query: + return obj + + return None + def log_get_profile_action(self, session_id, phase, username, task_id='', execution_id='', timestamp=None): """ Create InsomniacAction record @@ -281,6 +290,20 @@ def log_change_profile_info_action(self, session_id, phase, profile_pic_url, nam name=name, description=description) + def log_management_action(self, session_id, management_action_type, management_action_params, phase, task_id='', execution_id='', timestamp=None): + with db.connection_context(): + session = SessionInfo.get(SessionInfo.id == session_id) + action = InsomniacAction.create(actor_profile=self, + type=management_action_type.__name__, + task_id=task_id, + execution_id=execution_id, + session=session, + phase=phase, + timestamp=(timestamp if timestamp is not None else datetime.now())) + + params = {**management_action_params, **{'action': action}} + management_action_type.create(**params) + def publish_scrapped_account(self, username, scrape_for_account_list): """ Use this function when you are a scrapper and you have found a profile according to filters. @@ -492,8 +515,8 @@ class InsomniacAction(InsomniacModel): actor_profile = ForeignKeyField(InstagramProfile, backref='actions') type = TextField() timestamp = TimestampField(default=datetime.now) - task_id = UUIDField() - execution_id = UUIDField() + task_id = TextField() + execution_id = TextField() session = ForeignKeyField(SessionInfo, backref='session_actions') phase = TextField(default='task') @@ -616,6 +639,51 @@ class Meta: db_table = 'follow_status' +class CloneCreationAction(InsomniacModel): + action = ForeignKeyField(InsomniacAction, backref='clone_creation_action_info') + username = TextField() + device = TextField() + + class Meta: + db_table = 'clone_creation_actions' + + +class CloneInstallationAction(InsomniacModel): + action = ForeignKeyField(InsomniacAction, backref='clone_installation_action_info') + username = TextField() + device = TextField() + + class Meta: + db_table = 'clone_installation_actions' + + +class CloneRemovalAction(InsomniacModel): + action = ForeignKeyField(InsomniacAction, backref='clone_removal_action_info') + username = TextField() + device = TextField() + + class Meta: + db_table = 'clone_removal_actions' + + +class AppDataCleanupAction(InsomniacModel): + action = ForeignKeyField(InsomniacAction, backref='app_data_cleanup_action_info') + username = TextField() + device = TextField() + + class Meta: + db_table = 'app_data_cleanup_actions' + + +class ProfileRegistrationAction(InsomniacModel): + action = ForeignKeyField(InsomniacAction, backref='profile_registration_action_info') + username = TextField() + device = TextField() + + class Meta: + db_table = 'profile_registration_actions' + + class SchemaVersion(InsomniacModel): version = SmallIntegerField(default=DATABASE_VERSION) updated_at = TimestampField(default=datetime.now) @@ -642,7 +710,12 @@ class Meta: FilterAction, ChangeProfileInfoAction, ScrappedProfile, - FollowStatus + FollowStatus, + CloneCreationAction, + CloneInstallationAction, + CloneRemovalAction, + AppDataCleanupAction, + ProfileRegistrationAction ] @@ -872,6 +945,18 @@ def _migrate_db_from_version_2_to_3(migrator): ) +def _migrate_db_from_version_3_to_4(migrator): + """ + Changes added on DB version 4: + * Added tables: CloneCreationAction, CloneInstallationAction, + CloneRemovalAction, AppDataCleanupAction, ProfileRegistrationAction + """ + new_tables = [CloneCreationAction, CloneInstallationAction, CloneRemovalAction, + AppDataCleanupAction, ProfileRegistrationAction] + + db.create_tables(new_tables) + + def _migrate(curr_version, migrator): print(f"[Database] Going to run database migration from version {curr_version} to {curr_version+1}") migration_method = database_migrations[f"{curr_version}->{curr_version + 1}"] @@ -884,5 +969,10 @@ def _migrate(curr_version, migrator): database_migrations = { "1->2": _migrate_db_from_version_1_to_2, - "2->3": _migrate_db_from_version_2_to_3 + "2->3": _migrate_db_from_version_2_to_3, + "3->4": _migrate_db_from_version_3_to_4 } + + +class QueryIsNotAllowedException(Exception): + pass diff --git a/insomniac/extra_features/action_manage_clones.py b/insomniac/extra_features/action_manage_clones.py new file mode 100644 index 0000000..e6cace8 --- /dev/null +++ b/insomniac/extra_features/action_manage_clones.py @@ -0,0 +1,3 @@ +from insomniac import activation_controller + +exec(activation_controller.get_extra_feature('action_manage_clones')) diff --git a/insomniac/extra_features/management_actions_types.py b/insomniac/extra_features/management_actions_types.py new file mode 100644 index 0000000..592e814 --- /dev/null +++ b/insomniac/extra_features/management_actions_types.py @@ -0,0 +1,3 @@ +from insomniac import activation_controller + +exec(activation_controller.get_extra_feature('management_actions_types')) diff --git a/insomniac/extra_features/report.py b/insomniac/extra_features/report.py new file mode 100644 index 0000000..be49393 --- /dev/null +++ b/insomniac/extra_features/report.py @@ -0,0 +1,3 @@ +from insomniac import activation_controller + +exec(activation_controller.get_extra_feature('report')) diff --git a/insomniac/extra_features/session_state.py b/insomniac/extra_features/session_state.py new file mode 100644 index 0000000..e313bf1 --- /dev/null +++ b/insomniac/extra_features/session_state.py @@ -0,0 +1,3 @@ +from insomniac import activation_controller + +exec(activation_controller.get_extra_feature('session_state')) diff --git a/insomniac/extra_features/storage.py b/insomniac/extra_features/storage.py new file mode 100644 index 0000000..36a6013 --- /dev/null +++ b/insomniac/extra_features/storage.py @@ -0,0 +1,3 @@ +from insomniac import activation_controller + +exec(activation_controller.get_extra_feature('storage')) diff --git a/insomniac/globals.py b/insomniac/globals.py index 3e343ac..e81f3be 100644 --- a/insomniac/globals.py +++ b/insomniac/globals.py @@ -6,5 +6,13 @@ do_location_permission_dialog_checks = True # no need in these checks if location permission is denied beforehand +def callback(profile_name): + pass + + +hardban_detected_callback = callback +softban_detected_callback = callback + + def is_insomniac(): return execution_id == '' diff --git a/insomniac/hardban_indicator.py b/insomniac/hardban_indicator.py new file mode 100644 index 0000000..449fdad --- /dev/null +++ b/insomniac/hardban_indicator.py @@ -0,0 +1,37 @@ +from insomniac.utils import * + + +class HardBanError(Exception): + pass + + +class HardBanIndicator: + + WEBVIEW_ACTIVITY_NAME = 'com.instagram.simplewebview.SimpleWebViewActivity' + + def detect_webview(self, device): + """ + While "hard banned" Instagram shows you a webview with CAPTCHA and request to confirm your account. So what we + need is to simply detect that topmost activity is a webview. + """ + device_id = device.device_id + app_id = device.app_id + resumed_activity_output = execute_command("adb" + ("" if device_id is None else " -s " + device_id) + + f" shell dumpsys activity | grep 'mResumedActivity'") + + max_attempts = 3 + attempt = 1 + while attempt <= max_attempts: + sleep(1) + full_webview_activity_name = f"{app_id}/{self.WEBVIEW_ACTIVITY_NAME}" + if resumed_activity_output is not None and full_webview_activity_name in resumed_activity_output: + print(COLOR_FAIL + "WebView is shown. Counting that as a hard-ban indicator!" + COLOR_ENDC) + self.indicate_ban() + return + attempt += 1 + + def indicate_ban(self): + raise HardBanError("Hard ban indicated!") + + +hardban_indicator = HardBanIndicator() diff --git a/insomniac/migration.py b/insomniac/migration.py index e1949d4..fe7228c 100644 --- a/insomniac/migration.py +++ b/insomniac/migration.py @@ -1,7 +1,7 @@ import json from insomniac.db_models import DATABASE_NAME, is_ig_profile_exists -from insomniac.session_state import SessionState +from insomniac.session_state import InsomniacSessionState from insomniac.sessions import FILENAME_SESSIONS, Sessions from insomniac.storage import * from insomniac.utils import * @@ -115,7 +115,7 @@ def migrate_from_json_to_sql(my_username): with open(sessions_path, encoding="utf-8") as json_file: sessions = json.load(json_file) for session in sessions: - session_state = SessionState() + session_state = InsomniacSessionState() session_state.id = session["id"] session_state.args = str(session["args"]) session_state.app_version = session.get("app_version", "") diff --git a/insomniac/network.py b/insomniac/network.py index b7f2ea3..670858b 100644 --- a/insomniac/network.py +++ b/insomniac/network.py @@ -1,6 +1,8 @@ import socket import ssl import urllib.request +import urllib.parse +import json from urllib.error import HTTPError, URLError import insomniac.__version__ as __version__ @@ -22,6 +24,29 @@ ] +def post(url, data, user_agent=INSOMNIAC_USER_AGENT): + """ + Perform HTTP POST request. + + :param url: URL to request for + :param data: data to send as the request body + :param user_agent: optional custom user-agent + :return: tuple of: response code, body (if response has one), and fail reason which is None if code is 200 + """ + + # parsed_data = urllib.parse.urlencode(data).encode() + json_data = json.dumps(data) + json_data_as_bytes = json_data.encode('utf-8') + + headers = { + 'User-Agent': user_agent, + 'Content-Type': 'application/json; charset=utf-8', + 'Content-Length': len(json_data_as_bytes) + } + + return _request(url=url, data=json_data_as_bytes, headers=headers) + + def get(url, user_agent=INSOMNIAC_USER_AGENT): """ Perform HTTP GET request. @@ -33,7 +58,20 @@ def get(url, user_agent=INSOMNIAC_USER_AGENT): headers = { 'User-Agent': user_agent } - request = urllib.request.Request(url, headers=headers) + + return _request(url=url, data=None, headers=headers) + + +def _request(url, data, headers): + """ + Perform HTTP GET request. + + :param url: URL to request for + :param user_agent: optional custom user-agent + :return: tuple of: response code, body (if response has one), and fail reason which is None if code is 200 + """ + + request = urllib.request.Request(url=url, data=data, headers=headers) body = None attempt = 0 while True: diff --git a/insomniac/params.py b/insomniac/params.py index 37f03ca..b477d01 100644 --- a/insomniac/params.py +++ b/insomniac/params.py @@ -61,10 +61,18 @@ def parse_arguments(all_args_dict, starter_conf_file_path): return True, args -def refresh_args_by_conf_file(args, conf_file_name=None): +def refresh_args_by_conf_file(args, conf_file_name=None) -> bool: + """ + Set new args taken from the config file. + + :return: true if setting of new args succeeded or args remained the same, false in case of some error + """ config_file = conf_file_name if config_file is None: config_file = args.config_file + if config_file is None: + # No config file neither in parameters nor in args, so just leave args the same + return True params = _load_params(config_file) if params is None: @@ -108,12 +116,24 @@ def load_app_id(config_file): return resolve_app_id(app_id, device_id, app_name) +def load_param(config_file, params_name): + params = _load_params(config_file) + if params is None: + return None + + for param in params: + if param.get(CONFIG_PARAMETER_NAME) == params_name and param.get(CONFIG_PARAMETER_ENABLED): + return param.get(CONFIG_PARAMETER_VALUE) + + return None + + def resolve_app_id(app_id, device_id, app_name): if app_name is not None: from insomniac.extra_features.utils import get_package_by_name app_id_by_name = get_package_by_name(device_id, app_name) if app_id_by_name is not None: - print(f"Found app id by app name: {app_id_by_name}") + print(f"Found app id by app name {app_name}: {app_id_by_name}") return app_id_by_name else: print(COLOR_FAIL + f"You provided app name \"{app_name}\" but there's no app with such name" + COLOR_ENDC) diff --git a/insomniac/session.py b/insomniac/session.py index 5533e93..23df03d 100644 --- a/insomniac/session.py +++ b/insomniac/session.py @@ -8,16 +8,17 @@ from insomniac.action_get_my_profile_info import get_my_profile_info from insomniac.action_runners.actions_runners_manager import CoreActionRunnersManager from insomniac.device import DeviceWrapper +from insomniac.hardban_indicator import HardBanError, hardban_indicator from insomniac.limits import LimitsManager from insomniac.migration import migrate_from_json_to_sql, migrate_from_sql_to_peewee from insomniac.navigation import close_instagram_and_system_dialogs from insomniac.params import parse_arguments, refresh_args_by_conf_file, load_app_id from insomniac.report import print_full_report -from insomniac.session_state import SessionState +from insomniac.session_state import InsomniacSessionState from insomniac.sessions import Sessions from insomniac.sleeper import sleeper from insomniac.softban_indicator import ActionBlockedError -from insomniac.storage import STORAGE_ARGS, Storage, DatabaseMigrationFailedException +from insomniac.storage import STORAGE_ARGS, InsomniacStorage, DatabaseMigrationFailedException from insomniac.utils import * from insomniac.views import UserSwitchFailedException @@ -30,6 +31,10 @@ def get_insomniac_session(starter_conf_file_path): class Session(ABC): SESSION_ARGS = { + "device": { + "help": 'device identifier. Should be used only when multiple devices are connected at once', + "metavar": '2443de990e017ece' + }, "repeat": { "help": 'repeat the same session again after N minutes after completion, disabled by default. ' 'It can be a number of minutes (e.g. 180) or a range (e.g. 120-180)', @@ -39,11 +44,6 @@ class Session(ABC): 'help': 'disable "typing" feature (typing symbols one-by-one as a human)', 'action': 'store_true' }, - "old": { - 'help': 'add this flag to use an old version of uiautomator. Use it only if you experience ' - 'problems with the default version', - 'action': 'store_true' - }, "debug": { 'help': 'add this flag to run insomniac in debug mode (more verbose logs)', 'action': 'store_true' @@ -62,12 +62,17 @@ class Session(ABC): 'metavar': '1-4', 'type': int, 'choices': range(1, 5) + }, + "no_speed_check": { + 'help': 'skip internet speed check at start', + 'action': 'store_true' } } + device = None repeat = None - old = None next_config_file = None + print_full_report_fn = print_full_report def __init__(self, starter_conf_file_path=None): self.starter_conf_file_path = starter_conf_file_path @@ -115,7 +120,7 @@ def repeat_session(self, args): sleep(60 * self.repeat) return refresh_args_by_conf_file(args, self.next_config_file) except KeyboardInterrupt: - print_full_report(self.sessions) + self.print_full_report_fn(self.sessions) sys.exit(0) @staticmethod @@ -127,9 +132,20 @@ def update_session_speed(args): print("(use " + COLOR_BOLD + "--no-speed-check" + COLOR_ENDC + " to skip this check)") sleeper.update_random_sleep_range() + @staticmethod + def should_close_app_after_session(args, device_wrapper): + if args.repeat is None: + return True + if float(args.repeat) > 0: + return True + return not Session.is_next_app_id_same(args, device_wrapper) + @staticmethod def is_next_app_id_same(args, device_wrapper): - return args.next_config_file is not None and load_app_id(args.next_config_file) == device_wrapper.app_id + if args.next_config_file is None: + # No config file => we'll use same app_id + return True + return load_app_id(args.next_config_file) == device_wrapper.app_id def run(self): raise NotImplementedError @@ -137,20 +153,12 @@ def run(self): class InsomniacSession(Session): INSOMNIAC_SESSION_ARGS = { - "device": { - "help": 'device identifier. Should be used only when multiple devices are connected at once', - "metavar": '2443de990e017ece' - }, "wait_for_device": { 'help': 'keep waiting for ADB-device to be ready for connection (if no device-id is provided using ' '--device flag, will wait for any available device)', 'action': 'store_true', "default": False }, - "no_speed_check": { - 'help': 'skip internet speed check at start', - 'action': 'store_true' - }, "app_id": { "help": 'apk package identifier. Should be used only if you are using cloned-app. ' 'Using \'com.instagram.android\' by default', @@ -160,6 +168,11 @@ class InsomniacSession(Session): "app_name": { "default": None }, + "old": { + 'help': 'add this flag to use an old version of uiautomator. Use it only if you experience ' + 'problems with the default version', + 'action': 'store_true' + }, "dont_indicate_softban": { "help": "by default Insomniac tries to indicate if there is a softban on your acoount. Set this flag in " "order to ignore those softban indicators", @@ -182,7 +195,6 @@ class InsomniacSession(Session): } } - device = None username = None def __init__(self, starter_conf_file_path=None): @@ -233,23 +245,26 @@ def get_device_wrapper(self, args): return device_wrapper, app_version def prepare_session_state(self, args, device_wrapper, app_version, save_profile_info=True): - self.session_state = SessionState() + self.session_state = InsomniacSessionState() self.session_state.args = args.__dict__ self.session_state.app_id = args.app_id self.session_state.app_version = app_version self.sessions.append(self.session_state) - device_wrapper.get().wake_up() + device = device_wrapper.get() + device.wake_up() print_timeless(COLOR_REPORT + "\n-------- START: " + str(self.session_state.startTime) + " --------" + COLOR_ENDC) if __version__.__debug_mode__: - device_wrapper.get().start_screen_record() - open_instagram(device_wrapper.device_id, device_wrapper.app_id) + device.start_screen_record() + if open_instagram(device_wrapper.device_id, device_wrapper.app_id): + # IG was just opened, check that we are not hard banned + hardban_indicator.detect_webview(device) if save_profile_info: self.session_state.my_username, \ self.session_state.my_followers_count, \ - self.session_state.my_following_count = get_my_profile_info(device_wrapper.get(), self.username) + self.session_state.my_following_count = get_my_profile_info(device, self.username) return self.session_state @@ -298,7 +313,7 @@ def run(self): self.prepare_session_state(args, device_wrapper, app_version, save_profile_info=True) migrate_from_json_to_sql(self.session_state.my_username) migrate_from_sql_to_peewee(self.session_state.my_username) - self.storage = Storage(self.session_state.my_username, args) + self.storage = InsomniacStorage(self.session_state.my_username, args) self.session_state.set_storage_layer(self.storage) self.session_state.start_session() @@ -310,7 +325,13 @@ def run(self): except (KeyboardInterrupt, UserSwitchFailedException, DatabaseMigrationFailedException): self.end_session(device_wrapper) return - except ActionBlockedError as ex: + except (ActionBlockedError, HardBanError) as ex: + if type(ex) is ActionBlockedError: + self.storage.log_softban() + + if type(ex) is HardBanError and args.app_name is not None: + InsomniacStorage.log_hardban(args.app_name) + print_timeless("") print(COLOR_FAIL + describe_exception(ex, with_stacktrace=False) + COLOR_ENDC) save_crash(device_wrapper.get()) @@ -324,7 +345,7 @@ def run(self): print(COLOR_FAIL + describe_exception(ex) + COLOR_ENDC) save_crash(device_wrapper.get(), ex) - self.end_session(device_wrapper, with_app_closing=(not self.is_next_app_id_same(args, device_wrapper))) + self.end_session(device_wrapper, with_app_closing=self.should_close_app_after_session(args, device_wrapper)) if self.repeat is not None: if not self.repeat_session(args): break diff --git a/insomniac/session_state.py b/insomniac/session_state.py index 1d5d93e..32e8ded 100644 --- a/insomniac/session_state.py +++ b/insomniac/session_state.py @@ -1,16 +1,54 @@ +from abc import ABC from datetime import datetime from typing import Optional from insomniac.actions_types import LikeAction, InteractAction, FollowAction, GetProfileAction, ScrapeAction, \ UnfollowAction, RemoveMassFollowerAction, StoryWatchAction, CommentAction, DirectMessageAction, FilterAction -from insomniac.storage import Storage, SessionPhase +from insomniac.storage import Storage, InsomniacStorage, SessionPhase -class SessionState: - SOURCE_NAME_TARGETS = "targets" - +class SessionState(ABC): id = None args = {} + startTime = None + finishTime = None + storage: Optional[Storage] = None + is_started = False + + def __init__(self): + self.id = None + self.args = {} + self.startTime = datetime.now() + self.finishTime = None + self.storage = None + self.is_started = False + + def set_storage_layer(self, storage_instance): + self.storage = storage_instance + + def start_session(self): + self.is_started = True + self.start_session_impl() + + def start_session_impl(self): + raise NotImplementedError + + def end_session(self): + if not self.is_started: + return + + self.finishTime = datetime.now() # For metadata-in-memory only + if self.storage is not None: + self.storage.end_session(self.id) + + def is_finished(self): + return self.finishTime is not None + + +class InsomniacSessionState(SessionState): + SOURCE_NAME_TARGETS = "targets" + + storage: Optional[InsomniacStorage] = None app_id = None app_version = None my_username = None @@ -25,15 +63,10 @@ class SessionState: totalUnfollowed = 0 totalStoriesWatched = 0 removedMassFollowers = [] - startTime = None - finishTime = None - storage: Optional[Storage] = None session_phase = SessionPhase.TASK_LOGIC - is_started = False def __init__(self): - self.id = None - self.args = {} + super().__init__() self.app_id = None self.app_version = None self.my_username = None @@ -50,31 +83,14 @@ def __init__(self): self.totalUnfollowed = 0 self.totalStoriesWatched = 0 self.removedMassFollowers = [] - self.startTime = datetime.now() - self.finishTime = None - self.storage = None self.session_phase = SessionPhase.TASK_LOGIC - self.is_started = False - def set_storage_layer(self, storage_instance): - self.storage = storage_instance - - def start_session(self): - self.is_started = True - - session_id = self.storage.start_session(self.app_id, self.app_version, self.args, + def start_session_impl(self): + session_id = self.storage.start_session(self.args, self.app_id, self.app_version, self.my_followers_count, self.my_following_count) if session_id is not None: self.id = session_id - def end_session(self): - if not self.is_started: - return - - self.finishTime = datetime.now() # For metadata-in-memory only - if self.storage is not None: - self.storage.end_session(self.id) - def start_warmap(self): self.session_phase = SessionPhase.WARMUP @@ -144,6 +160,3 @@ def add_action(self, action): if type(action) == RemoveMassFollowerAction: self.removedMassFollowers.append(action.user) - - def is_finished(self): - return self.finishTime is not None diff --git a/insomniac/softban_indicator.py b/insomniac/softban_indicator.py index ed1a573..04f954d 100644 --- a/insomniac/softban_indicator.py +++ b/insomniac/softban_indicator.py @@ -53,7 +53,7 @@ def detect_empty_list(self, device): is_list_empty_from_profiles = list_view.is_list_empty() if is_list_empty_from_profiles: print(COLOR_FAIL + "List of followers seems to be empty. " - "Counting that as a soft-ban indicator!." + COLOR_ENDC) + "Counting that as a soft-ban indicator!" + COLOR_ENDC) self.indications[IndicationType.EMPTY_LISTS]["curr"] += 1 self.indicate_block() @@ -66,7 +66,7 @@ def detect_empty_profile(self, device): is_profile_empty = followers_count is None if is_profile_empty: print(COLOR_FAIL + "A profile-page seems to be empty. " - "Counting that as a soft-ban indicator!." + COLOR_ENDC) + "Counting that as a soft-ban indicator!" + COLOR_ENDC) self.indications[IndicationType.EMPTY_PROFILES]["curr"] += 1 self.indicate_block() @@ -77,7 +77,7 @@ def detect_action_blocked_dialog(self, device): is_blocked = DialogView(device).is_visible() if is_blocked: print(COLOR_FAIL + "Probably block dialog is shown. " - "Counting that as a soft-ban indicator!." + COLOR_ENDC) + "Counting that as a soft-ban indicator!" + COLOR_ENDC) self.indications[IndicationType.ACTION_BLOCKED_DIALOGS]["curr"] += 1 self.indicate_block() diff --git a/insomniac/storage.py b/insomniac/storage.py index 5cf6eaa..8638a1f 100644 --- a/insomniac/storage.py +++ b/insomniac/storage.py @@ -33,9 +33,6 @@ } -IS_USING_DATABASE = False - - ACTION_TYPES_MAPPING = { insomniac_actions_types.GetProfileAction: insomniac_db.GetProfileAction, insomniac_actions_types.LikeAction: insomniac_db.LikeAction, @@ -48,18 +45,36 @@ } -def database_api(func): - def wrap(*args, **kwargs): - if IS_USING_DATABASE: - return func(*args, **kwargs) - - return None - return wrap - - class Storage: my_username = None profile = None + + def _reset_state(self): + self.my_username = None + self.profile = None + + def __init__(self, my_username): + db_models.init() + + self._reset_state() + + if my_username is None: + print(COLOR_OKGREEN + "No username, so the script won't read/write from the database" + COLOR_ENDC) + return + + self.my_username = my_username + self.profile = get_ig_profile_by_profile_name(my_username) + + def start_session(self, args, app_id=None, app_version=None, followers_count=None, following_count=None): + session_id = self.profile.start_session(app_id, app_version, args, ProfileStatus.VALID.value, + followers_count, following_count) + return session_id + + def end_session(self, session_id): + self.profile.end_session(session_id) + + +class InsomniacStorage(Storage): scrape_for_account_list = [] recheck_follow_status_after = None whitelist = [] @@ -70,10 +85,8 @@ class Storage: url_targets_list_from_parameters = [] def _reset_state(self): - global IS_USING_DATABASE - IS_USING_DATABASE = False - self.my_username = None - self.profile = None + super()._reset_state() + self.scrape_for_account_list = [] self.recheck_follow_status_after = None self.whitelist = [] @@ -84,19 +97,8 @@ def _reset_state(self): self.url_targets_list_from_parameters = [] def __init__(self, my_username, args): - db_models.init() - - self._reset_state() + super().__init__(my_username) - if my_username is None: - print(COLOR_OKGREEN + "No username, so the script won't read/write from the database" + COLOR_ENDC) - return - - global IS_USING_DATABASE - IS_USING_DATABASE = True - - self.my_username = my_username - self.profile = get_ig_profile_by_profile_name(my_username) scrape_for_account = args.__dict__.get('scrape_for_account', []) self.scrape_for_account_list = scrape_for_account if isinstance(scrape_for_account, list) else [scrape_for_account] if args.reinteract_after is not None: @@ -157,33 +159,15 @@ def __init__(self, my_username, args): if count_from_scrapping > 0: print(f"Profiles to interact from scrapping: {count_from_scrapping}") - @database_api - def start_session(self, app_id, app_version, args, followers_count, following_count): - session_id = self.profile.start_session(app_id, app_version, args, ProfileStatus.VALID.value, - followers_count, following_count) - return session_id - - @database_api - def end_session(self, session_id): - self.profile.end_session(session_id) - - @database_api def check_user_was_interacted(self, username): return self.profile.is_interacted(username, hours=self.reinteract_after) - @database_api - def check_user_was_interacted(self, username): - return self.profile.is_interacted(username, hours=72) - - @database_api def check_user_was_scrapped(self, username): return self.profile.is_scrapped(username, self.scrape_for_account_list) - @database_api def check_user_was_filtered(self, username): return self.profile.is_filtered(username, hours=self.refilter_after) - @database_api def get_following_status(self, username): if not self.profile.used_to_follow(username): return FollowingStatus.NONE @@ -192,7 +176,6 @@ def get_following_status(self, username): return FollowingStatus.NONE return FollowingStatus.FOLLOWED if do_i_follow else FollowingStatus.UNFOLLOWED - @database_api def is_profile_follows_me_by_cache(self, username): """ Return True if and only if "username" follows me and the last check was within @@ -202,11 +185,9 @@ def is_profile_follows_me_by_cache(self, username): return False return self.profile.is_follow_me(username, hours=self.recheck_follow_status_after) is True - @database_api def is_dm_sent_to(self, username): return self.profile.is_dm_sent_to(username) - @database_api def update_follow_status(self, username, is_follow_me=None, do_i_follow_him=None): if is_follow_me is None and do_i_follow_him is None: print(COLOR_FAIL + "Provide either is_follow_me or do_i_follow_him or both in update_follow_status()!") @@ -217,62 +198,66 @@ def update_follow_status(self, username, is_follow_me=None, do_i_follow_him=None do_i_follow_him = self.profile.do_i_follow(username) self.profile.update_follow_status(username, is_follow_me, do_i_follow_him) - @database_api def log_get_profile_action(self, session_id, phase, username): self.profile.log_get_profile_action(session_id, phase.value, username, insomniac_globals.task_id, insomniac_globals.execution_id) - @database_api def log_like_action(self, session_id, phase, username, source_type, source_name): self.profile.log_like_action(session_id, phase.value, username, source_type, source_name, insomniac_globals.task_id, insomniac_globals.execution_id) - @database_api def log_follow_action(self, session_id, phase, username, source_type, source_name): self.profile.log_follow_action(session_id, phase.value, username, source_type, source_name, insomniac_globals.task_id, insomniac_globals.execution_id) - @database_api def log_story_watch_action(self, session_id, phase, username, source_type, source_name): self.profile.log_story_watch_action(session_id, phase.value, username, source_type, source_name, insomniac_globals.task_id, insomniac_globals.execution_id) - @database_api def log_comment_action(self, session_id, phase, username, comment, source_type, source_name): self.profile.log_comment_action(session_id, phase.value, username, comment, source_type, source_name, insomniac_globals.task_id, insomniac_globals.execution_id) - @database_api def log_direct_message_action(self, session_id, phase, username, message): self.profile.log_direct_message_action(session_id, phase.value, username, message, insomniac_globals.task_id, insomniac_globals.execution_id) - @database_api def log_unfollow_action(self, session_id, phase, username): self.profile.log_unfollow_action(session_id, phase.value, username, insomniac_globals.task_id, insomniac_globals.execution_id) - @database_api def log_scrape_action(self, session_id, phase, username, source_type, source_name): self.profile.log_scrape_action(session_id, phase.value, username, source_type, source_name, insomniac_globals.task_id, insomniac_globals.execution_id) - @database_api def log_filter_action(self, session_id, phase, username): self.profile.log_filter_action(session_id, phase.value, username, insomniac_globals.task_id, insomniac_globals.execution_id) - @database_api def log_change_profile_info_action(self, session_id, phase, profile_pic_url, name, description): self.profile.log_change_profile_info_action(session_id, phase.value, profile_pic_url, name, description, insomniac_globals.task_id, insomniac_globals.execution_id) - @database_api def publish_scrapped_account(self, username): self.profile.publish_scrapped_account(username, self.scrape_for_account_list) - @database_api def get_actions_count_within_hours(self, action_type, hours): return self.profile.get_actions_count_within_hours(ACTION_TYPES_MAPPING[action_type], hours) - @database_api def get_session_time_in_seconds_within_minutes(self, minutes): return self.profile.get_session_time_in_seconds_within_minutes(minutes) - @database_api def get_sessions_count_within_hours(self, hours): return self.profile.get_session_count_within_hours(hours) + def log_softban(self): + InsomniacStorage._update_profile_status(self.profile, ProfileStatus.SOFT_BAN) + + @staticmethod + def log_hardban(username): + profile = get_ig_profile_by_profile_name(username) + InsomniacStorage._update_profile_status(profile, ProfileStatus.HARD_BAN) + + @staticmethod + def _update_profile_status(profile, status): + followers_count = 0 + following_count = 0 + latest_profile_info = profile.get_latsest_profile_info() + if latest_profile_info is not None: + followers_count = latest_profile_info.followers + following_count = latest_profile_info.following + profile.update_profile_info(status, followers_count, following_count) + def get_target(self, session_id): """ Get a target from args (users/posts) -> OR from targets file (users/posts) -> OR from scrapping (only users). @@ -404,7 +389,9 @@ class FollowingStatus(Enum): class ProfileStatus(Enum): VALID = "valid" UNKNOWN = "unknown" - # TODO: request list of possible statuses from Jey + SOFT_BAN = "soft-ban" + HARD_BAN = "hard-ban" + CANT_LOGIN = "cant-login" @unique diff --git a/insomniac/typewriter.py b/insomniac/typewriter.py index c6344b0..36fc854 100644 --- a/insomniac/typewriter.py +++ b/insomniac/typewriter.py @@ -26,7 +26,7 @@ def __init__(self, device_id): def set_adb_keyboard(self): if not self._is_adb_ime_existing(): print("Installing ADB Keyboard to enable typewriting...") - apk_path = os.path.join(os.path.dirname(os.path.abspath(insomniac.__file__)), "apk", ADB_KEYBOARD_APK) + apk_path = os.path.join(os.path.dirname(os.path.abspath(insomniac.__file__)), "assets", ADB_KEYBOARD_APK) os.popen("adb" + ("" if self.device_id is None else " -s " + self.device_id) + f" install {apk_path}").close() self.is_adb_keyboard_set = self._set_adb_ime() diff --git a/insomniac/utils.py b/insomniac/utils.py index f73f405..d834d11 100644 --- a/insomniac/utils.py +++ b/insomniac/utils.py @@ -11,6 +11,7 @@ from random import randint from subprocess import PIPE from time import sleep +from typing import Optional import colorama from colorama import Fore, Style, AnsiToWin32 @@ -64,10 +65,10 @@ def get_connected_devices_adb_ids(): if devices_count == 0: return [] - devices = [] + devices = set() for line in output.split('\n'): if '\tdevice' in line: - devices.append(line.split('\t')[0]) + devices.add(line.split('\t')[0]) return devices @@ -120,15 +121,22 @@ def check_adb_connection(device_id, wait_for_device): return is_ok -def open_instagram(device_id, app_id): +def open_instagram(device_id, app_id) -> bool: + """ + :return: true if IG app was opened, false if it was already opened + """ print("Open Instagram app") cmd = ("adb" + ("" if device_id is None else " -s " + device_id) + f" shell am start -n {app_id}/com.instagram.mainactivity.MainActivity") cmd_res = subprocess.run(cmd, stdout=PIPE, stderr=PIPE, shell=True, encoding="utf8") err = cmd_res.stderr.strip() - if err and err != APP_REOPEN_WARNING: - print(COLOR_FAIL + err + COLOR_ENDC) + if err: + if err == APP_REOPEN_WARNING: + return False + else: + print(COLOR_FAIL + err + COLOR_ENDC) + return True def open_instagram_with_url(device_id, app_id, url): @@ -146,7 +154,7 @@ def open_instagram_with_url(device_id, app_id, url): def close_instagram(device_id, app_id): - print("Close Instagram app") + print(f"Close Instagram app {app_id}") os.popen("adb" + ("" if device_id is None else " -s " + device_id) + f" shell am force-stop {app_id}").close() # Press HOME to leave a possible state of opened system dialog(s) @@ -160,6 +168,15 @@ def clear_instagram_data(device_id, app_id): f" shell pm clear {app_id}").close() +def execute_command(cmd) -> Optional[str]: + cmd_res = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True, encoding="utf8") + err = cmd_res.stderr.strip() + if err: + print(COLOR_FAIL + err + COLOR_ENDC) + return None + return cmd_res.stdout.strip() + + def save_crash(device, ex=None): global print_log @@ -329,9 +346,10 @@ def get_random_string(length): return ''.join(random.choices(string.ascii_uppercase + string.digits, k=length)) -def describe_exception(ex, with_stacktrace=True): +def describe_exception(ex, with_stacktrace=True, context=None): + exception_context = f'({context}): ' if context is not None else '' trace = ''.join(traceback.format_exception(etype=type(ex), value=ex, tb=ex.__traceback__)) if with_stacktrace else '' - description = f"Error - {str(ex)}\n{trace}" + description = f"{exception_context}Error - {str(ex)}\n{trace}" return description