diff --git a/insomniac/__init__.py b/insomniac/__init__.py index 790510f..a63f9f2 100644 --- a/insomniac/__init__.py +++ b/insomniac/__init__.py @@ -9,31 +9,31 @@ from insomniac.utils import * +ACTIVATION_CODE_ARG_NAME = 'activation_code' + + def run(activation_code="", starter_conf_file_path=None): if not __version__.__debug_mode__: print_timeless(COLOR_OKGREEN + __version__.__logo__ + COLOR_ENDC) print_version() - activation_code_from_args = _get_activation_code_from_args() + activation_code_from_args = get_arg_value(ACTIVATION_CODE_ARG_NAME) if activation_code_from_args is not None: activation_code = activation_code_from_args activation_controller.validate(activation_code) if not activation_controller.is_activated: - from insomniac.session import InsomniacSession + from insomniac.session import get_insomniac_session print_timeless("Using insomniac session-manager without extra-features") - insomniac_session = InsomniacSession(starter_conf_file_path) + insomniac_session = get_insomniac_session(starter_conf_file_path) else: - from insomniac.extra_features.session import ExtendedInsomniacSession - insomniac_session = ExtendedInsomniacSession(starter_conf_file_path) + from insomniac.extra_features.session import get_insomniac_extended_features_session + insomniac_session = get_insomniac_extended_features_session(starter_conf_file_path) insomniac_session.run() def is_newer_version_available(): - def versiontuple(v): - return tuple(map(int, (v.split(".")))) - current_version = __version__.__version__ latest_version = _get_latest_version('insomniac') if latest_version is not None and versiontuple(latest_version) > versiontuple(current_version): @@ -60,14 +60,14 @@ def _get_latest_version(package): return latest_version -def _get_activation_code_from_args(): +def get_arg_value(arg_name): parser = ArgumentParser(add_help=False) - parser.add_argument('--activation-code') + parser.add_argument(f'--{arg_name.replace("_", "-")}') try: args, _ = parser.parse_known_args() except (argparse.ArgumentError, TypeError): return None - return args.activation_code + return getattr(args, arg_name) class ArgumentParser(argparse.ArgumentParser): diff --git a/insomniac/__version__.py b/insomniac/__version__.py index 03fe516..e821c9b 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.21' +__version__ = '3.7.25' __debug_mode__ = False __author__ = 'Insomniac Team' __author_email__ = 'info@insomniac-bot.com' diff --git a/insomniac/action_get_my_profile_info.py b/insomniac/action_get_my_profile_info.py index f5eb691..d4de8e5 100644 --- a/insomniac/action_get_my_profile_info.py +++ b/insomniac/action_get_my_profile_info.py @@ -24,7 +24,7 @@ def get_my_profile_info(device, username): except UserSwitchFailedException as e: raise e except Exception as e: - print(COLOR_FAIL + f"Exception: {e}" + COLOR_ENDC) + print(COLOR_FAIL + describe_exception(e) + COLOR_ENDC) save_crash(device, e) switch_to_english(device) diff --git a/insomniac/action_runners/actions_runners_manager.py b/insomniac/action_runners/actions_runners_manager.py index 3b79cf5..a8ef512 100644 --- a/insomniac/action_runners/actions_runners_manager.py +++ b/insomniac/action_runners/actions_runners_manager.py @@ -1,14 +1,12 @@ +from abc import ABC + from insomniac.action_runners import * from insomniac.utils import * -class ActionRunnersManager(object): - action_runners = {} - +class ActionRunnersManager(ABC): def __init__(self): - for clazz in get_core_action_runners_classes(): - instance = clazz() - self.action_runners[instance.ACTION_ID] = instance + self.action_runners = {} def get_actions_args(self): actions_args = {} @@ -39,3 +37,12 @@ def select_action_runner(self, args): COLOR_ENDC) return selected_action_runners[0] + + +class CoreActionRunnersManager(ActionRunnersManager): + def __init__(self): + super().__init__() + + for clazz in get_core_action_runners_classes(): + instance = clazz() + self.action_runners[instance.ACTION_ID] = instance diff --git a/insomniac/action_runners/interact/__init__.py b/insomniac/action_runners/interact/__init__.py index 17334d2..47cc73f 100644 --- a/insomniac/action_runners/interact/__init__.py +++ b/insomniac/action_runners/interact/__init__.py @@ -1,7 +1,9 @@ from insomniac.action_runners import * from insomniac.actions_types import TargetType +from insomniac.navigation import navigate from insomniac.safely_runner import run_safely from insomniac.utils import * +from insomniac.views import TabBarTabs class InteractBySourceActionRunner(CoreActionsRunner): @@ -80,8 +82,6 @@ def reset_params(self): self.comments_list = [] def set_params(self, args): - self.reset_params() - if args.likes_count is not None: self.likes_count = args.likes_count @@ -144,6 +144,7 @@ def run(self, device_wrapper, storage, session_state, on_action, is_limit_reache @run_safely(device_wrapper=device_wrapper) def job(): self.action_status.set(ActionState.RUNNING) + navigate(device_wrapper.get(), TabBarTabs.PROFILE) if source[0] == '@': source_name, instructions = extract_blogger_instructions(source) handle_blogger(device_wrapper.get(), @@ -288,8 +289,6 @@ def reset_params(self): self.comments_list = [] def set_params(self, args): - self.reset_params() - if args.likes_count is not None: self.likes_count = args.likes_count diff --git a/insomniac/action_runners/interact/action_handle_blogger.py b/insomniac/action_runners/interact/action_handle_blogger.py index 793fc6d..f6ccdd7 100644 --- a/insomniac/action_runners/interact/action_handle_blogger.py +++ b/insomniac/action_runners/interact/action_handle_blogger.py @@ -122,7 +122,6 @@ def interact_with_follower(follower_name, follower_name_view): should_continue, is_all_filters_satisfied = is_passed_filters(device, follower_name, reset=True, filters_tags=['BEFORE_PROFILE_CLICK']) if not should_continue: - on_action(FilterAction(user=follower_name)) return True if not is_all_filters_satisfied: @@ -146,7 +145,6 @@ def interact_with_follower(follower_name, follower_name_view): if not is_all_filters_satisfied: should_continue, _ = is_passed_filters(device, follower_name, reset=False) if not should_continue: - on_action(FilterAction(user=follower_name)) # Continue to next follower print("Back to profiles list") device.back() diff --git a/insomniac/action_runners/interact/action_handle_hashtag.py b/insomniac/action_runners/interact/action_handle_hashtag.py index dafba6a..e73c21b 100644 --- a/insomniac/action_runners/interact/action_handle_hashtag.py +++ b/insomniac/action_runners/interact/action_handle_hashtag.py @@ -99,7 +99,6 @@ def interact_with_profile(liker_username, liker_username_view): should_continue, is_all_filters_satisfied = is_passed_filters(device, liker_username, reset=True, filters_tags=['BEFORE_PROFILE_CLICK']) if not should_continue: - on_action(FilterAction(user=liker_username)) return True if not is_all_filters_satisfied: @@ -121,7 +120,6 @@ def interact_with_profile(liker_username, liker_username_view): if not is_all_filters_satisfied: should_continue, _ = is_passed_filters(device, liker_username, reset=False) if not should_continue: - on_action(FilterAction(user=liker_username)) # Continue to next follower print("Back to likers list") device.back() diff --git a/insomniac/action_runners/interact/action_handle_place.py b/insomniac/action_runners/interact/action_handle_place.py index 3837323..1b37c26 100644 --- a/insomniac/action_runners/interact/action_handle_place.py +++ b/insomniac/action_runners/interact/action_handle_place.py @@ -110,7 +110,6 @@ def interact_with_profile(liker_username, liker_username_view): should_continue, is_all_filters_satisfied = is_passed_filters(device, liker_username, reset=True, filters_tags=['BEFORE_PROFILE_CLICK']) if not should_continue: - on_action(FilterAction(liker_username)) return True if not is_all_filters_satisfied: @@ -132,7 +131,6 @@ def interact_with_profile(liker_username, liker_username_view): if not is_all_filters_satisfied: should_continue, _ = is_passed_filters(device, liker_username, reset=False) if not should_continue: - on_action(FilterAction(liker_username)) # Continue to next follower print("Back to likers list") device.back() diff --git a/insomniac/action_runners/interact/action_handle_target.py b/insomniac/action_runners/interact/action_handle_target.py index 2433511..3591f70 100644 --- a/insomniac/action_runners/interact/action_handle_target.py +++ b/insomniac/action_runners/interact/action_handle_target.py @@ -45,7 +45,6 @@ def pre_conditions(target_name, target_name_view): should_continue, is_all_filters_satisfied = is_passed_filters(device, target_name, reset=True, filters_tags=['BEFORE_PROFILE_CLICK']) if not should_continue: - on_action(FilterAction(user=target_name)) return False if not is_all_filters_satisfied: @@ -74,7 +73,6 @@ def interact_with_username_target(target_name, target_name_view): if not is_all_filters_satisfied: should_continue, _ = is_passed_filters(device, target_name, reset=False) if not should_continue: - on_action(FilterAction(user=target_name)) print("Moving to next target") return diff --git a/insomniac/action_runners/unfollow/__init__.py b/insomniac/action_runners/unfollow/__init__.py index c12356e..d0635d0 100644 --- a/insomniac/action_runners/unfollow/__init__.py +++ b/insomniac/action_runners/unfollow/__init__.py @@ -1,7 +1,9 @@ from insomniac.action_runners import * from insomniac.actions_impl import FollowingsSortOrder +from insomniac.navigation import navigate from insomniac.safely_runner import run_safely from insomniac.utils import * +from insomniac.views import TabBarTabs class UnfollowActionRunner(CoreActionsRunner): @@ -49,8 +51,6 @@ def reset_params(self): self.followings_sort_order = FollowingsSortOrder.EARLIEST def set_params(self, args): - self.reset_params() - if args.unfollow_followed_by_anyone is not None: self.unfollow_followed_by_anyone = True @@ -75,6 +75,7 @@ def run(self, device_wrapper, storage, session_state, on_action, is_limit_reache @run_safely(device_wrapper=device_wrapper) def job(): self.action_status.set(ActionState.RUNNING) + navigate(device_wrapper.get(), TabBarTabs.PROFILE) unfollow(device=device_wrapper.get(), on_action=on_action, storage=storage, diff --git a/insomniac/actions_impl.py b/insomniac/actions_impl.py index 32e5dea..19b8d21 100644 --- a/insomniac/actions_impl.py +++ b/insomniac/actions_impl.py @@ -807,13 +807,7 @@ def do_unfollow(device, my_username, username, storage, check_if_is_follower, us if dialog_view.is_visible(): print("Confirming unfollow...") unfollow_confirmed = dialog_view.click_unfollow() - sleeper.random_sleep() - confirmation_dialog_view = DialogView(device) - if confirmation_dialog_view.is_visible(): - print("Confirming unfollowing...") - unfollow_confirmed = confirmation_dialog_view.click_unfollow() - sleeper.random_sleep() if unfollow_confirmed: sleeper.random_sleep() else: diff --git a/insomniac/actions_types.py b/insomniac/actions_types.py index 5e47e8b..876ba8a 100644 --- a/insomniac/actions_types.py +++ b/insomniac/actions_types.py @@ -12,6 +12,7 @@ FilterAction = namedtuple('FilterAction', 'user') InteractAction = namedtuple('InteractAction', 'source_name source_type user succeed') RemoveMassFollowerAction = namedtuple('RemoveMassFollowerAction', 'user') +StartSessionAction = namedtuple('StartSessionAction', '') @unique diff --git a/insomniac/db_models.py b/insomniac/db_models.py index 64ffe47..73c74e0 100644 --- a/insomniac/db_models.py +++ b/insomniac/db_models.py @@ -1,17 +1,15 @@ import uuid from collections import defaultdict -from datetime import timedelta -from enum import Enum, unique from typing import Optional from peewee import * from playhouse.migrate import SqliteMigrator, migrate from insomniac.utils import * -from insomniac.globals import db_name +from insomniac.globals import executable_name -DATABASE_NAME = db_name -DATABASE_VERSION = 2 +DATABASE_NAME = f'{executable_name}.db' +DATABASE_VERSION = 3 db = SqliteDatabase(DATABASE_NAME, autoconnect=False) @@ -84,7 +82,7 @@ def update_profile_info(self, profile_status, followers_count, following_count): followers=followers_count, following=following_count) - def log_get_profile_action(self, session_id, username, task_id=insomniac_globals.task_id, execution_id=insomniac_globals.execution_id, timestamp=None): + def log_get_profile_action(self, session_id, phase, username, task_id='', execution_id='', timestamp=None): """ Create InsomniacAction record Create GetProfileAction record @@ -96,11 +94,12 @@ def log_get_profile_action(self, session_id, username, task_id=insomniac_globals task_id=task_id, execution_id=execution_id, session=session, + phase=phase, timestamp=(timestamp if timestamp is not None else datetime.now())) GetProfileAction.create(action=action, target_user=username) - def log_like_action(self, session_id, username, source_type, source_name, task_id=insomniac_globals.task_id, execution_id=insomniac_globals.execution_id, timestamp=None): + def log_like_action(self, session_id, phase, username, source_type, source_name, task_id='', execution_id='', timestamp=None): """ Create InsomniacAction record Create LikeAction record @@ -113,6 +112,7 @@ def log_like_action(self, session_id, username, source_type, source_name, task_i task_id=task_id, execution_id=execution_id, session=session, + phase=phase, timestamp=(timestamp if timestamp is not None else datetime.now())) LikeAction.create(action=action, target_user=username) @@ -121,7 +121,7 @@ def log_like_action(self, session_id, username, source_type, source_name, task_i type=source_type, name=source_name) - def log_follow_action(self, session_id, username, source_type, source_name, task_id=insomniac_globals.task_id, execution_id=insomniac_globals.execution_id, timestamp=None): + def log_follow_action(self, session_id, phase, username, source_type, source_name, task_id='', execution_id='', timestamp=None): """ Create InsomniacAction record Create FollowAction record @@ -134,6 +134,7 @@ def log_follow_action(self, session_id, username, source_type, source_name, task task_id=task_id, execution_id=execution_id, session=session, + phase=phase, timestamp=(timestamp if timestamp is not None else datetime.now())) FollowAction.create(action=action, target_user=username) @@ -142,7 +143,7 @@ def log_follow_action(self, session_id, username, source_type, source_name, task type=source_type, name=source_name) - def log_story_watch_action(self, session_id, username, source_type, source_name, task_id=insomniac_globals.task_id, execution_id=insomniac_globals.execution_id, timestamp=None): + def log_story_watch_action(self, session_id, phase, username, source_type, source_name, task_id='', execution_id='', timestamp=None): """ Create InsomniacAction record Create StoryWatchAction record @@ -155,6 +156,7 @@ def log_story_watch_action(self, session_id, username, source_type, source_name, task_id=task_id, execution_id=execution_id, session=session, + phase=phase, timestamp=(timestamp if timestamp is not None else datetime.now())) StoryWatchAction.create(action=action, target_user=username) @@ -163,7 +165,7 @@ def log_story_watch_action(self, session_id, username, source_type, source_name, type=source_type, name=source_name) - def log_comment_action(self, session_id, username, comment, source_type, source_name, task_id=insomniac_globals.task_id, execution_id=insomniac_globals.execution_id, timestamp=None): + def log_comment_action(self, session_id, phase, username, comment, source_type, source_name, task_id='', execution_id='', timestamp=None): """ Create InsomniacAction record Create CommentAction record @@ -176,6 +178,7 @@ def log_comment_action(self, session_id, username, comment, source_type, source_ task_id=task_id, execution_id=execution_id, session=session, + phase=phase, timestamp=(timestamp if timestamp is not None else datetime.now())) CommentAction.create(action=action, target_user=username, @@ -185,7 +188,7 @@ def log_comment_action(self, session_id, username, comment, source_type, source_ type=source_type, name=source_name) - def log_direct_message_action(self, session_id, username, message, task_id=insomniac_globals.task_id, execution_id=insomniac_globals.execution_id, timestamp=None): + def log_direct_message_action(self, session_id, phase, username, message, task_id='', execution_id='', timestamp=None): """ Create InsomniacAction record Create DirectMessageAction record @@ -197,12 +200,13 @@ def log_direct_message_action(self, session_id, username, message, task_id=insom task_id=task_id, execution_id=execution_id, session=session, + phase=phase, timestamp=(timestamp if timestamp is not None else datetime.now())) DirectMessageAction.create(action=action, target_user=username, message=message) - def log_unfollow_action(self, session_id, username, task_id=insomniac_globals.task_id, execution_id=insomniac_globals.execution_id, timestamp=None): + def log_unfollow_action(self, session_id, phase, username, task_id='', execution_id='', timestamp=None): """ Create InsomniacAction record Create UnfollowAction record @@ -214,11 +218,12 @@ def log_unfollow_action(self, session_id, username, task_id=insomniac_globals.ta task_id=task_id, execution_id=execution_id, session=session, + phase=phase, timestamp=(timestamp if timestamp is not None else datetime.now())) UnfollowAction.create(action=action, target_user=username) - def log_scrape_action(self, session_id, username, source_type, source_name, task_id=insomniac_globals.task_id, execution_id=insomniac_globals.execution_id, timestamp=None): + def log_scrape_action(self, session_id, phase, username, source_type, source_name, task_id='', execution_id='', timestamp=None): """ Create InsomniacAction record Create ScrapeAction record @@ -231,6 +236,7 @@ def log_scrape_action(self, session_id, username, source_type, source_name, task task_id=task_id, execution_id=execution_id, session=session, + phase=phase, timestamp=(timestamp if timestamp is not None else datetime.now())) ScrapeAction.create(action=action, target_user=username) @@ -239,7 +245,7 @@ def log_scrape_action(self, session_id, username, source_type, source_name, task type=(source_type if source_type is not None else None), name=source_name) - def log_filter_action(self, session_id, username, task_id=insomniac_globals.task_id, execution_id=insomniac_globals.execution_id, timestamp=None): + def log_filter_action(self, session_id, phase, username, task_id='', execution_id='', timestamp=None): """ Create InsomniacAction record Create FilterAction record @@ -251,11 +257,12 @@ def log_filter_action(self, session_id, username, task_id=insomniac_globals.task task_id=task_id, execution_id=execution_id, session=session, + phase=phase, timestamp=(timestamp if timestamp is not None else datetime.now())) FilterAction.create(action=action, target_user=username) - def log_change_profile_info_action(self, session_id, profile_pic_url, name, description, task_id=insomniac_globals.task_id, execution_id=insomniac_globals.execution_id, timestamp=None): + def log_change_profile_info_action(self, session_id, phase, profile_pic_url, name, description, task_id='', execution_id='', timestamp=None): """ Create InsomniacAction record Create ChangeProfileInfoAction record @@ -267,6 +274,7 @@ def log_change_profile_info_action(self, session_id, profile_pic_url, name, desc task_id=task_id, execution_id=execution_id, session=session, + phase=phase, timestamp=(timestamp if timestamp is not None else datetime.now())) ChangeProfileInfoAction.create(action=action, profile_pic_url=profile_pic_url, @@ -308,6 +316,16 @@ def is_follow_me(self, username, hours=None): follow_status = self._get_follow_status(username, hours) return follow_status.is_follow_me if follow_status is not None else None + def is_dm_sent_to(self, username): + with db.connection_context(): + if len(DirectMessageAction.select() + .join(InsomniacAction, join_type=JOIN.LEFT_OUTER) + .where((DirectMessageAction.target_user == username) + & (InsomniacAction.actor_profile == self)) + .limit(1)) > 0: + return True + return False + def do_i_follow(self, username, hours=None): follow_status = self._get_follow_status(username, hours) return follow_status.do_i_follow_him if follow_status is not None else None @@ -394,12 +412,21 @@ def get_actions_count_within_hours(self, action_type, hours) -> int: Returns the amount of actions by 'action_type', within the last 'hours' """ with db.connection_context(): - actions_count = len(InsomniacAction.select(InsomniacAction.id) - .where((InsomniacAction.type == action_type.__name__) & - (InsomniacAction.actor_profile == self) & - ((datetime.now().timestamp() - InsomniacAction.timestamp <= - timedelta(hours=hours).total_seconds()) if hours is not None else True))) - return actions_count + return get_actions_count_within_hours(action_type, hours, profile=self) + + def get_session_time_in_seconds_within_minutes(self, minutes) -> int: + """ + Returns the amount of active-session-seconds, within the last 'minutes' + """ + with db.connection_context(): + return get_session_time_in_seconds_within_minutes_for_profile(minutes, profile=self) + + def get_session_count_within_hours(self, hours) -> int: + """ + Returns the amount of sessions, within the last 'hours' + """ + with db.connection_context(): + return get_session_count_within_hours_for_profile(hours, profile=self) def _get_scrapped_profiles_query(self): def get_profiles_reached_by_action(action): @@ -468,6 +495,7 @@ class InsomniacAction(InsomniacModel): task_id = UUIDField() execution_id = UUIDField() session = ForeignKeyField(SessionInfo, backref='session_actions') + phase = TextField(default='task') class Meta: db_table = 'actions_log' @@ -596,12 +624,6 @@ class Meta: db_table = 'schema_version' -@unique -class ProfileStatus(Enum): - VALID = "valid" - # TODO: request list of possible statuses from Jey - - MODELS = [ SchemaVersion, InstagramProfile, @@ -659,34 +681,162 @@ def get_ig_profile_by_profile_name(profile_name) -> InstagramProfile: return profile +def get_ig_profiles_by_profiles_names(profiles_names) -> dict: + profiles_names_to_objects = {} + + with db.connection_context(): + for name in profiles_names: + profile_obj, _ = InstagramProfile.get_or_create(name=name) + profiles_names_to_objects[name] = profile_obj + return profiles_names_to_objects + + def is_ig_profile_exists(profile_name) -> bool: with db.connection_context(): is_exists = InstagramProfile.select().where(InstagramProfile.name == profile_name).exists() return is_exists -def get_ig_profiles_actions_by_task_id(actions_task_id, action_types_list) -> dict: - """Returns a dict of ig_profile_name -> list of InsomniacAction objects""" - ig_profiles_to_actions = defaultdict(list) - requested_actions_types = list(map(lambda action_type: action_type.__name__, action_types_list)) +def get_session_time_in_seconds_within_minutes_for_profile(minutes=None, profile=None) -> int: + """Returns the amount of session time in seconds per profile, on the last 'minutes' for 'profiles'""" - with db.connection_context(): - actions_by_task_id = InsomniacAction.select(InsomniacAction.id, InsomniacAction.actor_profile, - InsomniacAction.timestamp, InsomniacAction.type) \ - .where((InsomniacAction.task_id == actions_task_id) & - (InsomniacAction.type.in_(requested_actions_types))) + total_session_time_by_profile_name = get_session_time_in_seconds_within_minutes(minutes, [profile] if profile is not None else None) + + session_time_secs = sum(total_session_time_by_profile_name.values()) + + return session_time_secs + + +def get_session_time_in_seconds_within_minutes(minutes=None, profiles=None) -> dict: + """Returns the amount of session time in seconds per profile, on the last 'minutes' for 'profiles'""" + + timestamp_from, timestamp_to = None, None + if minutes is not None: + timestamp_from, timestamp_to = get_from_to_timestamps_by_minutes(minutes) + + return get_session_time_in_seconds_from_to(timestamp_from, timestamp_to, profiles) + + +def get_session_time_in_seconds_from_to(timestamp_from=None, timestamp_to=None, profiles=None) -> dict: + """Returns the amount of session time in seconds per profile, from 'timestamp_from' to 'timestamp_to' for 'profiles'""" + + # Taking every session that started during the specified time + profiles_sessions = InstagramProfile.select(InstagramProfile.name, SessionInfo.start.alias('start'), SessionInfo.end.alias('end')) \ + .join(InstagramProfileInfo, join_type=JOIN.LEFT_OUTER) \ + .where(InstagramProfileInfo.profile.in_(profiles) if profiles else True) \ + .join(SessionInfo, join_type=JOIN.LEFT_OUTER) \ + .where((SessionInfo.start >= timestamp_from if timestamp_from else True) & + (SessionInfo.start <= timestamp_to if timestamp_to else True)) + + total_session_time_by_profile_name = defaultdict(int) + + for session in profiles_sessions.dicts(): + if not session['start'] or not session['end']: + # Not counting sessions that stopped without a end_session function call + continue + + session_time = session['end'] - session['start'] + + total_session_time_by_profile_name[session['name']] += session_time.total_seconds() + + return total_session_time_by_profile_name + + +def get_session_count_within_hours_for_profile(hours=None, profile=None) -> int: + """Returns the amount of session time in seconds per profile, on the last 'minutes' for 'profiles'""" - for action in actions_by_task_id: - ig_profiles_to_actions[action.actor_profile_id].append(action) + session_count_by_profile_name = get_session_count_within_hours(hours, [profile] if profile is not None else None) - ig_profiles_names_in_task = InstagramProfile.select(InstagramProfile.id, InstagramProfile.name) \ - .where(InstagramProfile.id.in_(list(ig_profiles_to_actions.keys()))) \ - .distinct() + session_count = sum(session_count_by_profile_name.values()) - for profile in ig_profiles_names_in_task: - ig_profiles_to_actions[profile.name] = ig_profiles_to_actions.pop(profile.id) + return session_count - return ig_profiles_to_actions + +def get_session_count_within_hours(hours=None, profiles=None) -> dict: + """ + Returns the amount of session, within the last 'hours' for 'profiles' + """ + timestamp_from, timestamp_to = None, None + if hours is not None: + timestamp_from, timestamp_to = get_from_to_timestamps_by_hours(hours) + + return get_session_count_from_to(timestamp_from, timestamp_to, profiles) + + +def get_session_count_from_to(timestamp_from=None, timestamp_to=None, profiles=None) -> dict: + """ + Returns the amount of session, from 'timestamp_from' to 'timestamp_to' for 'profiles' + """ + # Taking every session that started during the specified time + profiles_sessions = InstagramProfile.select(InstagramProfile.name) \ + .join(InstagramProfileInfo, join_type=JOIN.LEFT_OUTER) \ + .where(InstagramProfileInfo.profile.in_(profiles) if profiles else True) \ + .join(SessionInfo, join_type=JOIN.LEFT_OUTER) \ + .where((SessionInfo.start >= timestamp_from if timestamp_from else True) & + (SessionInfo.start <= timestamp_to if timestamp_to else True)) + + total_session_count_by_profile_name = defaultdict(int) + + for session in profiles_sessions.dicts(): + total_session_count_by_profile_name[session['name']] += 1 + + return total_session_count_by_profile_name + + +def get_actions_count_within_hours(action_type=None, hours=None, profile=None, task_id=None) -> int: + """ + Returns the amount of actions by 'action_type', within the last 'hours' + """ + actions_count = 0 + actions_per_profile = get_actions_count_within_hours_for_profiles(action_types=[action_type] if action_type else None, + hours=hours if hours else None, + profiles=[profile] if profile else None, + task_ids=[task_id] if task_id else None) + + for actions in actions_per_profile.values(): + for count in actions.values(): + actions_count += count + + return actions_count + + +def get_actions_count_within_hours_for_profiles(action_types=None, hours=None, + profiles=None, task_ids=None, session_phases=None) -> dict: + timestamp_from, timestamp_to = None, None + if hours is not None: + timestamp_from, timestamp_to = get_from_to_timestamps_by_hours(hours) + + return get_actions_count_for_profiles(action_types=action_types, + timestamp_from=timestamp_from, + timestamp_to=timestamp_to, + profiles=profiles, + task_ids=task_ids, + session_phases=session_phases) + + +def get_actions_count_for_profiles(action_types=None, timestamp_from=None, timestamp_to=None, + profiles=None, task_ids=None, session_phases=None) -> dict: + """ + Returns the amount of actions by 'action_type', within the last 'hours', that been done by 'profiles' + """ + action_types_names = [at.__name__ for at in action_types] if action_types else None + session_phases_values = [sp.value for sp in session_phases] if session_phases else None + + profiles_actions = defaultdict(dict) + + query = InsomniacAction.select(InsomniacAction.actor_profile, InsomniacAction.type, fn.COUNT(InsomniacAction.id).alias('ct'))\ + .where((InsomniacAction.actor_profile.in_(profiles) if profiles else True) & + (InsomniacAction.task_id.in_(task_ids) if task_ids else True) & + (InsomniacAction.phase.in_(session_phases_values) if session_phases_values else True) & + (InsomniacAction.type.in_(action_types_names) if action_types_names else True) & + (InsomniacAction.timestamp >= timestamp_from if timestamp_from else True) & + (InsomniacAction.timestamp <= timestamp_to if timestamp_to else True))\ + .group_by(InsomniacAction.actor_profile, InsomniacAction.type) + + for obj in query: + profiles_actions[obj.actor_profile.name][obj.type] = obj.ct + + return profiles_actions def _get_db_version() -> Optional[int]: @@ -711,6 +861,17 @@ def _migrate_db_from_version_1_to_2(migrator): ) +def _migrate_db_from_version_2_to_3(migrator): + """ + Changes added on DB version 3: + + * Added InsomniacAction.phase field + """ + migrate( + migrator.add_column('actions_log', 'phase', TextField(default='task')), + ) + + 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}"] @@ -722,5 +883,6 @@ def _migrate(curr_version, migrator): database_migrations = { - "1->2": _migrate_db_from_version_1_to_2 + "1->2": _migrate_db_from_version_1_to_2, + "2->3": _migrate_db_from_version_2_to_3 } diff --git a/insomniac/device.py b/insomniac/device.py index 9324e98..ca81d2e 100644 --- a/insomniac/device.py +++ b/insomniac/device.py @@ -1,4 +1,5 @@ from insomniac.device_facade import create_device +from insomniac.params import resolve_app_id from insomniac.typewriter import Typewriter from insomniac.utils import * @@ -6,9 +7,10 @@ class DeviceWrapper(object): device = None - def __init__(self, device_id, old_uiautomator, wait_for_device, app_id, dont_set_typewriter): + def __init__(self, device_id, old_uiautomator, wait_for_device, app_id, app_name, dont_set_typewriter): self.device_id = device_id - self.app_id = app_id + self.app_id = resolve_app_id(app_id, device_id, app_name) + self.app_name = app_name self.old_uiautomator = old_uiautomator self.create(wait_for_device, dont_set_typewriter) diff --git a/insomniac/device_facade.py b/insomniac/device_facade.py index c93e5c1..123d4dd 100644 --- a/insomniac/device_facade.py +++ b/insomniac/device_facade.py @@ -68,12 +68,14 @@ def find(self, *args, **kwargs): raise DeviceFacade.JsonRpcError(e) return DeviceFacade.View(is_old=False, view=view, device=self) - def back(self): + def back(self) -> bool: """ Press back and check that UI hierarchy was changed. If it didn't change, it means that back press didn't work. So, we try to press back several times until it is finally changed. + + :return: whether backpress succeed """ - max_attempts = 5 + max_attempts = 2 def normalize(hierarchy): """ @@ -96,6 +98,7 @@ def normalize(hierarchy): print(COLOR_OKGREEN + "Pressed back but nothing changed on the screen. Will try again." + COLOR_ENDC) sleeper.random_sleep() attempts += 1 + return succeed def _press_back(self): if self.deviceV1 is not None: @@ -268,7 +271,7 @@ def _swipe(_from, _to): swipe_dir = "down" self.deviceV2.swipe_ext(swipe_dir, scale=scale) - def swipe_points(self, sx, sy, ex, ey): + def swipe_points(self, sx, sy, ex, ey, duration=None): if self.deviceV1 is not None: import uiautomator try: @@ -278,7 +281,10 @@ def swipe_points(self, sx, sy, ex, ey): else: import uiautomator2 try: - self.deviceV2.swipe_points([[sx, sy], [ex, ey]], uniform(0.2, 0.6)) + if duration: + self.deviceV2.swipe_points([[sx, sy], [ex, ey]], duration) + else: + self.deviceV2.swipe_points([[sx, sy], [ex, ey]], uniform(0.2, 0.6)) except uiautomator2.JSONRPCError as e: raise DeviceFacade.JsonRpcError(e) @@ -665,7 +671,7 @@ def is_focused(self) -> bool: raise DeviceFacade.JsonRpcError(e) def set_text(self, text): - if self.device.typewriter.write(self, text): + if self.device.typewriter is not None and self.device.typewriter.write(self, text): return if self.viewV1 is not None: import uiautomator diff --git a/insomniac/extra_features/management_actions_runners.py b/insomniac/extra_features/management_actions_runners.py new file mode 100644 index 0000000..c7c43b0 --- /dev/null +++ b/insomniac/extra_features/management_actions_runners.py @@ -0,0 +1,3 @@ +from insomniac import activation_controller + +exec(activation_controller.get_extra_feature('management_actions_runners')) diff --git a/insomniac/globals.py b/insomniac/globals.py index 5d62b9e..3e343ac 100644 --- a/insomniac/globals.py +++ b/insomniac/globals.py @@ -2,7 +2,8 @@ is_ui_process = False execution_id = '' task_id = '' -db_name = 'insomniac.db' +executable_name = 'insomniac' +do_location_permission_dialog_checks = True # no need in these checks if location permission is denied beforehand def is_insomniac(): diff --git a/insomniac/limits.py b/insomniac/limits.py index fe41723..1795b66 100644 --- a/insomniac/limits.py +++ b/insomniac/limits.py @@ -105,18 +105,25 @@ class TotalLikesLimit(CoreLimit): LIMIT_TYPE = LimitType.SESSION LIMIT_ARGS = { "total_likes_limit": { - "help": "limit on total amount of likes during the session, 300 by default. " + "help": "deprecated - use likes_session_limit instead. " + "limit on total amount of likes during the session, 300 by default. " "It can be a number presenting specific limit (e.g. 300) or a range (e.g. 100-120)", "metavar": "300", "default": "1000" + }, + "likes_session_limit": { + "help": "limit on total amount of likes during the session, disabled by default. " + "It can be a number presenting specific limit (e.g. 300) or a range (e.g. 100-120)", + "metavar": "150", + "default": None } } total_likes_limit = 1000 def set_limit_values(self, args): - if args.total_likes_limit is not None: - self.total_likes_limit = get_value(args.total_likes_limit, "Total likes limit: {}", 1000) + if args.likes_session_limit is not None or args.total_likes_limit is not None: + self.total_likes_limit = get_value(args.likes_session_limit or args.total_likes_limit, "Total likes limit: {}", 1000) def is_reached_for_action(self, action, session_state): if not type(action) == LikeAction: @@ -139,26 +146,41 @@ class TotalInteractionsLimit(CoreLimit): "help": "number of total interactions (successful & unsuccessful) per session, disabled by default. " "It can be a number (e.g. 70) or a range (e.g. 60-80)", "metavar": '60-80' + }, + "interaction_session_limit": { + "help": "number of total interactions (successful & unsuccessful) per session, disabled by default. " + "It can be a number (e.g. 70) or a range (e.g. 60-80)", + "metavar": "150" } } total_interactions_limit = None + interaction_session_limit = None def set_limit_values(self, args): if args.total_interactions_limit is not None: self.total_interactions_limit = get_value(args.total_interactions_limit, "Total interactions limit: {}", 1000) - def is_reached_for_action(self, action, session_state): - if self.total_interactions_limit is None: - return False + if args.interaction_session_limit is not None: + self.interaction_session_limit = get_value(args.interaction_session_limit, "Interactions session limit: {}", 1000) + def is_reached_for_action(self, action, session_state): if not type(action) == InteractAction: return False - return sum(session_state.totalInteractions.values()) >= self.total_interactions_limit + if self.total_interactions_limit is not None: + if sum(session_state.totalInteractions.values()) >= self.total_interactions_limit: + return True + + if self.interaction_session_limit is not None: + if sum(session_state.totalInteractions.values()) >= self.interaction_session_limit: + return True + + return False def reset(self): self.total_interactions_limit = None + self.interaction_session_limit = None def update_state(self, action): pass @@ -172,27 +194,43 @@ class TotalSuccessfulInteractionsLimit(CoreLimit): "help": "number of total successful interactions per session, disabled by default. " "It can be a number (e.g. 70) or a range (e.g. 60-80)", "metavar": '60-80' + }, + "successful_interaction_session_limit": { + "help": "number of total successful interactions per session, disabled by default. " + "It can be a number (e.g. 70) or a range (e.g. 60-80)", + "metavar": "150" } } total_successful_interactions_limit = None + successful_interaction_session_limit = None def set_limit_values(self, args): if args.total_successful_interactions_limit is not None: self.total_successful_interactions_limit = get_value(args.total_successful_interactions_limit, "Total successful-interactions limit: {}", 1000) - def is_reached_for_action(self, action, session_state): - if self.total_successful_interactions_limit is None: - return False + if args.successful_interaction_session_limit is not None: + self.successful_interaction_session_limit = get_value(args.successful_interaction_session_limit, + "Successful-interactions session limit: {}", 1000) + def is_reached_for_action(self, action, session_state): if not type(action) == InteractAction: return False - return sum(session_state.successfulInteractions.values()) >= self.total_successful_interactions_limit + if self.total_successful_interactions_limit is not None: + if sum(session_state.successfulInteractions.values()) >= self.total_successful_interactions_limit: + return True + + if self.successful_interaction_session_limit is not None: + if sum(session_state.successfulInteractions.values()) >= self.successful_interaction_session_limit: + return True + + return False def reset(self): self.total_successful_interactions_limit = None + self.successful_interaction_session_limit = None def update_state(self, action): pass @@ -203,6 +241,12 @@ class TotalFollowLimit(CoreLimit): LIMIT_TYPE = LimitType.SESSION LIMIT_ARGS = { "total_follow_limit": { + "help": "deprecated - use follow_session_limit instead. " + "limit on total amount of follows during the session, disabled by default. " + "It can be a number (e.g. 27) or a range (e.g. 20-30)", + "metavar": "50" + }, + "follow_session_limit": { "help": "limit on total amount of follows during the session, disabled by default. " "It can be a number (e.g. 27) or a range (e.g. 20-30)", "metavar": "50" @@ -212,8 +256,8 @@ class TotalFollowLimit(CoreLimit): total_follow_limit = None def set_limit_values(self, args): - if args.total_follow_limit is not None: - self.total_follow_limit = get_value(args.total_follow_limit, "Total follow limit: {}", 70) + if args.total_follow_limit is not None or args.follow_session_limit is not None: + self.total_follow_limit = get_value(args.follow_session_limit or args.total_follow_limit, "Total follow limit: {}", 70) def is_reached_for_action(self, action, session_state): if self.total_follow_limit is None: @@ -236,6 +280,12 @@ class TotalStoryWatchLimit(CoreLimit): LIMIT_TYPE = LimitType.SESSION LIMIT_ARGS = { "total_story_limit": { + "help": "deprecated - use story_session_limit instead. " + "limit on total amount of stories watches during the session, disabled by default. " + "It can be a number (e.g. 27) or a range (e.g. 20-30)", + "metavar": "300", + }, + "story_session_limit": { "help": "limit on total amount of stories watches during the session, disabled by default. " "It can be a number (e.g. 27) or a range (e.g. 20-30)", "metavar": "300", @@ -245,8 +295,8 @@ class TotalStoryWatchLimit(CoreLimit): total_story_limit = None def set_limit_values(self, args): - if args.total_story_limit is not None: - self.total_story_limit = get_value(args.total_story_limit, "Total story-watches limit: {}", 1000) + if args.total_story_limit is not None or args.story_session_limit is not None: + self.total_story_limit = get_value(args.story_session_limit or args.total_story_limit, "Total story-watches limit: {}", 1000) def is_reached_for_action(self, action, session_state): if self.total_story_limit is None: @@ -269,6 +319,13 @@ class TotalCommentsLimit(CoreLimit): LIMIT_TYPE = LimitType.SESSION LIMIT_ARGS = { "total_comments_limit": { + "help": "deprecated - use comment_session_limit instead. " + "limit on total amount of comments during the session, 50 by default. " + "It can be a number presenting specific limit (e.g. 300) or a range (e.g. 100-120)", + "metavar": "300", + "default": "50" + }, + "comment_session_limit": { "help": "limit on total amount of comments during the session, 50 by default. " "It can be a number presenting specific limit (e.g. 300) or a range (e.g. 100-120)", "metavar": "300", @@ -279,8 +336,8 @@ class TotalCommentsLimit(CoreLimit): total_comments_limit = 50 def set_limit_values(self, args): - if args.total_comments_limit is not None: - self.total_comments_limit = get_value(args.total_comments_limit, "Total comments limit: {}", 50) + if args.total_comments_limit is not None or args.comment_session_limit is not None: + self.total_comments_limit = get_value(args.comment_session_limit or args.total_comments_limit, "Total comments limit: {}", 50) def is_reached_for_action(self, action, session_state): if not type(action) == CommentAction: @@ -470,25 +527,41 @@ def update_state(self, action): class UnfollowingLimit(CoreLimit): LIMIT_ID = "unfollowing_limit" LIMIT_TYPE = LimitType.SESSION - LIMIT_ARGS = {} + LIMIT_ARGS = { + "unfollow_session_limit": { + "help": "limit on total amount of unfollow-actions during the current session, disabled by default. " + "It can be a number (e.g. 100) or a range (e.g. 90-120)", + "metavar": "150" + } + } - unfollow_limit = None + unfollow_config_limit = None + unfollow_session_limit = None def set_limit_values(self, args): if args.unfollow is not None: - self.unfollow_limit = get_value(args.unfollow, "Unfollow: {}", 100) + self.unfollow_config_limit = get_value(args.unfollow, "Unfollow: {}", 100) - def is_reached_for_action(self, action, session_state): - if self.unfollow_limit is None: - return False + if args.unfollow_session_limit is not None: + self.unfollow_session_limit = get_value(args.unfollow_session_limit, "Unfollow session limit: {}", 100) + def is_reached_for_action(self, action, session_state): if not type(action) == UnfollowAction: return False - return session_state.totalUnfollowed >= self.unfollow_limit + if self.unfollow_config_limit is not None: + if session_state.totalUnfollowed >= self.unfollow_config_limit: + return True + + if self.unfollow_session_limit is not None: + if session_state.totalUnfollowed >= self.unfollow_session_limit: + return True + + return False def reset(self): - self.unfollow_limit = None + self.unfollow_config_limit = None + self.unfollow_session_limit = None def update_state(self, action): pass @@ -567,17 +640,23 @@ class TotalGetProfileLimit(CoreLimit): LIMIT_TYPE = LimitType.SESSION LIMIT_ARGS = { "total_get_profile_limit": { + "help": "deprecated - use get_profile_session_limit instead. " + "limit on total amount of get-profile actions during the session, disabled by default. " + "It can be a number (e.g. 600) or a range (e.g. 500-700)", + "metavar": "1500" + }, + "get_profile_session_limit": { "help": "limit on total amount of get-profile actions during the session, disabled by default. " "It can be a number (e.g. 600) or a range (e.g. 500-700)", "metavar": "1500" - } + }, } total_get_profile_limit = None def set_limit_values(self, args): - if args.total_get_profile_limit is not None: - self.total_get_profile_limit = get_value(args.total_get_profile_limit, "Total get-profile limit: {}", 1000) + if args.total_get_profile_limit is not None or args.get_profile_session_limit is not None: + self.total_get_profile_limit = get_value(args.get_profile_session_limit or args.total_get_profile_limit, "Total get-profile limit: {}", 1000) def is_reached_for_action(self, action, session_state): if self.total_get_profile_limit is None: diff --git a/insomniac/migration.py b/insomniac/migration.py index a434884..e1949d4 100644 --- a/insomniac/migration.py +++ b/insomniac/migration.py @@ -168,20 +168,20 @@ def migrate_from_sql_to_peewee(my_username): None, session["app_version"], session["args"] or "", - ProfileStatus.VALID, + ProfileStatus.VALID.value, session["followers"], session["following"], datetime.strptime(session["start_time"], '%Y-%m-%d %H:%M:%S.%f') if session["start_time"] is not None else datetime.now(), datetime.strptime(session["finish_time"], '%Y-%m-%d %H:%M:%S.%f') if session["finish_time"] is not None else None ) - session_id = my_profile.start_session(None, "Unknown app version: migration", "Unknown args: migration", ProfileStatus.VALID, -1, -1) + session_id = my_profile.start_session(None, "Unknown app version: migration", "Unknown args: migration", ProfileStatus.VALID.value, -1, -1) print(f"[Migration] Migrating interacted users to the {DATABASE_NAME}...") for interacted_user in get_all_interacted_users(database): - my_profile.log_like_action(session_id, interacted_user["username"], None, None) + my_profile.log_like_action(session_id, SessionPhase.TASK_LOGIC.value, interacted_user["username"], None, None) print(f"[Migration] Migrating filtered users to the {DATABASE_NAME}...") for filtered_user in get_all_filtered_users(database): - my_profile.log_filter_action(session_id, filtered_user["username"]) + my_profile.log_filter_action(session_id, SessionPhase.TASK_LOGIC.value, filtered_user["username"]) print(f"[Migration] Migrating scraped users to the {DATABASE_NAME}...") for scraped_user in get_all_scraped_users(database): my_profile.publish_scrapped_account(scraped_user["username"], [my_username]) diff --git a/insomniac/navigation.py b/insomniac/navigation.py index 8128cce..26976c7 100644 --- a/insomniac/navigation.py +++ b/insomniac/navigation.py @@ -1,6 +1,5 @@ -from insomniac.sleeper import sleeper from insomniac.utils import * -from insomniac.views import TabBarView, ProfileView, TabBarTabs, LanguageNotEnglishException +from insomniac.views import TabBarView, ProfileView, TabBarTabs, LanguageNotEnglishException, DialogView SEARCH_CONTENT_DESC_REGEX = '[Ss]earch and [Ee]xplore' @@ -8,27 +7,16 @@ def navigate(device, tab, switch_to_english_on_exception=True): try: TabBarView(device).navigate_to(tab) - except LanguageNotEnglishException as e: + except LanguageNotEnglishException as ex: if not switch_to_english_on_exception: - raise e - save_crash(device) + raise ex + save_crash(device, ex) switch_to_english(device) raise LanguageChangedException() def search_for(device, username=None, hashtag=None, place=None, on_action=None): - tab_bar_view = TabBarView(device) - - # There may be no TabBarView if Instagram was opened via a deeplink. Then we have to clear the backstack. - is_message_printed = False - while not tab_bar_view.is_visible(): - if not is_message_printed: - print(COLOR_OKGREEN + "Clearing the back stack..." + COLOR_ENDC) - is_message_printed = True - tab_bar_view.press_back_arrow() - sleeper.random_sleep() - - search_view = tab_bar_view.navigate_to_search() + search_view = TabBarView(device).navigate_to_search() target_view = None if username is not None: @@ -52,5 +40,11 @@ def switch_to_english(device): .switch_to_english() +def close_instagram_and_system_dialogs(device): + close_instagram(device.device_id, device.app_id) + # If the app crashed there will be a system dialog + DialogView(device).close_not_responding_dialog_if_visible() + + class LanguageChangedException(Exception): pass diff --git a/insomniac/params.py b/insomniac/params.py index 39b8ad0..37f03ca 100644 --- a/insomniac/params.py +++ b/insomniac/params.py @@ -3,6 +3,16 @@ from insomniac.utils import * +DEFAULT_APP_ID = "com.instagram.android" + +CONFIG_PARAMETER_ENABLED = "enabled" +CONFIG_PARAMETER_NAME = "parameter-name" +CONFIG_PARAMETER_VALUE = "value" + +PARAMETER_APP_ID = "app_id" +PARAMETER_DEVICE_ID = "device" +PARAMETER_APP_NAME = "app_name" + def parse_arguments(all_args_dict, starter_conf_file_path): parser = argparse.ArgumentParser( @@ -56,29 +66,68 @@ def refresh_args_by_conf_file(args, conf_file_name=None): if config_file is None: config_file = args.config_file - if config_file is not None: - if not os.path.exists(config_file): - print(COLOR_FAIL + "Config file {0} could not be found - aborting. " - "Please check your file-path and try again.".format(config_file) + COLOR_ENDC) - return False + params = _load_params(config_file) + if params is None: + return False - try: - args_by_conf_file = args.__getattribute__('args_by_conf_file') - for arg_name in args_by_conf_file: - args.__setattr__(arg_name, None) - except AttributeError: - pass + try: + args_by_conf_file = args.__getattribute__('args_by_conf_file') + for arg_name in args_by_conf_file: + args.__setattr__(arg_name, None) + except AttributeError: + pass - args_by_conf_file = [] + args_by_conf_file = [] + for param in params: + if param[CONFIG_PARAMETER_ENABLED]: + args.__setattr__(param[CONFIG_PARAMETER_NAME], param[CONFIG_PARAMETER_VALUE]) + args_by_conf_file.append(param[CONFIG_PARAMETER_NAME]) - with open(config_file, encoding="utf-8") as json_file: - params = json.load(json_file) + args.__setattr__('args_by_conf_file', args_by_conf_file) - for param in params: - if param["enabled"]: - args.__setattr__(param["parameter-name"], param["value"]) - args_by_conf_file.append(param["parameter-name"]) + return True - args.__setattr__('args_by_conf_file', args_by_conf_file) - return True +def load_app_id(config_file): + params = _load_params(config_file) + if params is None: + return DEFAULT_APP_ID + + app_id = None + device_id = None + app_name = None + + for param in params: + if param.get(CONFIG_PARAMETER_NAME) == PARAMETER_APP_ID and param.get(CONFIG_PARAMETER_ENABLED): + app_id = param.get(CONFIG_PARAMETER_VALUE) + elif param.get(CONFIG_PARAMETER_NAME) == PARAMETER_DEVICE_ID and param.get(CONFIG_PARAMETER_ENABLED): + device_id = param.get(CONFIG_PARAMETER_VALUE) + elif param.get(CONFIG_PARAMETER_NAME) == PARAMETER_APP_NAME and param.get(CONFIG_PARAMETER_ENABLED): + app_name = param.get(CONFIG_PARAMETER_VALUE) + + return resolve_app_id(app_id, device_id, app_name) + + +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}") + 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) + + return app_id or DEFAULT_APP_ID + + +def _load_params(config_file): + if config_file is None: + return None + + if not os.path.exists(config_file): + print(COLOR_FAIL + "Config file {0} could not be found".format(config_file) + COLOR_ENDC) + return None + + with open(config_file, encoding="utf-8") as json_file: + return json.load(json_file) diff --git a/insomniac/report.py b/insomniac/report.py index ceb5262..73188fe 100644 --- a/insomniac/report.py +++ b/insomniac/report.py @@ -1,34 +1,8 @@ -from datetime import timedelta - from insomniac.utils import * def print_full_report(sessions): - if len(sessions) > 1: - for index, session in enumerate(sessions): - finish_time = session.finishTime or datetime.now() - print_timeless("\n") - print_timeless(COLOR_REPORT + "SESSION #" + str(index + 1) + f" - {session.my_username}" + COLOR_ENDC) - print_timeless(COLOR_REPORT + "Start time: " + str(session.startTime) + COLOR_ENDC) - print_timeless(COLOR_REPORT + "Finish time: " + str(finish_time) + COLOR_ENDC) - print_timeless(COLOR_REPORT + "Duration: " + str(finish_time - session.startTime) + COLOR_ENDC) - print_timeless(COLOR_REPORT + "Total interactions: " + _stringify_interactions(session.totalInteractions) - + COLOR_ENDC) - print_timeless(COLOR_REPORT + "Successful interactions: " - + _stringify_interactions(session.successfulInteractions) + COLOR_ENDC) - print_timeless(COLOR_REPORT + "Total followed: " - + _stringify_interactions(session.totalFollowed) + COLOR_ENDC) - print_timeless(COLOR_REPORT + "Total likes: " + str(session.totalLikes) + COLOR_ENDC) - print_timeless(COLOR_REPORT + "Total comments: " + str(session.totalComments) + COLOR_ENDC) - print_timeless(COLOR_REPORT + "Total unfollowed: " + str(session.totalUnfollowed) + COLOR_ENDC) - print_timeless(COLOR_REPORT + "Total get-profile: " + str(session.totalGetProfile) + COLOR_ENDC) - print_timeless(COLOR_REPORT + "Total scraped: " - + _stringify_interactions(session.totalScraped) + COLOR_ENDC) - print_timeless(COLOR_REPORT + "Removed mass followers: " - + _stringify_removed_mass_followers(session.removedMassFollowers) + COLOR_ENDC) - print_timeless("\n") - print_timeless(COLOR_REPORT + "TOTAL" + COLOR_ENDC) completed_sessions = [session for session in sessions if session.is_finished()] print_timeless(COLOR_REPORT + "Completed sessions: " + str(len(completed_sessions)) + COLOR_ENDC) diff --git a/insomniac/safely_runner.py b/insomniac/safely_runner.py index a6a1838..6f8b4b5 100644 --- a/insomniac/safely_runner.py +++ b/insomniac/safely_runner.py @@ -1,11 +1,15 @@ from http.client import HTTPException from socket import timeout +import adbutils +import urllib3 + +from insomniac import __version__ from insomniac.device_facade import DeviceFacade -from insomniac.navigation import navigate, LanguageChangedException +from insomniac.globals import is_insomniac +from insomniac.navigation import LanguageChangedException, close_instagram_and_system_dialogs from insomniac.sleeper import sleeper from insomniac.utils import * -from insomniac.views import TabBarTabs class RestartJobRequiredException(Exception): @@ -16,24 +20,25 @@ def run_safely(device_wrapper): def actual_decorator(func): def wrapper(*args, **kwargs): try: - func(*args, **kwargs) - except (DeviceFacade.JsonRpcError, IndexError, HTTPException, timeout) as ex: - print(COLOR_FAIL + describe_exception(ex) + COLOR_ENDC) + return func(*args, **kwargs) + except (IndexError, OSError, RuntimeError, + HTTPException, urllib3.exceptions.HTTPError, + DeviceFacade.JsonRpcError, adbutils.errors.AdbError) as ex: + print(COLOR_FAIL + describe_exception(ex, with_stacktrace=__version__.__debug_mode__ or not is_insomniac()) + COLOR_ENDC) + # Check that adb works fine + check_adb_connection(device_wrapper.device_id, wait_for_device=True) + # Try to save the crash save_crash(device_wrapper.get(), ex) print("No idea what it was. Let's try again.") # Hack for the case when IGTV was accidentally opened - close_instagram(device_wrapper.device_id, device_wrapper.app_id) - sleeper.random_sleep() + close_instagram_and_system_dialogs(device_wrapper.get()) open_instagram(device_wrapper.device_id, device_wrapper.app_id) - navigate(device_wrapper.get(), TabBarTabs.PROFILE) except LanguageChangedException: print_timeless("") print("Language was changed. We'll have to start from the beginning.") - navigate(device_wrapper.get(), TabBarTabs.PROFILE) except RestartJobRequiredException: print_timeless("") print("Restarting job...") - navigate(device_wrapper.get(), TabBarTabs.PROFILE) return wrapper return actual_decorator @@ -55,8 +60,8 @@ def wrapper(*args, **kwargs): # Instagram app restart new_identity(device_wrapper.device_id, device_wrapper.app_id) sleeper.random_sleep(multiplier=2.0) - close_instagram(device_wrapper.device_id, device_wrapper.app_id) - airplane_mode_on_off(device_wrapper.get()) + close_instagram_and_system_dialogs(device_wrapper.get()) + airplane_mode_on_off(device_wrapper) open_instagram(device_wrapper.device_id, device_wrapper.app_id) sleeper.random_sleep() return wrapper diff --git a/insomniac/session.py b/insomniac/session.py index e9c2852..5533e93 100644 --- a/insomniac/session.py +++ b/insomniac/session.py @@ -1,11 +1,17 @@ +import json +from abc import ABC + import insomniac.__version__ as __version__ import insomniac.softban_indicator as softban_indicator +import insomniac.validations as insomniac_validations +from insomniac import network, HTTP_OK from insomniac.action_get_my_profile_info import get_my_profile_info -from insomniac.action_runners.actions_runners_manager import ActionRunnersManager +from insomniac.action_runners.actions_runners_manager import CoreActionRunnersManager from insomniac.device import DeviceWrapper from insomniac.limits import LimitsManager from insomniac.migration import migrate_from_json_to_sql, migrate_from_sql_to_peewee -from insomniac.params import parse_arguments, refresh_args_by_conf_file +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.sessions import Sessions @@ -14,18 +20,123 @@ from insomniac.storage import STORAGE_ARGS, Storage, DatabaseMigrationFailedException from insomniac.utils import * from insomniac.views import UserSwitchFailedException -import insomniac.validations as insomniac_validations sessions = Sessions() -class InsomniacSession(object): +def get_insomniac_session(starter_conf_file_path): + return InsomniacSession(starter_conf_file_path) + + +class Session(ABC): SESSION_ARGS = { "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)', "metavar": '120-180' }, + "no_typing": { + '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' + }, + "next_config_file": { + "help": 'configuration that will be loaded after session is finished and the bot \"sleeps\" for time ' + 'specified by the \"--repeat\" argument. You can use this argument to run multiple Insomniac ' + 'sessions one by one with different parameters. E.g. different action (interact and then unfollow),' + ' or different \"--username\". By default uses the same config file as been loaded for the first ' + 'session. Note that you must use \"--repeat\" with this argument!', + "metavar": 'CONFIG_FILE', + "default": None + }, + "speed": { + 'help': 'manually specify the speed setting, from 1 (slowest) to 4 (fastest)', + 'metavar': '1-4', + 'type': int, + 'choices': range(1, 5) + } + } + + repeat = None + old = None + next_config_file = None + + def __init__(self, starter_conf_file_path=None): + self.starter_conf_file_path = starter_conf_file_path + self.sessions = sessions + + def get_session_args(self): + all_args = {} + all_args.update(self.SESSION_ARGS) + + return all_args + + def reset_params(self): + self.repeat = None + self.next_config_file = None + __version__.__debug_mode__ = False + + def set_session_args(self, args): + self.reset_params() + + if args.repeat is not None: + self.repeat = get_float_value(args.repeat, "Sleep time (min) before repeat: {:.2f}", 180.0) + + if args.debug is not None and bool(args.debug): + __version__.__debug_mode__ = True + + if args.next_config_file is not None: + self.next_config_file = args.next_config_file + + def parse_args(self): + ok, args = parse_arguments(self.get_session_args(), self.starter_conf_file_path) + if not ok: + return None + return args + + @staticmethod + def print_session_params(args): + if args.debug: + print("All parameters:") + for k, v in vars(args).items(): + print(f"{k}: {v} (value-type: {type(v)})") + + def repeat_session(self, args): + print("Sleep for {:.2f} minutes".format(self.repeat)) + try: + sleep(60 * self.repeat) + return refresh_args_by_conf_file(args, self.next_config_file) + except KeyboardInterrupt: + print_full_report(self.sessions) + sys.exit(0) + + @staticmethod + def update_session_speed(args): + if args.speed is not None: + sleeper.set_random_sleep_range(int(args.speed)) + elif not args.no_speed_check: + print("Checking your Internet speed to adjust the script speed, please wait for a minute...") + print("(use " + COLOR_BOLD + "--no-speed-check" + COLOR_ENDC + " to skip this check)") + sleeper.update_random_sleep_range() + + @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 + + def run(self): + raise NotImplementedError + + +class InsomniacSession(Session): + INSOMNIAC_SESSION_ARGS = { "device": { "help": 'device identifier. Should be used only when multiple devices are connected at once', "metavar": '2443de990e017ece' @@ -40,21 +151,15 @@ class InsomniacSession(object): 'help': 'skip internet speed check at start', 'action': 'store_true' }, - "no_typing": { - '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' - }, "app_id": { "help": 'apk package identifier. Should be used only if you are using cloned-app. ' 'Using \'com.instagram.android\' by default', "metavar": 'com.instagram.android', "default": 'com.instagram.android' }, + "app_name": { + "default": None + }, "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", @@ -68,52 +173,29 @@ class InsomniacSession(object): 'action': 'store_true', "default": False }, - "debug": { - 'help': 'add this flag to run insomniac in debug mode (more verbose logs)', - 'action': 'store_true' - }, "username": { "help": 'if you have configured multiple Instagram accounts in your app, use this parameter in order to ' 'switch into a specific one. Not trying to switch account by default. ' 'If the account does not exist - the session won\'t start', "metavar": 'my_account_name', "default": None - }, - "next_config_file": { - "help": 'configuration that will be loaded after session is finished and the bot \"sleeps\" for time ' - 'specified by the \"--repeat\" argument. You can use this argument to run multiple Insomniac ' - 'sessions one by one with different parameters. E.g. different action (interact and then unfollow),' - ' or different \"--username\". By default uses the same config file as been loaded for the first ' - 'session. Note that you must use \"--repeat\" with this argument!', - "metavar": 'CONFIG_FILE', - "default": None - }, - "speed": { - 'help': 'manually specify the speed setting, from 1 (slowest) to 4 (fastest)', - 'metavar': '1-4', - 'type': int, - 'choices': range(1, 5) - }, + } } - repeat = None device = None - old = None username = None - next_config_file = None def __init__(self, starter_conf_file_path=None): - self.starter_conf_file_path = starter_conf_file_path + super().__init__(starter_conf_file_path) - self.sessions = sessions self.storage = None self.session_state = None - self.actions_mgr = ActionRunnersManager() + self.actions_mgr = CoreActionRunnersManager() self.limits_mgr = LimitsManager() def get_session_args(self): - all_args = {} - all_args.update(self.SESSION_ARGS) + all_args = super().get_session_args() + all_args.update(self.INSOMNIAC_SESSION_ARGS) all_args.update(STORAGE_ARGS) all_args.update(self.actions_mgr.get_actions_args()) all_args.update(self.limits_mgr.get_limits_args()) @@ -121,21 +203,13 @@ def get_session_args(self): return all_args def reset_params(self): - self.repeat = None + super().reset_params() self.username = None - self.next_config_file = None - __version__.__debug_mode__ = False softban_indicator.should_indicate_softban = True insomniac_validations.should_validate_profile_existence = True def set_session_args(self, args): - self.reset_params() - - if args.repeat is not None: - self.repeat = get_value(args.repeat, "Sleep time (min) before repeat: {}", 180) - - if args.debug is not None and bool(args.debug): - __version__.__debug_mode__ = True + super().set_session_args(args) if args.dont_indicate_softban: softban_indicator.should_indicate_softban = False @@ -146,28 +220,19 @@ def set_session_args(self, args): if args.username is not None: self.username = args.username - if args.next_config_file is not None: - self.next_config_file = args.next_config_file - - def parse_args(self): - ok, args = parse_arguments(self.get_session_args(), self.starter_conf_file_path) - if not ok: - return None - return args - def get_device_wrapper(self, args): - device_wrapper = DeviceWrapper(args.device, args.old, args.wait_for_device, args.app_id, args.no_typing) + device_wrapper = DeviceWrapper(args.device, args.old, args.wait_for_device, args.app_id, args.app_name, args.no_typing) device = device_wrapper.get() if device is None: return None, None - app_version = get_instagram_version(args.device, args.app_id) - + app_version = get_instagram_version(device_wrapper.device_id, device_wrapper.app_id) print("Instagram version: " + app_version) + self.verify_instagram_version(app_version) return device_wrapper, app_version - def start_session(self, args, device_wrapper, app_version, save_profile_info=True): + def prepare_session_state(self, args, device_wrapper, app_version, save_profile_info=True): self.session_state = SessionState() self.session_state.args = args.__dict__ self.session_state.app_id = args.app_id @@ -178,12 +243,9 @@ def start_session(self, args, device_wrapper, app_version, save_profile_info=Tru print_timeless(COLOR_REPORT + "\n-------- START: " + str(self.session_state.startTime) + " --------" + COLOR_ENDC) - close_instagram(device_wrapper.device_id, device_wrapper.app_id) - sleeper.random_sleep() - if __version__.__debug_mode__: device_wrapper.get().start_screen_record() - open_instagram(args.device, args.app_id) + open_instagram(device_wrapper.device_id, device_wrapper.app_id) if save_profile_info: self.session_state.my_username, \ self.session_state.my_followers_count, \ @@ -191,8 +253,9 @@ def start_session(self, args, device_wrapper, app_version, save_profile_info=Tru return self.session_state - def end_session(self, device_wrapper): - close_instagram(device_wrapper.device_id, device_wrapper.app_id) + def end_session(self, device_wrapper, with_app_closing=True): + if with_app_closing: + close_instagram_and_system_dialogs(device_wrapper.get()) if __version__.__debug_mode__: device_wrapper.get().stop_screen_record() print_copyright() @@ -202,25 +265,10 @@ def end_session(self, device_wrapper): print_full_report(self.sessions) print_timeless("") - def repeat_session(self, args): - print("Sleep for {} minutes".format(self.repeat)) - try: - sleep(60 * self.repeat) - return refresh_args_by_conf_file(args, self.next_config_file) - except KeyboardInterrupt: - print_full_report(self.sessions) - sys.exit(0) - def on_action_callback(self, action): self.session_state.add_action(action) self.limits_mgr.update_state(action) - def print_session_params(self, args): - if args.debug: - print("All parameters:") - for k, v in vars(args).items(): - print(f"{k}: {v} (value-type: {type(v)})") - def run(self): args = self.parse_args() if args is None: @@ -229,12 +277,7 @@ def run(self): while True: self.print_session_params(args) - if args.speed is not None: - sleeper.set_random_sleep_range(int(args.speed)) - elif not args.no_speed_check: - print("Checking your Internet speed to adjust the script speed, please wait for a minute...") - print("(use " + COLOR_BOLD + "--no-speed-check" + COLOR_ENDC + " to skip this check)") - sleeper.update_random_sleep_range() + self.update_session_speed(args) device_wrapper, app_version = self.get_device_wrapper(args) if device_wrapper is None: @@ -247,15 +290,17 @@ def run(self): if action_runner is None: return + action_runner.reset_params() action_runner.set_params(args) self.limits_mgr.set_limits(args) try: - self.start_session(args, device_wrapper, app_version, save_profile_info=True) + 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.session_state.set_storage_layer(self.storage) + self.session_state.start_session() action_runner.run(device_wrapper, self.storage, @@ -267,7 +312,8 @@ def run(self): return except ActionBlockedError as ex: print_timeless("") - print_timeless(COLOR_FAIL + str(ex) + COLOR_ENDC) + print(COLOR_FAIL + describe_exception(ex, with_stacktrace=False) + COLOR_ENDC) + save_crash(device_wrapper.get()) if self.next_config_file is None: self.end_session(device_wrapper) return @@ -275,13 +321,36 @@ def run(self): if __version__.__debug_mode__: raise ex else: - print_timeless(COLOR_FAIL + f"\nCaught an exception:\n" + COLOR_ENDC) print(COLOR_FAIL + describe_exception(ex) + COLOR_ENDC) save_crash(device_wrapper.get(), ex) - self.end_session(device_wrapper) + self.end_session(device_wrapper, with_app_closing=(not self.is_next_app_id_same(args, device_wrapper))) if self.repeat is not None: if not self.repeat_session(args): break else: break + + @staticmethod + def verify_instagram_version(installed_ig_version): + code, body, _ = network.get(f"https://insomniac-bot.com/get_latest_supported_ig_version/") + if code == HTTP_OK and body is not None: + json_config = json.loads(body) + latest_supported_ig_version = json_config['message'] + else: + return + + try: + is_ok = versiontuple(installed_ig_version) <= versiontuple(latest_supported_ig_version) + except ValueError: + print_debug(COLOR_FAIL + "Cannot compare IG versions" + COLOR_ENDC) + return + + if not is_ok: + print_timeless("") + print_timeless(COLOR_FAIL + f"IG version ({installed_ig_version}) is newer than " + f"latest supported ({latest_supported_ig_version})." + COLOR_ENDC) + if insomniac_globals.is_insomniac(): + print_timeless(COLOR_FAIL + "Please uninstall IG and download recommended apk from here:" + COLOR_ENDC) + print_timeless(COLOR_FAIL + COLOR_BOLD + "https://insomniac-bot.com/get_latest_supported_ig_apk/" + COLOR_ENDC) + print_timeless("") diff --git a/insomniac/session_state.py b/insomniac/session_state.py index 1f4a075..1d5d93e 100644 --- a/insomniac/session_state.py +++ b/insomniac/session_state.py @@ -3,7 +3,7 @@ from insomniac.actions_types import LikeAction, InteractAction, FollowAction, GetProfileAction, ScrapeAction, \ UnfollowAction, RemoveMassFollowerAction, StoryWatchAction, CommentAction, DirectMessageAction, FilterAction -from insomniac.storage import Storage +from insomniac.storage import Storage, SessionPhase class SessionState: @@ -28,6 +28,8 @@ class SessionState: startTime = None finishTime = None storage: Optional[Storage] = None + session_phase = SessionPhase.TASK_LOGIC + is_started = False def __init__(self): self.id = None @@ -51,52 +53,68 @@ def __init__(self): 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, 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 + + def end_warmap(self): + self.session_phase = SessionPhase.TASK_LOGIC + def add_action(self, action): if type(action) == GetProfileAction: self.totalGetProfile += 1 - self.storage.log_get_profile_action(self.id, action.user) + self.storage.log_get_profile_action(self.id, self.session_phase, action.user) if type(action) == LikeAction: self.totalLikes += 1 - self.storage.log_like_action(self.id, action.user, action.source_type, action.source_name) + self.storage.log_like_action(self.id, self.session_phase, action.user, action.source_type, action.source_name) if type(action) == FollowAction: - if self.totalFollowed.get(action.source_name) is None: - self.totalFollowed[action.source_name] = 1 + source_name = action.source_name if action.source_type is not None else self.SOURCE_NAME_TARGETS + if self.totalFollowed.get(source_name) is None: + self.totalFollowed[source_name] = 1 else: - self.totalFollowed[action.source_name] += 1 + self.totalFollowed[source_name] += 1 - self.storage.log_follow_action(self.id, action.user, action.source_type, action.source_name) + self.storage.log_follow_action(self.id, self.session_phase, action.user, action.source_type, action.source_name) self.storage.update_follow_status(action.user, do_i_follow_him=True) if type(action) == StoryWatchAction: self.totalStoriesWatched += 1 - self.storage.log_story_watch_action(self.id, action.user, action.source_type, action.source_name) + self.storage.log_story_watch_action(self.id, self.session_phase, action.user, action.source_type, action.source_name) if type(action) == CommentAction: self.totalComments += 1 - self.storage.log_comment_action(self.id, action.user, action.comment, action.source_type, action.source_name) + self.storage.log_comment_action(self.id, self.session_phase, action.user, action.comment, action.source_type, action.source_name) if type(action) == DirectMessageAction: self.totalDirectMessages += 1 - self.storage.log_direct_message_action(self.id, action.user, action.message) + self.storage.log_direct_message_action(self.id, self.session_phase, action.user, action.message) if type(action) == UnfollowAction: self.totalUnfollowed += 1 - self.storage.log_unfollow_action(self.id, action.user) + self.storage.log_unfollow_action(self.id, self.session_phase, action.user) self.storage.update_follow_status(action.user, do_i_follow_him=False) if type(action) == ScrapeAction: @@ -105,11 +123,11 @@ def add_action(self, action): else: self.totalScraped[action.source_name] += 1 - self.storage.log_scrape_action(self.id, action.user, action.source_type, action.source_name) + self.storage.log_scrape_action(self.id, self.session_phase, action.user, action.source_type, action.source_name) self.storage.publish_scrapped_account(action.user) if type(action) == FilterAction: - self.storage.log_filter_action(self.id, action.user) + self.storage.log_filter_action(self.id, self.session_phase, action.user) if type(action) == InteractAction: source_name = action.source_name if action.source_type is not None else self.SOURCE_NAME_TARGETS diff --git a/insomniac/sleeper.py b/insomniac/sleeper.py index 0cc0331..429a6f3 100644 --- a/insomniac/sleeper.py +++ b/insomniac/sleeper.py @@ -93,8 +93,9 @@ def _get_internet_speed(): s.download(threads=1) s.upload(threads=1) results_dict = s.results.dict() - except Exception: + except Exception as ex: print(COLOR_FAIL + "Failed to determine Internet speed, supposing it's zero" + COLOR_ENDC) + print(COLOR_FAIL + describe_exception(ex) + COLOR_ENDC) return SPEED_ZERO download_speed = results_dict['download'] diff --git a/insomniac/storage.py b/insomniac/storage.py index fa7f33b..5cf6eaa 100644 --- a/insomniac/storage.py +++ b/insomniac/storage.py @@ -1,9 +1,9 @@ +import insomniac.actions_types as insomniac_actions_types +import insomniac.db_models as insomniac_db from insomniac import db_models from insomniac.actions_types import TargetType -import insomniac.actions_types as insomniac_actions_types from insomniac.database_engine import * -from insomniac.db_models import get_ig_profile_by_profile_name, ProfileStatus -import insomniac.db_models as insomniac_db +from insomniac.db_models import get_ig_profile_by_profile_name from insomniac.utils import * FILENAME_WHITELIST = "whitelist.txt" @@ -105,8 +105,8 @@ def __init__(self, my_username, args): self.refilter_after = get_value(args.refilter_after, "Re-filter after {} hours", 168) if args.recheck_follow_status_after is not None: self.recheck_follow_status_after = get_value(args.recheck_follow_status_after, "Re-check follow status after {} hours", 168) - self.profiles_targets_list_from_parameters = args.__dict__.get('targets_list', []) - self.url_targets_list_from_parameters = args.__dict__.get('posts_urls_list', []) + self.profiles_targets_list_from_parameters = args.__dict__.get('targets_list', None) or [] # None may be there after --next-config-file + self.url_targets_list_from_parameters = args.__dict__.get('posts_urls_list', None) or [] # None may be there after --next-config-file whitelist_from_parameters = args.__dict__.get('whitelist_profiles', None) blacklist_from_parameters = args.__dict__.get('blacklist_profiles', None) @@ -159,7 +159,7 @@ def __init__(self, my_username, args): @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, + session_id = self.profile.start_session(app_id, app_version, args, ProfileStatus.VALID.value, followers_count, following_count) return session_id @@ -203,8 +203,8 @@ def is_profile_follows_me_by_cache(self, username): return self.profile.is_follow_me(username, hours=self.recheck_follow_status_after) is True @database_api - def is_new_follower(self, username): - return self.profile.is_follow_me(username) is None + 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): @@ -218,44 +218,44 @@ def update_follow_status(self, username, is_follow_me=None, do_i_follow_him=None self.profile.update_follow_status(username, is_follow_me, do_i_follow_him) @database_api - def log_get_profile_action(self, session_id, username): - self.profile.log_get_profile_action(session_id, username) + 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, username, source_type, source_name): - self.profile.log_like_action(session_id, username, source_type, source_name) + 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, username, source_type, source_name): - self.profile.log_follow_action(session_id, username, source_type, source_name) + 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, username, source_type, source_name): - self.profile.log_story_watch_action(session_id, username, source_type, source_name) + 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, username, comment, source_type, source_name): - self.profile.log_comment_action(session_id, username, comment, source_type, source_name) + 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, username, message): - self.profile.log_direct_message_action(session_id, username, message) + 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, username): - self.profile.log_unfollow_action(session_id, username) + 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, username, source_type, source_name): - self.profile.log_scrape_action(session_id, username, source_type, source_name) + 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, username): - self.profile.log_filter_action(session_id, username) + 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, profile_pic_url, name, description): - self.profile.log_change_profile_info_action(session_id, profile_pic_url, name, description) + 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): @@ -265,6 +265,14 @@ def publish_scrapped_account(self, username): 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 get_target(self, session_id): """ Get a target from args (users/posts) -> OR from targets file (users/posts) -> OR from scrapping (only users). @@ -277,18 +285,21 @@ def get_target(self, session_id): :returns: target and type """ + dropped_targets_count = 0 target, target_type = self._get_target() while target is not None: if self.profile.name == target \ or self.is_user_in_blacklist(target) \ or self.check_user_was_filtered(target) \ or self.check_user_was_interacted(target): - print(COLOR_OKGREEN + f"Target @{target} is dropped, going to the next target" + COLOR_ENDC) + dropped_targets_count += 1 # Mark this target as filtered, # so that profile.get_scrapped_profile_for_interaction() won't return it again. - self.log_filter_action(session_id, target) + self.log_filter_action(session_id, SessionPhase.TASK_LOGIC, target) target, target_type = self._get_target() continue + if dropped_targets_count > 0: + print(COLOR_OKGREEN + f"Dropped {dropped_targets_count} target(s)" + COLOR_ENDC) return target, target_type return None, None @@ -387,3 +398,16 @@ class FollowingStatus(Enum): NONE = 0 FOLLOWED = 1 UNFOLLOWED = 2 + + +@unique +class ProfileStatus(Enum): + VALID = "valid" + UNKNOWN = "unknown" + # TODO: request list of possible statuses from Jey + + +@unique +class SessionPhase(Enum): + WARMUP = "warmup" + TASK_LOGIC = "task" diff --git a/insomniac/tests/db_tests.py b/insomniac/tests/db_tests.py index 9c04cdf..01aeab8 100644 --- a/insomniac/tests/db_tests.py +++ b/insomniac/tests/db_tests.py @@ -1,11 +1,11 @@ import unittest -from datetime import timedelta from peewee import SqliteDatabase from insomniac import db_models from insomniac.actions_types import SourceType -from insomniac.db_models import get_ig_profile_by_profile_name, ProfileStatus, MODELS +from insomniac.db_models import get_ig_profile_by_profile_name, MODELS +from insomniac.storage import ProfileStatus, SessionPhase from insomniac.utils import * TEST_DATABASE_FILE = 'test.db' @@ -39,30 +39,30 @@ def job_interact(profile, session_id): assert profile.used_to_follow(username1) is False print(COLOR_BOLD + f"Check interaction counted for {username1} after a \"get profile\"" + COLOR_ENDC) - profile.log_get_profile_action(session_id, username1) + profile.log_get_profile_action(session_id, SessionPhase.TASK_LOGIC.value, username1) assert profile.is_interacted(username1) is True print(COLOR_BOLD + f"Check interaction counted for {username2} after a like" + COLOR_ENDC) - profile.log_like_action(session_id, username2, SourceType.BLOGGER.name, "some_blogger") + profile.log_like_action(session_id, SessionPhase.TASK_LOGIC.value, username2, SourceType.BLOGGER.name, "some_blogger") assert profile.is_interacted(username2) is True print(COLOR_BOLD + f"Check interaction counted for {username3} after a comment" + COLOR_ENDC) - profile.log_comment_action(session_id, username3, "Wow!", SourceType.BLOGGER.name, "some_blogger") + profile.log_comment_action(session_id, SessionPhase.TASK_LOGIC.value, username3, "Wow!", SourceType.BLOGGER.name, "some_blogger") assert profile.is_interacted(username3) is True print(COLOR_BOLD + f"Check interaction counted for {username4} after multiple actions" + COLOR_ENDC) - profile.log_get_profile_action(session_id, username4) - profile.log_like_action(session_id, username4, SourceType.BLOGGER.name, "some_blogger") - profile.log_like_action(session_id, username4, SourceType.HASHTAG.name, "some_hashtag") - profile.log_comment_action(session_id, username4, "Wow!", SourceType.PLACE.name, "some_place") + profile.log_get_profile_action(session_id, SessionPhase.TASK_LOGIC.value, username4) + profile.log_like_action(session_id, SessionPhase.TASK_LOGIC.value, username4, SourceType.BLOGGER.name, "some_blogger") + profile.log_like_action(session_id, SessionPhase.TASK_LOGIC.value, username4, SourceType.HASHTAG.name, "some_hashtag") + profile.log_comment_action(session_id, SessionPhase.TASK_LOGIC.value, username4, "Wow!", SourceType.PLACE.name, "some_place") assert profile.is_interacted(username4) is True print(COLOR_BOLD + f"Check interaction is NOT counted for {username5} after " f"follow / story watch / unfollow / filter actions" + COLOR_ENDC) - profile.log_follow_action(session_id, username5, None, None) - profile.log_story_watch_action(session_id, username5, None, None) - profile.log_unfollow_action(session_id, username5) - profile.log_filter_action(session_id, username5) + profile.log_follow_action(session_id, SessionPhase.TASK_LOGIC.value, username5, None, None) + profile.log_story_watch_action(session_id, SessionPhase.TASK_LOGIC.value, username5, None, None) + profile.log_unfollow_action(session_id, SessionPhase.TASK_LOGIC.value, username5) + profile.log_filter_action(session_id, SessionPhase.TASK_LOGIC.value, username5) assert profile.is_interacted(username5) is False print(COLOR_BOLD + f"Check \"used to follow\" is True after following" + COLOR_ENDC) @@ -88,7 +88,7 @@ def job_interact(profile, _): def job_interact(profile, session_id): print(COLOR_BOLD + f"Check interaction is not counted for {username1} if was too long ago" + COLOR_ENDC) - profile.log_like_action(session_id, username1, SourceType.BLOGGER.name, "some_blogger", timestamp=datetime.now()-timedelta(hours=48)) + profile.log_like_action(session_id, SessionPhase.TASK_LOGIC.value, username1, SourceType.BLOGGER.name, "some_blogger", timestamp=datetime.now()-timedelta(hours=48)) assert profile.is_interacted(username1) is True assert profile.is_interacted(username1, hours=24) is False self._run_inside_session(my_account2, job_interact) @@ -104,11 +104,11 @@ def job_interact(profile, session_id): assert profile.is_filtered(username1) is False print(COLOR_BOLD + f"Check that filter works" + COLOR_ENDC) - profile.log_filter_action(session_id, username1) + profile.log_filter_action(session_id, SessionPhase.TASK_LOGIC.value, username1) assert profile.is_filtered(username1) is True print(COLOR_BOLD + f"Check that filter NOT works if filtered too long ago" + COLOR_ENDC) - profile.log_filter_action(session_id, username2, timestamp=datetime.now()-timedelta(hours=48)) + profile.log_filter_action(session_id, SessionPhase.TASK_LOGIC.value, username2, timestamp=datetime.now()-timedelta(hours=48)) assert profile.is_filtered(username2) is True assert profile.is_filtered(username2, hours=24) is False self._run_inside_session(my_account1, job_interact) @@ -189,43 +189,43 @@ def job_real(profile, session_id): assert username == username1 print(COLOR_BOLD + "Real: check account is excluded after \"get profile action\"" + COLOR_ENDC) - profile.log_get_profile_action(session_id, username1) + profile.log_get_profile_action(session_id, SessionPhase.TASK_LOGIC.value, username1) username = profile.get_scrapped_profile_for_interaction() assert username == username2 print(COLOR_BOLD + "Real: check account is excluded after like-interaction" + COLOR_ENDC) - profile.log_like_action(session_id, username2, SourceType.BLOGGER.name, "some_blogger") + profile.log_like_action(session_id, SessionPhase.TASK_LOGIC.value, username2, SourceType.BLOGGER.name, "some_blogger") username = profile.get_scrapped_profile_for_interaction() assert username == username3 print(COLOR_BOLD + "Real: check account is excluded after follow-interaction" + COLOR_ENDC) - profile.log_follow_action(session_id, username3, SourceType.HASHTAG.name, "some_hashtag") + profile.log_follow_action(session_id, SessionPhase.TASK_LOGIC.value, username3, SourceType.HASHTAG.name, "some_hashtag") username = profile.get_scrapped_profile_for_interaction() assert username == username4 print(COLOR_BOLD + "Real: check account is excluded after story-watch-interaction" + COLOR_ENDC) - profile.log_story_watch_action(session_id, username4, SourceType.BLOGGER.name, "some_blogger") + profile.log_story_watch_action(session_id, SessionPhase.TASK_LOGIC.value, username4, SourceType.BLOGGER.name, "some_blogger") username = profile.get_scrapped_profile_for_interaction() assert username == username5 print(COLOR_BOLD + "Real: check account is excluded after comment-interaction" + COLOR_ENDC) - profile.log_comment_action(session_id, username5, "Wow!", SourceType.PLACE.name, "some_place") + profile.log_comment_action(session_id, SessionPhase.TASK_LOGIC.value, username5, "Wow!", SourceType.PLACE.name, "some_place") username = profile.get_scrapped_profile_for_interaction() assert username == username6 print(COLOR_BOLD + "Real: check account is excluded after being filtered" + COLOR_ENDC) - profile.log_filter_action(session_id, username6) + profile.log_filter_action(session_id, SessionPhase.TASK_LOGIC.value, username6) username = profile.get_scrapped_profile_for_interaction() assert username == username7 print(COLOR_BOLD + "Real: check account is NOT excluded after unfollow / change profile info actions" + COLOR_ENDC) - profile.log_unfollow_action(session_id, username4) - profile.log_change_profile_info_action(session_id, "some_url", "some_name", "some_description") + profile.log_unfollow_action(session_id, SessionPhase.TASK_LOGIC.value, username4) + profile.log_change_profile_info_action(session_id, SessionPhase.TASK_LOGIC.value, "some_url", "some_name", "some_description") username = profile.get_scrapped_profile_for_interaction() assert username == username7 - profile.log_like_action(session_id, username7, SourceType.BLOGGER.name, "some_blogger") - profile.log_like_action(session_id, username7, SourceType.BLOGGER.name, "some_blogger") # double action check + profile.log_like_action(session_id, SessionPhase.TASK_LOGIC.value, username7, SourceType.BLOGGER.name, "some_blogger") + profile.log_like_action(session_id, SessionPhase.TASK_LOGIC.value, username7, SourceType.BLOGGER.name, "some_blogger") # double action check print(COLOR_BOLD + "Real: check scraped accounts count is correct AFTER interaction" + COLOR_ENDC) assert profile.count_scrapped_profiles_for_interaction() == 0 @@ -266,11 +266,11 @@ def job_real(profile, _): def job_real(profile, session_id): print(f"Real: interact with {username8} from another account") - profile.log_get_profile_action(session_id, username8) - profile.log_like_action(session_id, username8, SourceType.BLOGGER.name, "some_blogger") - profile.log_follow_action(session_id, username8, SourceType.HASHTAG.name, "some_hashtag") - profile.log_story_watch_action(session_id, username8, SourceType.BLOGGER.name, "some_blogger") - profile.log_comment_action(session_id, username8, "Wow!", SourceType.PLACE.name, "some_place") + profile.log_get_profile_action(session_id, SessionPhase.TASK_LOGIC.value, username8) + profile.log_like_action(session_id, SessionPhase.TASK_LOGIC.value, username8, SourceType.BLOGGER.name, "some_blogger") + profile.log_follow_action(session_id, SessionPhase.TASK_LOGIC.value, username8, SourceType.HASHTAG.name, "some_hashtag") + profile.log_story_watch_action(session_id, SessionPhase.TASK_LOGIC.value, username8, SourceType.BLOGGER.name, "some_blogger") + profile.log_comment_action(session_id, SessionPhase.TASK_LOGIC.value, username8, "Wow!", SourceType.PLACE.name, "some_place") self._run_inside_session("some_another_account", job_real) def job_real(profile, _): @@ -286,7 +286,7 @@ def tearDown(self): def _run_inside_session(self, username, action): print(f"Starting session for {username}") profile = get_ig_profile_by_profile_name(username) - session_id = profile.start_session(None, "", "", ProfileStatus.VALID, 2200, 500) + session_id = profile.start_session(None, "", "", ProfileStatus.VALID.value, 2200, 500) print(f"session_id = {session_id}") action(profile, session_id) print(f"Ending session for {username}") diff --git a/insomniac/typewriter.py b/insomniac/typewriter.py index c2d232e..c6344b0 100644 --- a/insomniac/typewriter.py +++ b/insomniac/typewriter.py @@ -1,5 +1,3 @@ -import base64 - import insomniac from insomniac.utils import * diff --git a/insomniac/utils.py b/insomniac/utils.py index df117de..f73f405 100644 --- a/insomniac/utils.py +++ b/insomniac/utils.py @@ -51,6 +51,10 @@ def get_instagram_version(device_id, app_id): return version +def versiontuple(v): + return tuple(map(int, (v.split(".")))) + + def get_connected_devices_adb_ids(): stream = os.popen('adb devices') output = stream.read() @@ -147,7 +151,7 @@ def close_instagram(device_id, app_id): f" shell am force-stop {app_id}").close() # Press HOME to leave a possible state of opened system dialog(s) os.popen("adb" + ("" if device_id is None else " -s " + device_id) + - f" shell input keyevent 4").close() + f" shell input keyevent 3").close() def clear_instagram_data(device_id, app_id): @@ -159,9 +163,9 @@ def clear_instagram_data(device_id, app_id): def save_crash(device, ex=None): global print_log - device.wake_up() - try: + device.wake_up() + directory_name = "Crash-" + datetime.now().strftime("%Y-%m-%d-%H-%M-%S") try: os.makedirs(os.path.join("crashes", directory_name), exist_ok=False) @@ -226,7 +230,15 @@ def wrapper(*args, **kwargs): return wrapper -def get_value(count, name, default, max_count=None): +def get_value(count: str, name: str, default: int, max_count=None): + return _get_value(count, name, default, max_count, is_float=False) + + +def get_float_value(count: str, name: str, default: float, max_count=None): + return _get_value(count, name, default, max_count, is_float=True) + + +def _get_value(count, name, default, max_count, is_float): def print_error(): print(COLOR_FAIL + name.format(default) + f". Using default value instead of \"{count}\", because it must be " "either a number (e.g. 2) or a range (e.g. 2-4)." + COLOR_ENDC) @@ -237,15 +249,16 @@ def print_error(): print_error() elif len(parts) == 1: try: - value = int(count) - print(COLOR_BOLD + name.format(value) + COLOR_ENDC) + value = float(count) if is_float else int(count) + print(COLOR_BOLD + name.format(value, "%.2f") + COLOR_ENDC) except ValueError: value = default print_error() elif len(parts) == 2: try: - value = randint(int(parts[0]), int(parts[1])) - print(COLOR_BOLD + name.format(value) + COLOR_ENDC) + value = random.uniform(float(parts[0]), float(parts[1])) if is_float \ + else randint(int(parts[0]), int(parts[1])) + print(COLOR_BOLD + name.format(value, "%.2f") + COLOR_ENDC) except ValueError: value = default print_error() @@ -288,10 +301,26 @@ def print_error(): return value -def get_count_of_nums_in_str(string): +def get_from_to_timestamps_by_hours(hours): + """Returns a tuple of two timestamps: (given number of hours before; current time)""" + + return get_from_to_timestamps_by_minutes(hours*60) + + +def get_from_to_timestamps_by_minutes(minutes): + """Returns a tuple of two timestamps: (given number of minutes before; current time)""" + + time_to = datetime.now().timestamp() + delta = timedelta(minutes=minutes).total_seconds() + time_from = time_to - delta + + return time_from, time_to + + +def get_count_of_nums_in_str(str_to_check): count = 0 for i in range(0, 10): - count += string.count(str(i)) + count += str_to_check.count(str(i)) return count @@ -300,8 +329,8 @@ def get_random_string(length): return ''.join(random.choices(string.ascii_uppercase + string.digits, k=length)) -def describe_exception(ex): - trace = ''.join(traceback.format_exception(etype=type(ex), value=ex, tb=ex.__traceback__)) +def describe_exception(ex, with_stacktrace=True): + trace = ''.join(traceback.format_exception(etype=type(ex), value=ex, tb=ex.__traceback__)) if with_stacktrace else '' description = f"Error - {str(ex)}\n{trace}" return description @@ -340,7 +369,7 @@ def _get_logs_dir_name(): def _get_log_file_name(logs_directory_name): os.makedirs(os.path.join(logs_directory_name), exist_ok=True) curr_time = datetime.now().strftime("%Y-%m-%d-%H-%M-%S") - log_name = f"insomniac_log-{curr_time}{'-'+insomniac_globals.execution_id if insomniac_globals.execution_id != '' else ''}.log" + log_name = f"{insomniac_globals.executable_name}_log-{curr_time}{'-'+insomniac_globals.execution_id if insomniac_globals.execution_id != '' else ''}.log" log_path = os.path.join(logs_directory_name, log_name) return log_path @@ -374,6 +403,7 @@ class Logger(object): is_log_initiated = False def __init__(self): + sys.stdout.reconfigure(encoding='utf-8') self.wrapped_stdout = AnsiToWin32(sys.stdout) self.terminal = self.wrapped_stdout.stream self.log = None diff --git a/insomniac/views.py b/insomniac/views.py index d570e8b..d6ec972 100644 --- a/insomniac/views.py +++ b/insomniac/views.py @@ -5,11 +5,14 @@ from insomniac.actions_types import GetProfileAction from insomniac.counters_parser import parse from insomniac.device_facade import DeviceFacade +from insomniac.globals import do_location_permission_dialog_checks from insomniac.scroll_end_detector import ScrollEndDetector from insomniac.sleeper import sleeper from insomniac.utils import * TEXTVIEW_OR_BUTTON_REGEX = 'android.widget.TextView|android.widget.Button' +VIEW_OR_VIEWGROUP_REGEX = 'android.view.View|android.view.ViewGroup' +RECYCLERVIEW_OR_LISTVIEW_REGEX = 'androidx.recyclerview.widget.RecyclerView|android.widget.ListView' def case_insensitive_re(str_list): @@ -47,6 +50,7 @@ class ProfileTabs(Enum): class InstagramView: ACTION_BAR_TITLE_ID = "{0}:id/action_bar_title" + USERNAME_ALLOWED_SYMBOLS_REGEX = re.compile(r'[a-z0-9._-]+') def __init__(self, device: DeviceFacade): self.device = device @@ -69,19 +73,16 @@ def press_back_arrow(self) -> 'InstagramView': button_back.click() else: print(COLOR_FAIL + f"Cannot find back arrow in {self.__class__.__name__}, press hardware back" + COLOR_ENDC) - self.device.back() + if not self.device.back(): + raise RuntimeError("Unexpected app state: want to go back but can't") return self.on_back_pressed() def on_back_pressed(self) -> 'InstagramView': # Override this method to return a view after press_back_arrow() - pass + return self - def is_block_dialog_present(self) -> bool: - block_dialog_v1 = self.device.find(resourceId=f'{self.device.app_id}:id/dialog_root_view', - className='android.widget.FrameLayout') - block_dialog_v2 = self.device.find(resourceId=f'{self.device.app_id}:id/dialog_container', - className='android.view.ViewGroup') - return block_dialog_v1.exists(quick=True) or block_dialog_v2.exists(quick=True) + def format_username(self, raw_text): + return ''.join(re.findall(self.USERNAME_ALLOWED_SYMBOLS_REGEX, raw_text)) class TabBarView(InstagramView): @@ -144,6 +145,10 @@ def navigate_to(self, tab: TabBarTabs): print_debug(f"Navigate to {tab_name}") button = None tab_bar_view = self._get_tab_bar() + + # There may be no TabBarView if Instagram was opened via a deeplink. Then we have to clear the backstack. + self._clear_backstack() + if tab == TabBarTabs.HOME: button = tab_bar_view.child( descriptionMatches=case_insensitive_re(TabBarView.HOME_CONTENT_DESC) @@ -184,7 +189,7 @@ def navigate_to(self, tab: TabBarTabs): if self._is_correct_tab_opened(tab): return else: - print(COLOR_FAIL + f"{tab_name} tab is not opened, will try again." + COLOR_ENDC) + print(COLOR_OKGREEN + f"{tab_name} tab is not opened, will try again." + COLOR_ENDC) sleeper.random_sleep() else: seconds_left = timer.get_seconds_left() @@ -197,6 +202,16 @@ def navigate_to(self, tab: TabBarTabs): raise LanguageNotEnglishException() + def _clear_backstack(self): + is_message_printed = False + while not self.is_visible(): + if not is_message_printed: + print(COLOR_OKGREEN + "Clearing the back stack..." + COLOR_ENDC) + is_message_printed = True + self.press_back_arrow() + # On fresh apps there may be a location request window after a backpress + DialogView(self.device).close_location_access_dialog_if_visible() + def _is_correct_tab_opened(self, tab: TabBarTabs) -> bool: if tab == TabBarTabs.HOME: return HomeView(self.device).is_visible() @@ -288,11 +303,11 @@ def create_instance(device): class HomeView(InstagramView): - LOGO_ID = '{0}:id/action_bar_textview_custom_title_container' + LOGO_ID_REGEX = '{0}:id/(action_bar_textview_custom_title_container|action_bar_textview_title_container)' LOGO_CLASS_NAME = 'android.widget.FrameLayout' def is_visible(self) -> bool: - return self.device.find(resourceId=self.LOGO_ID.format(self.device.app_id), + return self.device.find(resourceIdMatches=self.LOGO_ID_REGEX.format(self.device.app_id), className=self.LOGO_CLASS_NAME).exists() def navigate_to_search(self): @@ -525,10 +540,7 @@ def navigate_to_place(self, place): return PlacesView(self.device) def _handle_permission_request(self): - dialog_view = DialogView(self.device) - if dialog_view.is_visible(): - print("Deny location permission request") - dialog_view.click_deny_location_access() + DialogView(self.device).close_location_access_dialog_if_visible() class PostsViewList(InstagramView): @@ -602,6 +614,9 @@ def setLanguage(self, language: str): class AccountView(InstagramView): + LIST_ID_REGEX = '{0}:id/recycler_view|android:id/list' + LIST_CLASSNAME_REGEX = RECYCLERVIEW_OR_LISTVIEW_REGEX + def navigate_to_language(self): print_debug("Navigate to Language") button = self.device.find( @@ -613,6 +628,34 @@ def navigate_to_language(self): return LanguageView(self.device) + def switch_to_business_account(self): + recycler_view = self.device.find(resourceIdMatches=self.LIST_ID_REGEX.format(self.device.app_id), + classNameMatches=self.LIST_CLASSNAME_REGEX) + recycler_view.scroll(DeviceFacade.Direction.BOTTOM) + + switch_button = self.device.find(textMatches=case_insensitive_re("Switch to Professional Account")) + switch_button.click() + radio_button = self.device.find(className="android.widget.RadioButton") + while not radio_button.exists(quick=True): + continue_button = self.device.find(textMatches=case_insensitive_re("Continue")) + continue_button.click() + radio_button.click() + done_button = self.device.find(textMatches=case_insensitive_re("Done")) + done_button.click() + sleeper.random_sleep(multiplier=2.0) + + DialogView(self.device).click_ok() + + business_account_item = self.device.find(textMatches=case_insensitive_re("Business")) + business_account_item.click() + + next_or_skip_button = self.device.find(textMatches=case_insensitive_re("Next|Skip")) + close_button = self.device.find(resourceId=f"{self.device.app_id}:id/action_bar_button_action", + descriptionMatches=case_insensitive_re("Close")) + while not close_button.exists(quick=True): + next_or_skip_button.click() + close_button.click() + class SettingsView(InstagramView): SETTINGS_LIST_ID_REGEX = 'android:id/list|{0}:id/recycler_view' @@ -722,7 +765,11 @@ def navigate_to_settings(self): """ print_debug("Navigate to Settings") settings_button = self.device.find(resourceId=f'{self.device.app_id}:id/menu_settings_row', - className='android.widget.TextView') + classNameMatches=TEXTVIEW_OR_BUTTON_REGEX) + if not settings_button.exists(): + # Just take the first item + settings_button = self.device.find(resourceIdMatches=f'{self.device.app_id}:id/menu_option_text', + classNameMatches=TEXTVIEW_OR_BUTTON_REGEX) settings_button.click() return SettingsView(self.device) @@ -782,7 +829,7 @@ def get_author_name(self) -> Optional[str]: className=self.TEXT_AUTHOR_NAME_CLASSNAME ) try: - return text_author_name.get_text() + return self.format_username(text_author_name.get_text()) except DeviceFacade.JsonRpcError: print(COLOR_FAIL + "Cannot read post author's name" + COLOR_ENDC) return None @@ -828,7 +875,7 @@ class PostsGridView(InstagramView): POSTS_GRID_RESOURCE_ID = '{0}:id/recycler_view' POSTS_GRID_CLASS_NAME = 'androidx.recyclerview.widget.RecyclerView|android.view.View' - POST_CLASS_NAME = 'android.widget.ImageView|android.widget.Button' + POST_CLASS_NAME_REGEX = 'android.widget.ImageView|android.widget.Button' def open_random_post(self) -> Optional['PostsViewList']: # Scroll down several times to pick random post @@ -844,7 +891,7 @@ def open_random_post(self) -> Optional['PostsViewList']: available_posts_coords = [] print("Choosing a random post from those on the screen") for post_view in posts_grid.child(resourceId=f'{self.device.app_id}:id/image_button', - classNameMatches=self.POST_CLASS_NAME): + classNameMatches=self.POST_CLASS_NAME_REGEX): if not ActionBarView.is_in_interaction_rect(post_view): continue bounds = post_view.get_bounds() @@ -884,7 +931,6 @@ class ProfileView(InstagramView): FOLLOWERS_BUTTON_ID_REGEX = '{0}:id/row_profile_header_followers_container|{1}:id/row_profile_header_container_followers' FOLLOWING_BUTTON_ID_REGEX = '{0}:id/row_profile_header_following_container|{1}:id/row_profile_header_container_following' MESSAGE_BUTTON_CLASS_NAME_REGEX = TEXTVIEW_OR_BUTTON_REGEX - USERNAME_REGEX = re.compile(r'[a-z0-9._-]+') def __init__(self, device: DeviceFacade, is_own_profile=False): super().__init__(device) @@ -892,7 +938,7 @@ def __init__(self, device: DeviceFacade, is_own_profile=False): def is_visible(self): return self.device.find(resourceId=f"{self.device.app_id}:id/row_profile_header", - className="android.view.ViewGroup").exists(quick=True) + classNameMatches=VIEW_OR_VIEWGROUP_REGEX).exists(quick=True) def refresh(self): re_case_insensitive = case_insensitive_re( @@ -973,11 +1019,11 @@ def _get_action_bar_title_btn(self): def get_username(self): title_view = self._get_action_bar_title_btn() if title_view.exists(): - username = title_view.get_text().strip() - if self.USERNAME_REGEX.fullmatch(username) is not None: + username = self.format_username(title_view.get_text()) + if len(username) > 0: return username else: - print(COLOR_FAIL + f"Username doesn't look like real username: {username}" + COLOR_ENDC) + print(COLOR_FAIL + f"Cannot parse username" + COLOR_ENDC) return None print(COLOR_FAIL + "Cannot get username" + COLOR_ENDC) @@ -1132,11 +1178,19 @@ def get_full_name(self): return fullname def has_business_category(self): - business_category_view = self.device.find( - resourceId=f'{self.device.app_id}:id/profile_header_business_category', - className='android.widget.TextView' - ) - return business_category_view.exists() + if self.is_own_profile: + insights_button = self.device.find( + resourceId=f'{self.device.app_id}:id/button_text', + classNameMatches=TEXTVIEW_OR_BUTTON_REGEX, + textMatches=case_insensitive_re("Insights") + ) + return insights_button.exists() + else: + business_category_view = self.device.find( + resourceId=f'{self.device.app_id}:id/profile_header_business_category', + className='android.widget.TextView' + ) + return business_category_view.exists() def is_private_account(self): private_profile_view = self.device.find( @@ -1402,24 +1456,29 @@ class DialogView(InstagramView): UNFOLLOW_BUTTON_TEXT_REGEX = case_insensitive_re("Unfollow") LOCATION_DENY_BUTTON_ID_REGEX = '.*?:id/permission_deny.*?' LOCATION_DENY_BUTTON_CLASS_NAME_REGEX = TEXTVIEW_OR_BUTTON_REGEX + LOCATION_DENY_AND_DONT_ASK_AGAIN_BUTTON_ID_REGEX = '.*?:id/permission_deny_and_dont_ask_again.*?' + LOCATION_DENY_AND_DONT_ASK_AGAIN_BUTTON_CLASS_NAME_REGEX = TEXTVIEW_OR_BUTTON_REGEX LOCATION_CHECKBOX_ID_REGEX = '.*?:id/do_not_ask_checkbox' + CONTINUE_BUTTON_ID = '{0}:id/primary_button' + CONTINUE_BUTTON_CLASS_NAME = 'android.widget.Button' + CONTINUE_BUTTTON_TEXT_REGEX = case_insensitive_re("Continue") + OK_BUTTON_ID = '{0}:id/primary_button' + OK_BUTTON_CLASS_NAME = 'android.widget.Button' + OK_BUTTTON_TEXT_REGEX = case_insensitive_re("OK") + CLOSE_APP_ID = 'android:id/aerr_close' + CLOSE_APP_CLASS_NAME = 'android.widget.Button' + CLOSE_APP_TEXT_REGEX = case_insensitive_re("Close app") def is_visible(self) -> bool: - dialog_v1 = self.device.find(resourceId=f'{self.device.app_id}:id/bottom_sheet_container', + dialog_v1 = self.device.find(resourceIdMatches=f'{self.device.app_id}:id/(bottom_sheet_container|dialog_root_view|content)', className='android.widget.FrameLayout') - dialog_v2 = self.device.find(resourceId=f'{self.device.app_id}:id/dialog_root_view', - className='android.widget.FrameLayout') - dialog_v3 = self.device.find(resourceId=f'{self.device.app_id}:id/dialog_container', + dialog_v2 = self.device.find(resourceId=f'{self.device.app_id}:id/dialog_container', classNameMatches='android.view.ViewGroup|android.view.View') - dialog_v4 = self.device.find(resourceId=f'{self.device.app_id}:id/content', - className='android.widget.FrameLayout') - dialog_v5 = self.device.find(resourceIdMatches='com.android.(permissioncontroller|packageinstaller):id/.*?', + dialog_v3 = self.device.find(resourceIdMatches='com.android.(permissioncontroller|packageinstaller):id/.*?', className='android.widget.LinearLayout') return dialog_v1.exists(quick=True) \ or dialog_v2.exists(quick=True) \ - or dialog_v3.exists(quick=True) \ - or dialog_v4.exists(quick=True) \ - or dialog_v5.exists(quick=True) + or dialog_v3.exists(quick=True) def click_unfollow(self) -> bool: unfollow_button = self.device.find( @@ -1432,18 +1491,62 @@ def click_unfollow(self) -> bool: return True return False - def click_deny_location_access(self) -> bool: + def close_not_responding_dialog_if_visible(self): + if self._click_close_app(): + print(COLOR_FAIL + "App crashed! Closing \"Isn't responding\" dialog." + COLOR_ENDC) + save_crash(self.device) + + def _click_close_app(self) -> bool: + close_app_button = self.device.find(resourceId=self.CLOSE_APP_ID.format(self.device.app_id), + className=self.CLOSE_APP_CLASS_NAME, + textMatches=self.CLOSE_APP_TEXT_REGEX) + if close_app_button.exists(): + close_app_button.click() + return True + return False + + def close_location_access_dialog_if_visible(self): + if not do_location_permission_dialog_checks: + return + + if self.is_visible(): + if self._click_deny_location_access(): + print("Deny location permission request") + def _click_deny_location_access(self) -> bool: + deny_and_dont_ask_button = self.device.find(resourceIdMatches=self.LOCATION_DENY_AND_DONT_ASK_AGAIN_BUTTON_ID_REGEX, + classNameMatches=self.LOCATION_DENY_AND_DONT_ASK_AGAIN_BUTTON_CLASS_NAME_REGEX) deny_button = self.device.find(resourceIdMatches=self.LOCATION_DENY_BUTTON_ID_REGEX, classNameMatches=self.LOCATION_DENY_BUTTON_CLASS_NAME_REGEX) checkbox = self.device.find(resourceIdMatches=self.LOCATION_CHECKBOX_ID_REGEX, className="android.widget.CheckBox") checkbox.click(ignore_if_missing=True) + if deny_and_dont_ask_button.exists(): + deny_and_dont_ask_button.click() + return True if deny_button.exists(): deny_button.click() return True return False + def click_continue(self) -> bool: + continue_button = self.device.find(resourceId=self.CONTINUE_BUTTON_ID.format(self.device.app_id), + className=self.CONTINUE_BUTTON_CLASS_NAME, + textMatches=self.CONTINUE_BUTTTON_TEXT_REGEX) + if continue_button.exists(): + continue_button.click() + return True + return False + + def click_ok(self) -> bool: + continue_button = self.device.find(resourceId=self.OK_BUTTON_ID.format(self.device.app_id), + className=self.OK_BUTTON_CLASS_NAME, + textMatches=self.OK_BUTTTON_TEXT_REGEX) + if continue_button.exists(): + continue_button.click() + return True + return False + class LanguageNotEnglishException(Exception): pass