diff --git a/.gitignore b/.gitignore index b79c0d8..c5316d9 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ /.idea interacted_users.* scrapped_users.json +filtered_users.json sessions.json targets.txt *.pyc diff --git a/README.md b/README.md index 7cb9272..bfbbf31 100644 --- a/README.md +++ b/README.md @@ -72,14 +72,14 @@ _IMPORTANT: if you previously used v2.x.x, then insomniac.py file will conflict 5. Type `adb devices` in terminal. It will display attached devices. There should be exactly one device. Then run the script (it works on Python 3): 6. Open Terminal / Command Prompt in the folder with downloaded [start.py](https://raw.githubusercontent.com/alexal1/Insomniac/master/start.py) (or type `cd `) and run ``` -python3 start.py --interact @natgeo +python3 start.py --interact @natgeo-followers ``` Make sure that the screen is turned on and device is unblocked. You don't have to open Instagram app, script opens it and closes when it's finished. Just make sure that Instagram app is installed. If everything's fine, script will open `@natgeo`'s followers and like their posts. ### Usage example Say you have a travel blog. Then you may want to use such setup: ``` -python3 start.py --interact @natgeo amazingtrips --interactions-count 20-30 --likes-count 1-2 --follow-percentage 80 --repeat 160-220 +python3 start.py --interact @natgeo-followers amazingtrips-recent-likers --interactions-count 20-30 --likes-count 1-2 --follow-percentage 80 --repeat 160-220 ``` Or just download a config file [interact.json](https://raw.githubusercontent.com/alexal1/Insomniac/master/config-examples/interact.json) and put it near `start.py`. Then run: ``` @@ -129,25 +129,29 @@ Pull requests are welcome! Any feature you implement will be included in the Ins ### Filtering You may want to ignore mass-followers (e.g. > 1000 followings) because they are most likely interested only in growing their audience. Or ignore too popular accounts (e.g. > 5000 followers) because they won't notice you. You can do this (and more) by using `filter.json` file. List of available parameters: -| Parameter | Value | Description | -| ------------------------------- | ----------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------| -| `skip_business` | `true/false` | skip business accounts if true | -| `skip_non_business` | `true/false` | skip non-business accounts if true | -| `min_followers` | 100 | skip accounts with less followers than given value | -| `max_followers` | 5000 | skip accounts with more followers than given value | -| `min_followings` | 10 | skip accounts with less followings than given value | -| `max_followings` | 1000 | skip accounts with more followings than given value | -| `min_potency_ratio` | 1 | skip accounts with ratio (followers/followings) less than given value (decimal values can be used too) | -| `max_potency_ratio` | 1 | skip accounts with ratio (followers/followings) higher than given value (decimal values can be used too) | -| `privacy_relation` ` | `"only_public"` / `"only_private"` / `"private_and_public"` | choose with accounts of which type you want to interact, `"only_public"` by default | -| `min_posts` | 7 | minimum posts in profile in order to interact | -| `max_digits_in_profile_name` | 4 | maximum amount of digits in profile name (more than that - won't be interacted) | -| `skip_profiles_without_stories` | `true/false` | skip accounts that doesnt have updated story (from last 24 hours) | -| `blacklist_words` | `["word1", "word2", "word3", ...]` | skip accounts that contains one of the words in the list in the profile biography | -| `mandatory_words` | `["word1", "word2", "word3", ...]` | skip accounts that doesn't have one of the words in the list in the profile biography | -| `specific_alphabet` | `["LATIN", "ARABIC", "GREEK", "HEBREW", ...]` | skip accounts that contains text in their biography/username which different than the provided alphabet list | - -Please read detailed explanation and instructions how to use it [in this post](https://www.patreon.com/posts/43362005). +| Parameter | Value | Description | +| --------------------------------- | ----------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------| +| `skip_business` | `true/false` | skip business accounts if true | +| `skip_non_business` | `true/false` | skip non-business accounts if true | +| `min_followers` | 100 | skip accounts with less followers than given value | +| `max_followers` | 5000 | skip accounts with more followers than given value | +| `min_followings` | 10 | skip accounts with less followings than given value | +| `max_followings` | 1000 | skip accounts with more followings than given value | +| `min_potency_ratio` | 1 | skip accounts with ratio (followers/followings) less than given value (decimal values can be used too) | +| `max_potency_ratio` | 1 | skip accounts with ratio (followers/followings) higher than given value (decimal values can be used too) | +| `privacy_relation` ` | `"only_public"` / `"only_private"` / `"private_and_public"` | choose with accounts of which type you want to interact, `"only_public"` by default | +| `min_posts` | 7 | minimum posts in profile in order to interact | +| `max_digits_in_profile_name` | 4 | maximum amount of digits in profile name (more than that - won't be interacted) | +| `skip_profiles_without_stories` | `true/false` | skip accounts that doesnt have updated story (from last 24 hours) | +| `blacklist_words` | `["word1", "word2", "word3", ...]` | skip accounts that contains one of the words in the list in the profile biography | +| `mandatory_words` | `["word1", "word2", "word3", ...]` | skip accounts that doesn't have one of the words in the list in the profile biography | +| `specific_alphabet` | `["LATIN", "ARABIC", "GREEK", "HEBREW", ...]` | skip accounts that contains text in their biography/username which different than the provided alphabet list | +| `skip_already_following_profiles` | `true/false` | skip accounts that your profile already followed, even if not followed by the bot | + + +Please read detailed explanation and instructions how to use filter in [this Patreon post](https://www.patreon.com/posts/43362005). + +_IMPORTANT: Please use_ `--total-get-profile-limit 500` _(or some other value) when using filter. You may get a soft ban because of opening and closing too large amount of Instagram profiles._ ### Whitelist and Blacklist **Whitelist** – affects `--remove-mass-followers`, `--unfollow` and all other unfollow actions. Users from this list will _never_ be removed from your followers or unfollowed. @@ -156,6 +160,11 @@ Please read detailed explanation and instructions how to use it [in this post](h Go to Insomniac folder and create a folder named as your Instagram nickname (or open an existing one, as Insomniac creates such folder when launched). Create there a file `whitelist.txt` or `blacklist.txt` (or both of them). Write usernames in these files, one username per line, no `@`'s, no commas. Don't forget to save. That's it! +### Targets Interaction +Go to Insomniac folder and create a folder named as your Instagram nickname (or open an existing one, as Insomniac creates such folder when launched). Create there a file `targets.txt`. Write usernames in these files, one username per line, no `@`'s, no commas. Don't forget to save. + +Run Insomniac with --interact-targets parameter, and the session will be targeted on those specific profiles form the `targets.txt` file. + ### Analytics There also is an analytics tool for this bot. It is a script that builds a report in PDF format. The report contains account's followers growth graphs for different periods. Liking, following and unfollowing actions' amounts are on the same axis to determine bot effectiveness. The report also contains stats of sessions length for different configurations that you've used. All data is taken from `sessions.json` file that's generated during bot's execution. diff --git a/config-examples-extra/interact.json b/config-examples-extra/interact.json index 223b71f..058985e 100644 --- a/config-examples-extra/interact.json +++ b/config-examples-extra/interact.json @@ -11,6 +11,12 @@ "value": "True", "description" : "add this flag to use an old version of uiautomator. Use it only if you experience problems with the default version" }, + { + "parameter-name": "no_speed_check", + "enabled": false, + "value": "True", + "description" : "skip internet speed check at start" + }, { "parameter-name": "repeat", "enabled": true, @@ -29,6 +35,12 @@ "value": "1-2", "description" : "number of likes for each interacted user, 2 by default. It can be a number (e.g. 2) or a range (e.g. 2-4)" }, + { + "parameter-name": "likes_percentage", + "enabled": true, + "value": "80", + "description" : "likes given percentage of interacted users, 100 by default" + }, { "parameter-name": "stories_count", "enabled": true, @@ -45,11 +57,19 @@ "parameter-name": "interact", "enabled": true, "value": [ - "@natgeo", - "amazingtrips" + "@natgeo-followers", + "@natgeo-following", + "amazingtrips-top-likers", + "amazingtrips-recent-likers" ], "description" : "list of hashtags and usernames. Usernames should start with \\\"@\\\" symbol. The script will interact with hashtags\\' posts likers and with users\\' followers" }, + { + "parameter-name": "reinteract_after", + "enabled": false, + "value": "72-96", + "description" : "set a time (in hours) to wait before re-interact with an already interacted profile, disabled by default (won't interact again). It can be a number (e.g. 48) or a range (e.g. 50-80)" + }, { "parameter-name": "interaction_users_amount", "enabled": true, @@ -68,6 +88,12 @@ "value": "150", "description" : "minimum amount of followings, after reaching this amount unfollow stops" }, + { + "parameter-name": "max_following", + "enabled": false, + "value": "150", + "description" : "maximum amount of followings, after reaching this amount follow stops. disabled by default" + }, { "parameter-name": "unfollow_non_followers", "enabled": false, @@ -96,8 +122,10 @@ "parameter-name": "scrape", "enabled": false, "value": [ - "@natgeo", - "morning" + "@natgeo-followers", + "@natgeo-following", + "amazingtrips-top-likers", + "amazingtrips-recent-likers" ], "description" : "list of hashtags and usernames. Usernames should start with \\\"@\\\" symbol. The script will scrape with hashtags\\' posts likers and with users\\' followers" }, @@ -108,7 +136,7 @@ "description" : "Remove given number of mass followers from the list of your followers. \"Mass followers\" are those who has more than N followings, where N can be set via --max-following" }, { - "parameter-name": "max_following", + "parameter-name": "mass_follower_min_following", "enabled": false, "value": "1-2", "description" : "Should be used together with --remove-mass-followers. Specifies max number of followings for any your follower, 1000 by default" @@ -167,9 +195,15 @@ "value": "50-60", "description" : "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)" }, + { + "parameter-name": "session_length_in_mins_limit", + "enabled": false, + "value": "50-60", + "description" : "limit the session length by time (minutes), disabled by default. It can be a number (e.g. 60) or a range (e.g. 40-70)" + }, { "parameter-name": "filters", - "enabled": true, + "enabled": false, "value": { "skip_business": false, "skip_non_business": false, @@ -178,13 +212,15 @@ "min_followings": 10, "max_followings": 10000, "min_potency_ratio": 0.45, + "max_potency_ratio": 100, "min_posts": 7, "max_digits_in_profile_name": 4, "privacy_relation": "private_and_public", "skip_profiles_without_stories": false, "blacklist_words": ["word1", "word2"], "mandatory_words": ["word1", "word2"], - "specific_alphabet": ["LATIN", "ARABIC", "HEBREW"] + "specific_alphabet": ["LATIN", "ARABIC", "HEBREW"], + "skip_already_following_profiles": true }, "description" : "add this argument if you want to pass filters as an argument and not from filters.json file" } diff --git a/config-examples-extra/interact_targets.json b/config-examples-extra/interact_targets.json index 9854192..70d9495 100644 --- a/config-examples-extra/interact_targets.json +++ b/config-examples-extra/interact_targets.json @@ -11,6 +11,12 @@ "value": "True", "description" : "add this flag to use an old version of uiautomator. Use it only if you experience problems with the default version" }, + { + "parameter-name": "no_speed_check", + "enabled": false, + "value": "True", + "description" : "skip internet speed check at start" + }, { "parameter-name": "repeat", "enabled": true, @@ -29,6 +35,12 @@ "value": "1-2", "description" : "number of likes for each interacted user, 2 by default. It can be a number (e.g. 2) or a range (e.g. 2-4)" }, + { + "parameter-name": "likes_percentage", + "enabled": true, + "value": "80", + "description" : "likes given percentage of interacted users, 100 by default" + }, { "parameter-name": "stories_count", "enabled": true, @@ -45,14 +57,22 @@ "parameter-name": "interact", "enabled": false, "value": [ - "@natgeo", - "amazingtrips" + "@natgeo-followers", + "@natgeo-following", + "amazingtrips-top-likers", + "amazingtrips-recent-likers" ], "description" : "list of hashtags and usernames. Usernames should start with \\\"@\\\" symbol. The script will interact with hashtags\\' posts likers and with users\\' followers" }, { - "parameter-name": "interaction_users_amount", + "parameter-name": "reinteract_after", "enabled": false, + "value": "72-96", + "description" : "set a time (in hours) to wait before re-interact with an already interacted profile, disabled by default (won't interact again). It can be a number (e.g. 48) or a range (e.g. 50-80)" + }, + { + "parameter-name": "interaction_users_amount", + "enabled": true, "value": "6-8", "description" : "add this argument to select an amount of users from the interact-list (users are randomized). It can be a number (e.g. 4) or a range (e.g. 3-8)" }, @@ -68,6 +88,12 @@ "value": "150", "description" : "minimum amount of followings, after reaching this amount unfollow stops" }, + { + "parameter-name": "max_following", + "enabled": false, + "value": "150", + "description" : "maximum amount of followings, after reaching this amount follow stops. disabled by default" + }, { "parameter-name": "unfollow_non_followers", "enabled": false, @@ -96,8 +122,10 @@ "parameter-name": "scrape", "enabled": false, "value": [ - "@natgeo", - "morning" + "@natgeo-followers", + "@natgeo-following", + "amazingtrips-top-likers", + "amazingtrips-recent-likers" ], "description" : "list of hashtags and usernames. Usernames should start with \\\"@\\\" symbol. The script will scrape with hashtags\\' posts likers and with users\\' followers" }, @@ -108,7 +136,7 @@ "description" : "Remove given number of mass followers from the list of your followers. \"Mass followers\" are those who has more than N followings, where N can be set via --max-following" }, { - "parameter-name": "max_following", + "parameter-name": "mass_follower_min_following", "enabled": false, "value": "1-2", "description" : "Should be used together with --remove-mass-followers. Specifies max number of followings for any your follower, 1000 by default" @@ -121,7 +149,7 @@ }, { "parameter-name": "total_get_profile_limit", - "enabled": false, + "enabled": true, "value": "300-400", "description" : "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)" }, @@ -151,13 +179,13 @@ }, { "parameter-name": "interactions_count", - "enabled": false, + "enabled": true, "value": "6-8", "description" : "number of interactions per each blogger/hashtag, 70 by default. It can be a number (e.g. 70) or a range (e.g. 60-80). Only successful interactions count" }, { "parameter-name": "follow_limit", - "enabled": false, + "enabled": true, "value": "1-2", "description" : "limit on amount of follows during interaction with each one user's followers, disabled by default. It can be a number (e.g. 10) or a range (e.g. 6-9)" }, @@ -167,6 +195,12 @@ "value": "50-60", "description" : "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)" }, + { + "parameter-name": "session_length_in_mins_limit", + "enabled": false, + "value": "50-60", + "description" : "limit the session length by time (minutes), disabled by default. It can be a number (e.g. 60) or a range (e.g. 40-70)" + }, { "parameter-name": "filters", "enabled": false, @@ -178,9 +212,15 @@ "min_followings": 10, "max_followings": 10000, "min_potency_ratio": 0.45, + "max_potency_ratio": 100, "min_posts": 7, "max_digits_in_profile_name": 4, - "privacy_relation": "private_and_public" + "privacy_relation": "private_and_public", + "skip_profiles_without_stories": false, + "blacklist_words": ["word1", "word2"], + "mandatory_words": ["word1", "word2"], + "specific_alphabet": ["LATIN", "ARABIC", "HEBREW"], + "skip_already_following_profiles": true }, "description" : "add this argument if you want to pass filters as an argument and not from filters.json file" } diff --git a/config-examples-extra/interact_with_filters.json b/config-examples-extra/interact_with_filters.json index 223b71f..a325397 100644 --- a/config-examples-extra/interact_with_filters.json +++ b/config-examples-extra/interact_with_filters.json @@ -11,6 +11,12 @@ "value": "True", "description" : "add this flag to use an old version of uiautomator. Use it only if you experience problems with the default version" }, + { + "parameter-name": "no_speed_check", + "enabled": false, + "value": "True", + "description" : "skip internet speed check at start" + }, { "parameter-name": "repeat", "enabled": true, @@ -29,6 +35,12 @@ "value": "1-2", "description" : "number of likes for each interacted user, 2 by default. It can be a number (e.g. 2) or a range (e.g. 2-4)" }, + { + "parameter-name": "likes_percentage", + "enabled": true, + "value": "80", + "description" : "likes given percentage of interacted users, 100 by default" + }, { "parameter-name": "stories_count", "enabled": true, @@ -45,11 +57,19 @@ "parameter-name": "interact", "enabled": true, "value": [ - "@natgeo", - "amazingtrips" + "@natgeo-followers", + "@natgeo-following", + "amazingtrips-top-likers", + "amazingtrips-recent-likers" ], "description" : "list of hashtags and usernames. Usernames should start with \\\"@\\\" symbol. The script will interact with hashtags\\' posts likers and with users\\' followers" }, + { + "parameter-name": "reinteract_after", + "enabled": false, + "value": "72-96", + "description" : "set a time (in hours) to wait before re-interact with an already interacted profile, disabled by default (won't interact again). It can be a number (e.g. 48) or a range (e.g. 50-80)" + }, { "parameter-name": "interaction_users_amount", "enabled": true, @@ -68,6 +88,12 @@ "value": "150", "description" : "minimum amount of followings, after reaching this amount unfollow stops" }, + { + "parameter-name": "max_following", + "enabled": false, + "value": "150", + "description" : "maximum amount of followings, after reaching this amount follow stops. disabled by default" + }, { "parameter-name": "unfollow_non_followers", "enabled": false, @@ -96,8 +122,10 @@ "parameter-name": "scrape", "enabled": false, "value": [ - "@natgeo", - "morning" + "@natgeo-followers", + "@natgeo-following", + "amazingtrips-top-likers", + "amazingtrips-recent-likers" ], "description" : "list of hashtags and usernames. Usernames should start with \\\"@\\\" symbol. The script will scrape with hashtags\\' posts likers and with users\\' followers" }, @@ -108,7 +136,7 @@ "description" : "Remove given number of mass followers from the list of your followers. \"Mass followers\" are those who has more than N followings, where N can be set via --max-following" }, { - "parameter-name": "max_following", + "parameter-name": "mass_follower_min_following", "enabled": false, "value": "1-2", "description" : "Should be used together with --remove-mass-followers. Specifies max number of followings for any your follower, 1000 by default" @@ -167,6 +195,12 @@ "value": "50-60", "description" : "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)" }, + { + "parameter-name": "session_length_in_mins_limit", + "enabled": false, + "value": "50-60", + "description" : "limit the session length by time (minutes), disabled by default. It can be a number (e.g. 60) or a range (e.g. 40-70)" + }, { "parameter-name": "filters", "enabled": true, @@ -178,13 +212,15 @@ "min_followings": 10, "max_followings": 10000, "min_potency_ratio": 0.45, + "max_potency_ratio": 100, "min_posts": 7, "max_digits_in_profile_name": 4, "privacy_relation": "private_and_public", "skip_profiles_without_stories": false, "blacklist_words": ["word1", "word2"], "mandatory_words": ["word1", "word2"], - "specific_alphabet": ["LATIN", "ARABIC", "HEBREW"] + "specific_alphabet": ["LATIN", "ARABIC", "HEBREW"], + "skip_already_following_profiles": true }, "description" : "add this argument if you want to pass filters as an argument and not from filters.json file" } diff --git a/config-examples-extra/scrape.json b/config-examples-extra/scrape.json index 24663a4..6ced4e2 100644 --- a/config-examples-extra/scrape.json +++ b/config-examples-extra/scrape.json @@ -11,6 +11,12 @@ "value": "True", "description" : "add this flag to use an old version of uiautomator. Use it only if you experience problems with the default version" }, + { + "parameter-name": "no_speed_check", + "enabled": false, + "value": "True", + "description" : "skip internet speed check at start" + }, { "parameter-name": "repeat", "enabled": true, @@ -29,6 +35,18 @@ "value": "1-2", "description" : "number of likes for each interacted user, 2 by default. It can be a number (e.g. 2) or a range (e.g. 2-4)" }, + { + "parameter-name": "likes_percentage", + "enabled": false, + "value": "80", + "description" : "likes given percentage of interacted users, 100 by default" + }, + { + "parameter-name": "stories_count", + "enabled": false, + "value": "1-2", + "description" : "number of stories to watch for each user, disabled by default. It can be a number (e.g. 2) or a range (e.g. 2-4)" + }, { "parameter-name": "follow_percentage", "enabled": false, @@ -39,11 +57,19 @@ "parameter-name": "interact", "enabled": false, "value": [ - "@natgeo", - "amazingtrips" + "@natgeo-followers", + "@natgeo-following", + "amazingtrips-top-likers", + "amazingtrips-recent-likers" ], "description" : "list of hashtags and usernames. Usernames should start with \\\"@\\\" symbol. The script will interact with hashtags\\' posts likers and with users\\' followers" }, + { + "parameter-name": "reinteract_after", + "enabled": false, + "value": "72-96", + "description" : "set a time (in hours) to wait before re-interact with an already interacted profile, disabled by default (won't interact again). It can be a number (e.g. 48) or a range (e.g. 50-80)" + }, { "parameter-name": "interaction_users_amount", "enabled": false, @@ -62,6 +88,12 @@ "value": "150", "description" : "minimum amount of followings, after reaching this amount unfollow stops" }, + { + "parameter-name": "max_following", + "enabled": false, + "value": "150", + "description" : "maximum amount of followings, after reaching this amount follow stops. disabled by default" + }, { "parameter-name": "unfollow_non_followers", "enabled": false, @@ -90,17 +122,13 @@ "parameter-name": "scrape", "enabled": true, "value": [ - "@natgeo", - "morning" + "@natgeo-followers", + "@natgeo-following", + "amazingtrips-top-likers", + "amazingtrips-recent-likers" ], "description" : "list of hashtags and usernames. Usernames should start with \\\"@\\\" symbol. The script will scrape with hashtags\\' posts likers and with users\\' followers" }, - { - "parameter-name": "scrape_users_amount", - "enabled": true, - "value": "6-8", - "description" : "add this argument to select an amount of users from the scrape-list (users are randomized). It can be a number (e.g. 4) or a range (e.g. 3-8)" - }, { "parameter-name": "remove_mass_followers", "enabled": false, @@ -108,7 +136,7 @@ "description" : "Remove given number of mass followers from the list of your followers. \"Mass followers\" are those who has more than N followings, where N can be set via --max-following" }, { - "parameter-name": "max_following", + "parameter-name": "mass_follower_min_following", "enabled": false, "value": "1-2", "description" : "Should be used together with --remove-mass-followers. Specifies max number of followings for any your follower, 1000 by default" @@ -161,6 +189,18 @@ "value": "1-2", "description" : "limit on amount of follows during interaction with each one user's followers, disabled by default. It can be a number (e.g. 10) or a range (e.g. 6-9)" }, + { + "parameter-name": "total_story_limit", + "enabled": false, + "value": "50-60", + "description" : "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)" + }, + { + "parameter-name": "session_length_in_mins_limit", + "enabled": false, + "value": "50-60", + "description" : "limit the session length by time (minutes), disabled by default. It can be a number (e.g. 60) or a range (e.g. 40-70)" + }, { "parameter-name": "filters", "enabled": true, @@ -172,13 +212,15 @@ "min_followings": 10, "max_followings": 10000, "min_potency_ratio": 0.45, + "max_potency_ratio": 100, "min_posts": 7, "max_digits_in_profile_name": 4, "privacy_relation": "private_and_public", "skip_profiles_without_stories": false, "blacklist_words": ["word1", "word2"], "mandatory_words": ["word1", "word2"], - "specific_alphabet": ["LATIN", "ARABIC", "HEBREW"] + "specific_alphabet": ["LATIN", "ARABIC", "HEBREW"], + "skip_already_following_profiles": true }, "description" : "add this argument if you want to pass filters as an argument and not from filters.json file" } diff --git a/config-examples-extra/unfollow.json b/config-examples-extra/unfollow.json index 139443c..afe076b 100644 --- a/config-examples-extra/unfollow.json +++ b/config-examples-extra/unfollow.json @@ -11,6 +11,12 @@ "value": "True", "description" : "add this flag to use an old version of uiautomator. Use it only if you experience problems with the default version" }, + { + "parameter-name": "no_speed_check", + "enabled": false, + "value": "True", + "description" : "skip internet speed check at start" + }, { "parameter-name": "repeat", "enabled": true, @@ -29,6 +35,18 @@ "value": "1-2", "description" : "number of likes for each interacted user, 2 by default. It can be a number (e.g. 2) or a range (e.g. 2-4)" }, + { + "parameter-name": "likes_percentage", + "enabled": false, + "value": "80", + "description" : "likes given percentage of interacted users, 100 by default" + }, + { + "parameter-name": "stories_count", + "enabled": false, + "value": "1-2", + "description" : "number of stories to watch for each user, disabled by default. It can be a number (e.g. 2) or a range (e.g. 2-4)" + }, { "parameter-name": "follow_percentage", "enabled": false, @@ -39,11 +57,19 @@ "parameter-name": "interact", "enabled": false, "value": [ - "@natgeo", - "amazingtrips" + "@natgeo-followers", + "@natgeo-following", + "amazingtrips-top-likers", + "amazingtrips-recent-likers" ], "description" : "list of hashtags and usernames. Usernames should start with \\\"@\\\" symbol. The script will interact with hashtags\\' posts likers and with users\\' followers" }, + { + "parameter-name": "reinteract_after", + "enabled": false, + "value": "72-96", + "description" : "set a time (in hours) to wait before re-interact with an already interacted profile, disabled by default (won't interact again). It can be a number (e.g. 48) or a range (e.g. 50-80)" + }, { "parameter-name": "interaction_users_amount", "enabled": false, @@ -62,6 +88,12 @@ "value": "150", "description" : "minimum amount of followings, after reaching this amount unfollow stops" }, + { + "parameter-name": "max_following", + "enabled": false, + "value": "150", + "description" : "maximum amount of followings, after reaching this amount follow stops. disabled by default" + }, { "parameter-name": "unfollow_non_followers", "enabled": false, @@ -90,8 +122,10 @@ "parameter-name": "scrape", "enabled": false, "value": [ - "@natgeo", - "morning" + "@natgeo-followers", + "@natgeo-following", + "amazingtrips-top-likers", + "amazingtrips-recent-likers" ], "description" : "list of hashtags and usernames. Usernames should start with \\\"@\\\" symbol. The script will scrape with hashtags\\' posts likers and with users\\' followers" }, @@ -102,7 +136,7 @@ "description" : "Remove given number of mass followers from the list of your followers. \"Mass followers\" are those who has more than N followings, where N can be set via --max-following" }, { - "parameter-name": "max_following", + "parameter-name": "mass_follower_min_following", "enabled": false, "value": "1-2", "description" : "Should be used together with --remove-mass-followers. Specifies max number of followings for any your follower, 1000 by default" @@ -155,6 +189,18 @@ "value": "1-2", "description" : "limit on amount of follows during interaction with each one user's followers, disabled by default. It can be a number (e.g. 10) or a range (e.g. 6-9)" }, + { + "parameter-name": "total_story_limit", + "enabled": false, + "value": "50-60", + "description" : "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)" + }, + { + "parameter-name": "session_length_in_mins_limit", + "enabled": false, + "value": "50-60", + "description" : "limit the session length by time (minutes), disabled by default. It can be a number (e.g. 60) or a range (e.g. 40-70)" + }, { "parameter-name": "filters", "enabled": false, @@ -166,9 +212,15 @@ "min_followings": 10, "max_followings": 10000, "min_potency_ratio": 0.45, + "max_potency_ratio": 100, "min_posts": 7, "max_digits_in_profile_name": 4, - "privacy_relation": "private_and_public" + "privacy_relation": "private_and_public", + "skip_profiles_without_stories": false, + "blacklist_words": ["word1", "word2"], + "mandatory_words": ["word1", "word2"], + "specific_alphabet": ["LATIN", "ARABIC", "HEBREW"], + "skip_already_following_profiles": true }, "description" : "add this argument if you want to pass filters as an argument and not from filters.json file" } diff --git a/config-examples/interact.json b/config-examples/interact.json index 0315a84..5d981ff 100644 --- a/config-examples/interact.json +++ b/config-examples/interact.json @@ -11,6 +11,12 @@ "value": "True", "description" : "add this flag to use an old version of uiautomator. Use it only if you experience problems with the default version" }, + { + "parameter-name": "no_speed_check", + "enabled": false, + "value": "True", + "description" : "skip internet speed check at start" + }, { "parameter-name": "repeat", "enabled": true, @@ -23,6 +29,12 @@ "value": "1-2", "description" : "number of likes for each interacted user, 2 by default. It can be a number (e.g. 2) or a range (e.g. 2-4)" }, + { + "parameter-name": "likes_percentage", + "enabled": true, + "value": "80", + "description" : "likes given percentage of interacted users, 100 by default" + }, { "parameter-name": "stories_count", "enabled": true, @@ -39,11 +51,19 @@ "parameter-name": "interact", "enabled": true, "value": [ - "@natgeo", - "amazingtrips" + "@natgeo-followers", + "@natgeo-following", + "amazingtrips-top-likers", + "amazingtrips-recent-likers" ], "description" : "list of hashtags and usernames. Usernames should start with \\\"@\\\" symbol. The script will interact with hashtags\\' posts likers and with users\\' followers" }, + { + "parameter-name": "reinteract_after", + "enabled": false, + "value": "72-96", + "description" : "set a time (in hours) to wait before re-interact with an already interacted profile, disabled by default (won't interact again). It can be a number (e.g. 48) or a range (e.g. 50-80)" + }, { "parameter-name": "interaction_users_amount", "enabled": true, @@ -62,6 +82,12 @@ "value": "150", "description" : "minimum amount of followings, after reaching this amount unfollow stops" }, + { + "parameter-name": "max_following", + "enabled": false, + "value": "150", + "description" : "maximum amount of followings, after reaching this amount follow stops. disabled by default" + }, { "parameter-name": "unfollow_non_followers", "enabled": false, @@ -109,6 +135,17 @@ "enabled": true, "value": "50-60", "description" : "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)" + }, + { + "parameter-name": "total_get_profile_limit", + "enabled": true, + "value": "300-400", + "description" : "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)" + }, + { + "parameter-name": "session_length_in_mins_limit", + "enabled": false, + "value": "50-60", + "description" : "limit the session length by time (minutes), disabled by default. It can be a number (e.g. 60) or a range (e.g. 40-70)" } ] - diff --git a/config-examples/interact_targets.json b/config-examples/interact_targets.json new file mode 100644 index 0000000..80838b9 --- /dev/null +++ b/config-examples/interact_targets.json @@ -0,0 +1,151 @@ +[ + { + "parameter-name": "device", + "enabled": false, + "value": "emulator-5554", + "description" : "device identifier. Should be used only when multiple devices are connected at once" + }, + { + "parameter-name": "old", + "enabled": false, + "value": "True", + "description" : "add this flag to use an old version of uiautomator. Use it only if you experience problems with the default version" + }, + { + "parameter-name": "repeat", + "enabled": true, + "value": "160-220", + "description" : "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)" + }, + { + "parameter-name": "likes_count", + "enabled": true, + "value": "1-2", + "description" : "number of likes for each interacted user, 2 by default. It can be a number (e.g. 2) or a range (e.g. 2-4)" + }, + { + "parameter-name": "likes_percentage", + "enabled": true, + "value": "80", + "description" : "likes given percentage of interacted users, 100 by default" + }, + { + "parameter-name": "stories_count", + "enabled": true, + "value": "1-2", + "description" : "number of stories to watch for each user, disabled by default. It can be a number (e.g. 2) or a range (e.g. 2-4)" + }, + { + "parameter-name": "follow_percentage", + "enabled": true, + "value": "80", + "description" : "follow given percentage of interacted users, 0 by default" + }, + { + "parameter-name": "interact", + "enabled": false, + "value": [ + "@natgeo-followers", + "@natgeo-following", + "amazingtrips-top-likers", + "amazingtrips-recent-likers" + ], + "description" : "list of hashtags and usernames. Usernames should start with \\\"@\\\" symbol. The script will interact with hashtags\\' posts likers and with users\\' followers" + }, + { + "parameter-name": "reinteract_after", + "enabled": false, + "value": "72-96", + "description" : "set a time (in hours) to wait before re-interact with an already interacted profile, disabled by default (won't interact again). It can be a number (e.g. 48) or a range (e.g. 50-80)" + }, + { + "parameter-name": "interact_targets", + "enabled": true, + "value": "True", + "description" : "use this argument in order to interact with profiles from targets.txt" + }, + { + "parameter-name": "interaction_users_amount", + "enabled": true, + "value": "6-8", + "description" : "add this argument to select an amount of users from the interact-list (users are randomized). It can be a number (e.g. 4) or a range (e.g. 3-8)" + }, + { + "parameter-name": "unfollow", + "enabled": false, + "value": "20-40", + "description" : "unfollow at most given number of users. Only users followed by this script will be unfollowed. The order is from oldest to newest followings. It can be a number (e.g. 100) or a range (e.g. 100-200)" + }, + { + "parameter-name": "min_following", + "enabled": false, + "value": "150", + "description" : "minimum amount of followings, after reaching this amount unfollow stops" + }, + { + "parameter-name": "max_following", + "enabled": false, + "value": "150", + "description" : "maximum amount of followings, after reaching this amount follow stops. disabled by default" + }, + { + "parameter-name": "unfollow_non_followers", + "enabled": false, + "value": "20-40", + "description" : "unfollow at most given number of users, that don\\'t follow you back. Only users followed by this script will be unfollowed. The order is from oldest to newest followings. It can be a number (e.g. 100) or a range (e.g. 100-200)" + }, + { + "parameter-name": "unfollow_any", + "enabled": false, + "value": "20-40", + "description" : "unfollow at most given number of users. The order is from oldest to newest followings. It can be a number (e.g. 100) or a range (e.g. 100-200)" + }, + { + "parameter-name": "total_likes_limit", + "enabled": true, + "value": "50-60", + "description" : "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)" + }, + { + "parameter-name": "total_interactions_limit", + "enabled": true, + "value": "50-60", + "description" : "number of total interactions per session, disabled by default. It can be a number (e.g. 70) or a range (e.g. 60-80). Only successful interactions count" + }, + { + "parameter-name": "total_follow_limit", + "enabled": true, + "value": "15-20", + "description" : "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)" + }, + { + "parameter-name": "interactions_count", + "enabled": true, + "value": "6-8", + "description" : "number of interactions per each blogger/hashtag, 70 by default. It can be a number (e.g. 70) or a range (e.g. 60-80). Only successful interactions count" + }, + { + "parameter-name": "follow_limit", + "enabled": true, + "value": "1-2", + "description" : "limit on amount of follows during interaction with each one user's followers, disabled by default. It can be a number (e.g. 10) or a range (e.g. 6-9)" + }, + { + "parameter-name": "total_story_limit", + "enabled": true, + "value": "50-60", + "description" : "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)" + }, + { + "parameter-name": "total_get_profile_limit", + "enabled": true, + "value": "300-400", + "description" : "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)" + }, + { + "parameter-name": "session_length_in_mins_limit", + "enabled": false, + "value": "50-60", + "description" : "limit the session length by time (minutes), disabled by default. It can be a number (e.g. 60) or a range (e.g. 40-70)" + } +] diff --git a/config-examples/unfollow.json b/config-examples/unfollow.json index bdc7872..255df56 100644 --- a/config-examples/unfollow.json +++ b/config-examples/unfollow.json @@ -11,6 +11,12 @@ "value": "True", "description" : "add this flag to use an old version of uiautomator. Use it only if you experience problems with the default version" }, + { + "parameter-name": "no_speed_check", + "enabled": false, + "value": "True", + "description" : "skip internet speed check at start" + }, { "parameter-name": "repeat", "enabled": true, @@ -22,6 +28,12 @@ "enabled": false, "value": "1-2", "description" : "number of likes for each interacted user, 2 by default. It can be a number (e.g. 2) or a range (e.g. 2-4)" + }, + { + "parameter-name": "likes_percentage", + "enabled": false, + "value": "80", + "description" : "likes given percentage of interacted users, 100 by default" }, { "parameter-name": "follow_percentage", @@ -33,11 +45,19 @@ "parameter-name": "interact", "enabled": false, "value": [ - "@natgeo", - "amazingtrips" + "@natgeo-followers", + "@natgeo-following", + "amazingtrips-top-likers", + "amazingtrips-recent-likers" ], "description" : "list of hashtags and usernames. Usernames should start with \\\"@\\\" symbol. The script will interact with hashtags\\' posts likers and with users\\' followers" }, + { + "parameter-name": "reinteract_after", + "enabled": false, + "value": "72-96", + "description" : "set a time (in hours) to wait before re-interact with an already interacted profile, disabled by default (won't interact again). It can be a number (e.g. 48) or a range (e.g. 50-80)" + }, { "parameter-name": "interaction_users_amount", "enabled": false, @@ -56,6 +76,12 @@ "value": "150", "description" : "minimum amount of followings, after reaching this amount unfollow stops" }, + { + "parameter-name": "max_following", + "enabled": false, + "value": "150", + "description" : "maximum amount of followings, after reaching this amount follow stops. disabled by default" + }, { "parameter-name": "unfollow_non_followers", "enabled": false, @@ -97,6 +123,24 @@ "enabled": false, "value": "1-2", "description" : "limit on amount of follows during interaction with each one user's followers, disabled by default. It can be a number (e.g. 10) or a range (e.g. 6-9)" + }, + { + "parameter-name": "total_story_limit", + "enabled": true, + "value": "50-60", + "description" : "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)" + }, + { + "parameter-name": "total_get_profile_limit", + "enabled": true, + "value": "300-400", + "description" : "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)" + }, + { + "parameter-name": "session_length_in_mins_limit", + "enabled": false, + "value": "50-60", + "description" : "limit the session length by time (minutes), disabled by default. It can be a number (e.g. 60) or a range (e.g. 40-70)" } ] diff --git a/insomniac/__version__.py b/insomniac/__version__.py index 28e39cb..8266a4a 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.3.2' +__version__ = '3.4.0' __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 14a2863..0da48de 100644 --- a/insomniac/action_get_my_profile_info.py +++ b/insomniac/action_get_my_profile_info.py @@ -2,6 +2,7 @@ from insomniac.counters_parser import parse from insomniac.device_facade import DeviceFacade from insomniac.navigation import navigate, Tabs, LanguageChangedException +from insomniac.sleeper import sleeper from insomniac.utils import * TITLE_VIEW_ID_REGEX = 'com.instagram.android:id/title_view|com.instagram.android:id/action_bar_large_title' @@ -9,14 +10,14 @@ def get_my_profile_info(device): navigate(device, Tabs.PROFILE) - random_sleep() + sleeper.random_sleep() print("Refreshing your profile status...") coordinator_layout = device.find(resourceId='com.instagram.android:id/coordinator_root_layout') if coordinator_layout.exists(): coordinator_layout.scroll(DeviceFacade.Direction.TOP) - random_sleep() + sleeper.random_sleep() update_interaction_rect(device) diff --git a/insomniac/action_handle_blogger.py b/insomniac/action_handle_blogger.py index bfab589..ea2174f 100644 --- a/insomniac/action_handle_blogger.py +++ b/insomniac/action_handle_blogger.py @@ -1,21 +1,48 @@ from functools import partial from insomniac.actions_impl import interact_with_user, open_user_followers, \ - scroll_to_bottom, iterate_over_followers, InteractionStrategy, is_private_account, do_have_story + scroll_to_bottom, iterate_over_followers, InteractionStrategy, is_private_account, do_have_story, \ + open_user_followings from insomniac.actions_runners import ActionState -from insomniac.actions_types import LikeAction, FollowAction, InteractAction, GetProfileAction, StoryWatchAction +from insomniac.actions_types import LikeAction, FollowAction, InteractAction, GetProfileAction, StoryWatchAction, \ + BloggerInteractionType from insomniac.limits import process_limits from insomniac.report import print_short_report, print_interaction_types from insomniac.storage import FollowingStatus from insomniac.utils import * +def extract_blogger_instructions(source): + split_idx = source.find('-') + if split_idx == -1: + print("There is no special interaction-instructions for " + source + ". Working with " + source + " followers.") + return source, BloggerInteractionType.FOLLOWERS + + selected_instruction = None + source_profile_name = source[:split_idx] + interaction_instructions_str = source[split_idx+1:] + + for blogger_instruction in BloggerInteractionType: + if blogger_instruction.value == interaction_instructions_str: + selected_instruction = blogger_instruction + break + + if selected_instruction is None: + print("Couldn't use interaction-instructions " + interaction_instructions_str + + ". Working with " + source + " followers.") + selected_instruction = BloggerInteractionType.FOLLOWERS + + return source_profile_name, selected_instruction + + def handle_blogger(device, username, + instructions, session_state, likes_count, stories_count, follow_percentage, + like_percentage, storage, on_action, is_limit_reached, @@ -28,8 +55,12 @@ def handle_blogger(device, my_username=session_state.my_username, on_action=on_action) - if not open_user_followers(device=device, username=username, on_action=on_action): - return + if instructions == BloggerInteractionType.FOLLOWERS: + if not open_user_followers(device=device, username=username, on_action=on_action): + return + elif instructions == BloggerInteractionType.FOLLOWING: + if not open_user_followings(device=device, username=username, on_action=on_action): + return if is_myself: scroll_to_bottom(device) @@ -77,7 +108,7 @@ def interact_with_follower(follower_name, follower_name_view): if not is_passed_filters(device, follower_name): storage.add_filtered_user(follower_name) # Continue to next follower - print("Back to followers list") + print("Back to profiles list") device.back() return True @@ -121,6 +152,7 @@ def interact_with_follower(follower_name, follower_name_view): do_story_watch=can_watch, likes_count=likes_value, follow_percentage=follow_percentage, + like_percentage=like_percentage, stories_count=stories_value) is_liked, is_followed, is_watch = interaction(username=follower_name, interaction_strategy=interaction_strategy) @@ -149,7 +181,7 @@ def interact_with_follower(follower_name, follower_name_view): can_continue = False action_status.set_limit(ActionState.SESSION_LIMIT_REACHED) - print("Back to followers list") + print("Back to profiles list") device.back() return can_continue diff --git a/insomniac/action_handle_hashtag.py b/insomniac/action_handle_hashtag.py index 4544b5b..8e11452 100644 --- a/insomniac/action_handle_hashtag.py +++ b/insomniac/action_handle_hashtag.py @@ -3,21 +3,48 @@ from insomniac.actions_impl import interact_with_user, ScrollEndDetector, open_likers, iterate_over_likers, \ is_private_account, InteractionStrategy, do_have_story from insomniac.actions_runners import ActionState -from insomniac.actions_types import InteractAction, LikeAction, FollowAction, GetProfileAction, StoryWatchAction +from insomniac.actions_types import InteractAction, LikeAction, FollowAction, GetProfileAction, StoryWatchAction, \ + HashtagInteractionType from insomniac.device_facade import DeviceFacade from insomniac.limits import process_limits from insomniac.navigation import search_for from insomniac.report import print_short_report, print_interaction_types +from insomniac.sleeper import sleeper from insomniac.storage import FollowingStatus from insomniac.utils import * +def extract_hashtag_instructions(source): + split_idx = source.find('-') + if split_idx == -1: + print("There is no special interaction-instructions for " + source + ". Working with " + source + " recent-likers.") + return source, HashtagInteractionType.RECENT_LIKERS + + selected_instruction = None + source_profile_name = source[:split_idx] + interaction_instructions_str = source[split_idx+1:] + + for hashtag_instruction in HashtagInteractionType: + if hashtag_instruction.value == interaction_instructions_str: + selected_instruction = hashtag_instruction + break + + if selected_instruction is None: + print("Couldn't use interaction-instructions " + interaction_instructions_str + + ". Working with " + source + " recent-likers.") + selected_instruction = HashtagInteractionType.RECENT_LIKERS + + return source_profile_name, selected_instruction + + def handle_hashtag(device, hashtag, + instructions, session_state, likes_count, stories_count, follow_percentage, + like_percentage, storage, on_action, is_limit_reached, @@ -47,7 +74,7 @@ def pre_conditions(liker_username, liker_username_view): return True - def interact_with_liker(liker_username, liker_username_view): + def interact_with_profile(liker_username, liker_username_view): is_interact_limit_reached, interact_reached_source_limit, interact_reached_session_limit = \ is_limit_reached(InteractAction(source=interaction_source, user=liker_username, succeed=True), session_state) @@ -114,6 +141,7 @@ def interact_with_liker(liker_username, liker_username_view): do_story_watch=can_watch, likes_count=likes_value, follow_percentage=follow_percentage, + like_percentage=like_percentage, stories_count=stories_value) is_liked, is_followed, is_watch = interaction(username=liker_username, @@ -148,25 +176,36 @@ def interact_with_liker(liker_username, liker_username_view): return can_continue - extract_hashtag_likers_and_interact(device, hashtag, interact_with_liker, pre_conditions, on_action) + extract_hashtag_profiles_and_interact(device, + hashtag, + instructions, + interact_with_profile, + pre_conditions, + on_action) -def extract_hashtag_likers_and_interact(device, hashtag, iteration_callback, iteration_callback_pre_conditions, on_action): - print("Interacting with #{0} recent-likers".format(hashtag)) +def extract_hashtag_profiles_and_interact(device, + hashtag, + instructions, + iteration_callback, + iteration_callback_pre_conditions, + on_action): + print("Interacting with #{0}-{1}".format(hashtag, instructions.value)) if not search_for(device, hashtag=hashtag, on_action=on_action): return # Switch to Recent tab - print("Switching to Recent tab") - tab_layout = device.find(resourceId='com.instagram.android:id/tab_layout', - className='android.widget.LinearLayout') - if tab_layout.exists(): - tab_layout.child(index=1).click() - else: - print("Can't Find recent tab. Interacting with Popular.") + if instructions == HashtagInteractionType.RECENT_LIKERS: + print("Switching to Recent tab") + tab_layout = device.find(resourceId='com.instagram.android:id/tab_layout', + className='android.widget.LinearLayout') + if tab_layout.exists(): + tab_layout.child(index=1).click() + else: + print("Can't Find recent tab. Interacting with Popular.") - random_sleep() + sleeper.random_sleep() # Open first post print("Opening the first post") @@ -174,7 +213,7 @@ def extract_hashtag_likers_and_interact(device, hashtag, iteration_callback, ite className='android.widget.ImageView', index=1) first_post_view.click() - random_sleep() + sleeper.random_sleep() posts_list_view = device.find(resourceId='android:id/list', className='androidx.recyclerview.widget.RecyclerView') @@ -192,7 +231,7 @@ def pre_conditions(liker_username, liker_username_view): print("List of likers is opened.") posts_end_detector.notify_new_page() - random_sleep() + sleeper.random_sleep() iterate_over_likers(device, iteration_callback, pre_conditions) diff --git a/insomniac/action_handle_target.py b/insomniac/action_handle_target.py index b2f435e..8c37548 100644 --- a/insomniac/action_handle_target.py +++ b/insomniac/action_handle_target.py @@ -15,6 +15,7 @@ def handle_target(device, likes_count, stories_count, follow_percentage, + like_percentage, storage, on_action, is_limit_reached, @@ -107,6 +108,7 @@ def interact_with_target(target_name, target_name_view): do_story_watch=can_watch, likes_count=likes_value, follow_percentage=follow_percentage, + like_percentage=like_percentage, stories_count=stories_value) is_liked, is_followed, is_watch = interaction(username=target_name, diff --git a/insomniac/action_unfollow.py b/insomniac/action_unfollow.py index 7699316..13e8bc7 100644 --- a/insomniac/action_unfollow.py +++ b/insomniac/action_unfollow.py @@ -3,6 +3,7 @@ from insomniac.actions_impl import open_user_followings, sort_followings_by_date, iterate_over_followings, do_unfollow from insomniac.actions_types import UnfollowAction, GetProfileAction from insomniac.limits import process_limits +from insomniac.sleeper import sleeper from insomniac.storage import FollowingStatus from insomniac.utils import * @@ -10,9 +11,9 @@ def unfollow(device, on_action, storage, unfollow_restriction, session_state, is_limit_reached, action_status): if not open_user_followings(device=device, username=None, on_action=on_action): return - random_sleep() + sleeper.random_sleep() sort_followings_by_date(device) - random_sleep() + sleeper.random_sleep() # noinspection PyUnusedLocal # following_name_view is a standard callback argument diff --git a/insomniac/actions_impl.py b/insomniac/actions_impl.py index baf3e75..caf8e7a 100644 --- a/insomniac/actions_impl.py +++ b/insomniac/actions_impl.py @@ -4,6 +4,7 @@ from insomniac.device_facade import DeviceFacade from insomniac.navigation import switch_to_english, search_for, LanguageChangedException from insomniac.scroll_end_detector import ScrollEndDetector +from insomniac.sleeper import sleeper from insomniac.utils import * FOLLOWERS_BUTTON_ID_REGEX = 'com.instagram.android:id/row_profile_header_followers_container' \ @@ -11,6 +12,7 @@ TEXTVIEW_OR_BUTTON_REGEX = 'android.widget.TextView|android.widget.Button' FOLLOW_REGEX = 'Follow|Follow Back' UNFOLLOW_REGEX = 'Following|Requested' +SHOP_REGEX = 'Add Shop|View Shop' FOLLOWING_BUTTON_ID_REGEX = 'com.instagram.android:id/row_profile_header_following_container' \ '|com.instagram.android:id/row_profile_header_container_following' USER_AVATAR_VIEW_ID = 'com.instagram.android:id/circular_image|^$' @@ -24,12 +26,13 @@ class InteractionStrategy(object): def __init__(self, do_like=False, do_follow=False, do_story_watch=False, - likes_count=2, follow_percentage=0, stories_count=2): + likes_count=2, like_percentage=100, follow_percentage=0, stories_count=2): self.do_like = do_like self.do_follow = do_follow self.do_story_watch = do_story_watch self.likes_count = likes_count self.follow_percentage = follow_percentage + self.like_percentage = like_percentage self.stories_count = stories_count @@ -114,7 +117,7 @@ def scrolled_to_top(): while True: print("Iterate over visible followers") if not iterate_without_sleep: - random_sleep() + sleeper.random_sleep() screen_iterated_followers = [] screen_skipped_followers_count = 0 @@ -174,7 +177,7 @@ def scrolled_to_top(): if retry_button.exists(): print("Press \"Load\" button") retry_button.click() - random_sleep() + sleeper.random_sleep() pressed_retry = True if need_swipe and not pressed_retry: @@ -250,9 +253,9 @@ def interact_with_user(device, if username == my_username: print("It's you, skip.") - return liked_count == interaction_strategy.likes_count, is_followed + return liked_count == interaction_strategy.likes_count, is_followed, is_watched - random_sleep() + sleeper.random_sleep() if interaction_strategy.do_story_watch: is_watched = _watch_stories(device, username, interaction_strategy.stories_count, on_action) @@ -280,9 +283,9 @@ def on_like(): row = photo_index // 3 column = photo_index - row * 3 - random_sleep() + sleeper.random_sleep() print("Open and like photo #" + str(i + 1) + " (" + str(row + 1) + " row, " + str(column + 1) + " column)") - if not _open_photo_and_like(device, row, column, on_like): + if not _open_photo_and_like(device, row, column, interaction_strategy.like_percentage, on_like): print(COLOR_OKGREEN + "Less than " + str(number_of_rows_to_use * 3) + " photos." + COLOR_ENDC) break @@ -295,7 +298,7 @@ def on_like(): return liked_count > 0, is_followed, is_watched -def _open_photo_and_like(device, row, column, on_like): +def _open_photo_and_like(device, row, column, like_percentage, on_like): def open_photo(): # recycler_view has a className 'androidx.recyclerview.widget.RecyclerView' on modern Android versions and # 'android.view.View' on Android 5.0.1 and probably earlier versions @@ -312,30 +315,39 @@ def open_photo(): if not open_photo(): return False - random_sleep() - print("Double click!") - photo_view = device.find(resourceId='com.instagram.android:id/layout_container_main', - className='android.widget.FrameLayout') - photo_view.double_click() - random_sleep() - - # If double click didn't work, set like by icon click - try: - # Click only button which is under the action bar and above the tab bar. - # It fixes bugs with accidental back / home clicks. - for like_button in device.find(resourceId='com.instagram.android:id/row_feed_button_like', - className='android.widget.ImageView', - selected=False): - if is_in_interaction_rect(like_button): - print("Double click didn't work, click on icon.") - like_button.click() - random_sleep() - break - except DeviceFacade.JsonRpcError: - print("Double click worked successfully.") + sleeper.random_sleep() + + to_like = True + like_chance = randint(1, 100) + if like_chance > like_percentage: + print("Not going to like image due to like-percentage hit") + to_like = False + + if to_like: + print("Double click!") + photo_view = device.find(resourceId='com.instagram.android:id/layout_container_main', + className='android.widget.FrameLayout') + photo_view.double_click() + sleeper.random_sleep() + + # If double click didn't work, set like by icon click + try: + # Click only button which is under the action bar and above the tab bar. + # It fixes bugs with accidental back / home clicks. + for like_button in device.find(resourceId='com.instagram.android:id/row_feed_button_like', + className='android.widget.ImageView', + selected=False): + if is_in_interaction_rect(like_button): + print("Double click didn't work, click on icon.") + like_button.click() + sleeper.random_sleep() + break + except DeviceFacade.JsonRpcError: + print("Double click worked successfully.") + + detect_block(device) + on_like() - detect_block(device) - on_like() print("Back to profile") device.back() return True @@ -351,41 +363,67 @@ def _follow(device, username, follow_percentage): if coordinator_layout.exists(): coordinator_layout.scroll(DeviceFacade.Direction.TOP) - random_sleep() + sleeper.random_sleep() - profile_header_actions_layout = device.find(resourceId='com.instagram.android:id/profile_header_actions_top_row', - className='android.widget.LinearLayout') - if not profile_header_actions_layout.exists(): - print(COLOR_FAIL + "Cannot find profile actions." + COLOR_ENDC) - return False + profile_header_main_layout = device.find(resourceId="com.instagram.android:id/profile_header_fixed_list", + className='android.widget.LinearLayout') - follow_button = profile_header_actions_layout.child(classNameMatches=TEXTVIEW_OR_BUTTON_REGEX, - clickable=True, - textMatches=FOLLOW_REGEX) - if not follow_button.exists(): - unfollow_button = profile_header_actions_layout.child(classNameMatches=TEXTVIEW_OR_BUTTON_REGEX, - clickable=True, - textMatches=UNFOLLOW_REGEX) - if unfollow_button.exists(): - print(COLOR_OKGREEN + "You already follow @" + username + "." + COLOR_ENDC) + shop_button = profile_header_main_layout.child(className='android.widget.Button', + clickable=True, + textMatches=SHOP_REGEX) + + if shop_button.exists(): + follow_button = profile_header_main_layout.child(className='android.widget.Button', + clickable=True, + textMatches=FOLLOW_REGEX) + if not follow_button.exists(): + print(COLOR_FAIL + "Look like a shop profile without an option to follow, continue." + COLOR_ENDC) + return False + else: + profile_header_actions_layout = device.find(resourceId='com.instagram.android:id/profile_header_actions_top_row', + className='android.widget.LinearLayout') + if not profile_header_actions_layout.exists(): + print(COLOR_FAIL + "Cannot find profile actions." + COLOR_ENDC) return False - else: - print(COLOR_FAIL + "Cannot find neither Follow button, nor Unfollow button. Maybe not " - "English language is set?" + COLOR_ENDC) - save_crash(device) - switch_to_english(device) - raise LanguageChangedException() + + follow_button = profile_header_actions_layout.child(classNameMatches=TEXTVIEW_OR_BUTTON_REGEX, + clickable=True, + textMatches=FOLLOW_REGEX) + + if not follow_button.exists(): + unfollow_button = profile_header_actions_layout.child(classNameMatches=TEXTVIEW_OR_BUTTON_REGEX, + clickable=True, + textMatches=UNFOLLOW_REGEX) + if unfollow_button.exists(): + print(COLOR_OKGREEN + "You already follow @" + username + "." + COLOR_ENDC) + return False + else: + print(COLOR_FAIL + "Cannot find neither Follow button, nor Unfollow button. Maybe not " + "English language is set?" + COLOR_ENDC) + save_crash(device) + switch_to_english(device) + return False follow_button.click() detect_block(device) print(COLOR_OKGREEN + "Followed @" + username + COLOR_ENDC) - random_sleep() + sleeper.random_sleep() return True def do_have_story(device): return device.find(resourceId="com.instagram.android:id/reel_ring", - className="android.view.View").exists() + className="android.view.View").exists(quick=True) + + +def is_already_followed(device): + # Using main layout in order to support shop pages + profile_header_main_layout = device.find(resourceId="com.instagram.android:id/profile_header_fixed_list", + className='android.widget.LinearLayout') + unfollow_button = profile_header_main_layout.child(classNameMatches=TEXTVIEW_OR_BUTTON_REGEX, + clickable=True, + textMatches=UNFOLLOW_REGEX) + return unfollow_button.exists() def _watch_stories(device, username, stories_value, on_action): @@ -403,12 +441,12 @@ def _watch_stories(device, username, stories_value, on_action): profile_picture.click() # Open the first story on_action(StoryWatchAction(user=username)) - random_sleep() + sleeper.random_sleep() for i in range(1, stories_value): if _skip_story(device): print("Watching next story...") - random_sleep() + sleeper.random_sleep() else: print(COLOR_OKGREEN + "Watched all stories" + COLOR_ENDC) break @@ -455,6 +493,8 @@ def _open_user(device, username, open_followers=False, open_followings=False, re if not search_for(device, username=username, on_action=on_action): return False + sleeper.random_sleep() + if open_followers: print("Open @" + username + " followers") followers_button = device.find(resourceIdMatches=FOLLOWERS_BUTTON_ID_REGEX) @@ -475,7 +515,7 @@ def iterate_over_followings(device, iteration_callback, iteration_callback_pre_c while True: print("Iterate over visible followings") - random_sleep() + sleeper.random_sleep() screen_iterated_followings = 0 for item in device.find(resourceId='com.instagram.android:id/follow_list_container', @@ -494,7 +534,7 @@ def iterate_over_followings(device, iteration_callback, iteration_callback_pre_c to_continue = iteration_callback(username, user_name_view) if to_continue: - random_sleep() + sleeper.random_sleep() else: print(COLOR_OKBLUE + "Stopping unfollowing" + COLOR_ENDC) return @@ -565,7 +605,7 @@ def do_unfollow(device, username, my_username, check_if_is_follower, on_action): return False confirm_unfollow_button.click() - random_sleep() + sleeper.random_sleep() _close_confirm_dialog_if_shown(device) detect_block(device) @@ -579,7 +619,7 @@ def open_likers(device): className='android.widget.TextView') if likes_view.exists(quick=True) and is_in_interaction_rect(likes_view): print("Opening post likers") - random_sleep() + sleeper.random_sleep() likes_view.click() return True else: @@ -591,7 +631,7 @@ def _check_is_follower(device, username, my_username): following_container = device.find(resourceIdMatches=FOLLOWING_BUTTON_ID_REGEX) following_container.click() - random_sleep() + sleeper.random_sleep() my_username_view = device.find(resourceId='com.instagram.android:id/follow_list_username', className='android.widget.TextView', @@ -615,7 +655,7 @@ def _close_confirm_dialog_if_shown(device): return print(COLOR_OKGREEN + "Dialog shown, confirm unfollowing." + COLOR_ENDC) - random_sleep() + sleeper.random_sleep() unfollow_button = dialog_root_view.child(resourceId='com.instagram.android:id/primary_button', className='android.widget.TextView') unfollow_button.click() diff --git a/insomniac/actions_runners.py b/insomniac/actions_runners.py index 276c82b..e832819 100644 --- a/insomniac/actions_runners.py +++ b/insomniac/actions_runners.py @@ -104,6 +104,11 @@ class InteractBySourceActionRunner(CoreActionsRunner): 'metavar': '2-4', "default": '2' }, + "like_percentage": { + "help": "likes given percentage of interacted users, 100 by default", + "metavar": '50', + "default": '100' + }, "follow_percentage": { "help": "follow given percentage of interacted users, 0 by default", "metavar": '50', @@ -112,9 +117,10 @@ class InteractBySourceActionRunner(CoreActionsRunner): "interact": { "nargs": '+', "help": 'list of hashtags and usernames. Usernames should start with \"@\" symbol. ' - 'The script will interact with hashtags\' posts likers and with users\' followers', + 'You can specify the way of interaction after a \"-\" sign: @username-followers, ' + '@username-following, hashtag-top-likers, hashtag-recent-likers', "default": [], - "metavar": ('hashtag', '@username') + "metavar": ('hashtag-top-likers', '@username-followers') }, "interaction_users_amount": { "help": 'add this argument to select an amount of users from the interact-list ' @@ -125,11 +131,12 @@ class InteractBySourceActionRunner(CoreActionsRunner): "help": 'number of stories to watch for each user, disabled by default. ' 'It can be a number (e.g. 2) or a range (e.g. 2-4)', 'metavar': '3-8' - } + }, } likes_count = '2' follow_percentage = 0 + like_percentage = 100 interact = [] stories_count = '0' @@ -150,6 +157,9 @@ def set_params(self, args): if args.follow_percentage is not None: self.follow_percentage = int(args.follow_percentage) + if args.like_percentage is not None: + self.like_percentage = int(args.like_percentage) + if args.interaction_users_amount is not None: if len(self.interact) > 0: users_amount = get_value(args.interaction_users_amount, "Interaction user amount {}", 100) @@ -163,8 +173,8 @@ def set_params(self, args): self.interact.remove(random.choice(self.interact)) def run(self, device_wrapper, storage, session_state, on_action, is_limit_reached, is_passed_filters=None): - from insomniac.action_handle_blogger import handle_blogger - from insomniac.action_handle_hashtag import handle_hashtag + from insomniac.action_handle_blogger import handle_blogger, extract_blogger_instructions + from insomniac.action_handle_hashtag import handle_hashtag, extract_hashtag_instructions random.shuffle(self.interact) @@ -183,24 +193,30 @@ def run(self, device_wrapper, storage, session_state, on_action, is_limit_reache def job(): self.action_status.set(ActionState.RUNNING) if source[0] == '@': + source_name, instructions = extract_blogger_instructions(source) handle_blogger(device_wrapper.get(), - source[1:], # drop "@" + source_name[1:], # drop "@" + instructions, session_state, self.likes_count, self.stories_count, self.follow_percentage, + self.like_percentage, storage, on_action, is_limit_reached, is_passed_filters, self.action_status) elif source[0] == '#': + source_name, instructions = extract_hashtag_instructions(source) handle_hashtag(device_wrapper.get(), - source[1:], # drop "#" + source_name[1:], # drop "#" + instructions, session_state, self.likes_count, self.stories_count, self.follow_percentage, + self.like_percentage, storage, on_action, is_limit_reached, @@ -356,6 +372,11 @@ class InteractByTargetsActionRunner(CoreActionsRunner): "metavar": '50', "default": '0' }, + "like_percentage": { + "help": "likes given percentage of interacted users, 100 by default", + "metavar": '50', + "default": '100' + }, "stories_count": { "help": 'number of stories to watch for each user, disabled by default. ' 'It can be a number (e.g. 2) or a range (e.g. 2-4)', @@ -365,6 +386,7 @@ class InteractByTargetsActionRunner(CoreActionsRunner): likes_count = '2' follow_percentage = 0 + like_percentage = 100 stories_count = '0' def is_action_selected(self, args): @@ -380,6 +402,9 @@ def set_params(self, args): if args.follow_percentage is not None: self.follow_percentage = int(args.follow_percentage) + if args.like_percentage is not None: + self.like_percentage = int(args.like_percentage) + def run(self, device_wrapper, storage, session_state, on_action, is_limit_reached, is_passed_filters=None): from insomniac.action_handle_target import handle_target @@ -399,6 +424,7 @@ def job(): self.likes_count, self.stories_count, self.follow_percentage, + self.like_percentage, storage, on_action, is_limit_reached, diff --git a/insomniac/actions_types.py b/insomniac/actions_types.py index 303c54c..d7881ea 100644 --- a/insomniac/actions_types.py +++ b/insomniac/actions_types.py @@ -1,4 +1,5 @@ from collections import namedtuple +from enum import unique, Enum LikeAction = namedtuple('LikeAction', 'source user') FollowAction = namedtuple('FollowAction', 'source user') @@ -8,3 +9,15 @@ GetProfileAction = namedtuple('GetProfileAction', 'user') ScrapeAction = namedtuple('ScrapeAction', 'source user') RemoveMassFollowerAction = namedtuple('RemoveMassFollowerAction', 'user') + + +@unique +class BloggerInteractionType(Enum): + FOLLOWERS = 'followers' + FOLLOWING = 'following' + + +@unique +class HashtagInteractionType(Enum): + TOP_LIKERS = 'top-likers' + RECENT_LIKERS = 'recent-likers' diff --git a/insomniac/counters_parser.py b/insomniac/counters_parser.py index 42dea1a..9cad1d5 100644 --- a/insomniac/counters_parser.py +++ b/insomniac/counters_parser.py @@ -24,10 +24,10 @@ def parse(device, text): multiplier = 100000 try: count = int(float(text) * multiplier) - except ValueError: + except ValueError as ex: print_timeless(COLOR_FAIL + "Cannot parse \"" + text + "\". Probably wrong language, will set English now." + COLOR_ENDC) - save_crash(device) + save_crash(device, ex) switch_to_english(device) raise LanguageChangedException() return count diff --git a/insomniac/limits.py b/insomniac/limits.py index afa5f9e..82a85db 100644 --- a/insomniac/limits.py +++ b/insomniac/limits.py @@ -358,6 +358,41 @@ def update_state(self, action): pass +class MaxFollowing(CoreLimit): + LIMIT_ID = "max_following" + LIMIT_TYPE = LimitType.SESSION + LIMIT_ARGS = { + "max_following": { + "help": 'maximum amount of followings, after reaching this amount follow stops. disabled by default', + "metavar": '100' + } + } + + max_following_limit = None + + def set_limit(self, args): + if args.max_following is not None: + self.max_following_limit = int(args.max_following) + + def is_reached_for_action(self, action, session_state): + if self.max_following_limit is None: + return False + + if not type(action) == FollowAction: + return False + + initial_following = session_state.my_following_count + followed_count = session_state.sum(session_state.totalFollowed.values()) + + return initial_following + followed_count >= self.max_following_limit + + def reset(self): + pass + + def update_state(self, action): + pass + + class TotalGetProfileLimit(CoreLimit): LIMIT_ID = "total_get_profile_limit" LIMIT_TYPE = LimitType.SESSION @@ -391,6 +426,41 @@ def update_state(self, action): pass +class SessionTimeMaxLengthLimit(CoreLimit): + LIMIT_ID = "session_length_in_mins_limit" + LIMIT_TYPE = LimitType.SESSION + LIMIT_ARGS = { + "session_length_in_mins_limit": { + "help": "limit the session length by time (minutes), disabled by default. " + "It can be a number (e.g. 60) or a range (e.g. 40-70)", + "metavar": "50-60" + } + } + + session_length_in_mins_limit = None + + def set_limit(self, args): + if args.session_length_in_mins_limit is not None: + self.session_length_in_mins_limit = get_value(args.session_length_in_mins_limit, "Session max-length (minutes): {}", 60) + + def is_reached_for_action(self, action, session_state): + if self.session_length_in_mins_limit is None: + return False + + # Apply this limit for every action (no action-type condition) + + delta = datetime.now() - session_state.startTime + mins_delta = delta.seconds // 60 + + return mins_delta > self.session_length_in_mins_limit + + def reset(self): + pass + + def update_state(self, action): + pass + + def get_core_limits_classes(): return CoreLimit.__subclasses__() diff --git a/insomniac/navigation.py b/insomniac/navigation.py index 84752b4..5aa4008 100644 --- a/insomniac/navigation.py +++ b/insomniac/navigation.py @@ -1,6 +1,7 @@ from enum import Enum, unique from insomniac.actions_types import GetProfileAction +from insomniac.sleeper import sleeper from insomniac.utils import * SEARCH_CONTENT_DESC_REGEX = '[Ss]earch and [Ee]xplore' @@ -36,7 +37,7 @@ def search_for(device, username=None, hashtag=None, on_action=None): className='android.widget.TextView', text=username) - random_sleep() + sleeper.random_sleep() if not username_view.exists(): print_timeless(COLOR_FAIL + "Cannot find user @" + username + ", abort." + COLOR_ENDC) return False @@ -62,7 +63,7 @@ def search_for(device, username=None, hashtag=None, on_action=None): className='android.widget.TextView', text=f"#{hashtag}") - random_sleep() + sleeper.random_sleep() if not hashtag_view.exists(): print_timeless(COLOR_FAIL + "Cannot find hashtag #" + hashtag + ", abort." + COLOR_ENDC) return False diff --git a/insomniac/persistent_list.py b/insomniac/persistent_list.py index af6ab47..8b4c66c 100644 --- a/insomniac/persistent_list.py +++ b/insomniac/persistent_list.py @@ -18,7 +18,7 @@ def persist(self, directory): if not os.path.exists(directory): os.makedirs(directory) - path = directory + "/" + self.filename + ".json" + path = os.path.join(directory, self.filename + ".json") if os.path.exists(path): with open(path) as json_file: diff --git a/insomniac/report.py b/insomniac/report.py index 2a13def..8a3981a 100644 --- a/insomniac/report.py +++ b/insomniac/report.py @@ -84,11 +84,13 @@ def print_short_report(source, session_state): total_likes = session_state.totalLikes total_followed = sum(session_state.totalFollowed.values()) interactions = session_state.successfulInteractions.get(source, 0) + total_successful_interactions = sum(session_state.successfulInteractions.values()) total_story_views = session_state.totalStoriesWatched print(COLOR_REPORT + "Session progress: " + str(total_likes) + " likes, " + str(total_followed) + " followed, " + str(total_story_views) + " stories watched, " + str(interactions) + " successful " + ("interaction" if interactions == 1 else "interactions") + - " for " + source + COLOR_ENDC) + " for " + source + "," + + " " + str(total_successful_interactions) + " successful interactions for the entire session" + COLOR_ENDC) def print_interaction_types(username, can_like, can_follow, can_watch): diff --git a/insomniac/safely_runner.py b/insomniac/safely_runner.py index 7bec940..ff053f6 100644 --- a/insomniac/safely_runner.py +++ b/insomniac/safely_runner.py @@ -1,11 +1,11 @@ import sys -import traceback from http.client import HTTPException from socket import timeout from insomniac.device_facade import DeviceFacade from insomniac.navigation import navigate, Tabs, LanguageChangedException from insomniac.report import print_full_report +from insomniac.sleeper import sleeper from insomniac.utils import * @@ -24,21 +24,22 @@ def wrapper(*args, **kwargs): print_full_report(sessions) sessions.persist(directory=session_state.my_username) sys.exit(0) - except (DeviceFacade.JsonRpcError, IndexError, HTTPException, timeout): + except (DeviceFacade.JsonRpcError, IndexError, HTTPException, timeout) as ex: print(COLOR_FAIL + traceback.format_exc() + COLOR_ENDC) - save_crash(device_wrapper.get()) + 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) - random_sleep() + sleeper.random_sleep() open_instagram(device_wrapper.device_id) + sleeper.random_sleep() navigate(device_wrapper.get(), Tabs.PROFILE) except LanguageChangedException: print_timeless("") print("Language was changed. We'll have to start from the beginning.") navigate(device_wrapper.get(), Tabs.PROFILE) except Exception as e: - save_crash(device_wrapper.get()) + save_crash(device_wrapper.get(), e) close_instagram(device_wrapper.device_id) print_full_report(sessions) sessions.persist(directory=session_state.my_username) diff --git a/insomniac/session.py b/insomniac/session.py index 6255057..d6ba743 100644 --- a/insomniac/session.py +++ b/insomniac/session.py @@ -1,5 +1,6 @@ import random import sys +from time import sleep import colorama @@ -12,6 +13,8 @@ from insomniac.persistent_list import PersistentList from insomniac.report import print_full_report from insomniac.session_state import SessionStateEncoder, SessionState +from insomniac.sleeper import sleeper +from insomniac.storage import STORAGE_ARGS from insomniac.storage import Storage from insomniac.utils import * @@ -29,6 +32,10 @@ class InsomniacSession(object): "help": 'device identifier. Should be used only when multiple devices are connected at once', "metavar": '2443de990e017ece' }, + "no_speed_check": { + 'help': 'skip internet speed check at start', + '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', @@ -53,6 +60,7 @@ def __init__(self): def get_session_args(self): all_args = {} all_args.update(self.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()) @@ -65,24 +73,28 @@ def set_session_args(self, args): def parse_args_and_get_device_wrapper(self): ok, args = parse_arguments(self.get_session_args()) if not ok: - return None, None + return None, None, None device_wrapper = DeviceWrapper(args.device, args.old) device = device_wrapper.get() if device is None: - return None, None + return None, None, None + + app_version = get_instagram_version(args.device) - print("Instagram version: " + get_instagram_version(args.device)) + print("Instagram version: " + app_version) - return args, device_wrapper + return args, device_wrapper, app_version - def start_session(self, args, device_wrapper): + def start_session(self, args, device_wrapper, app_version): self.session_state = SessionState() self.session_state.args = args.__dict__ + self.session_state.app_version = app_version self.sessions.append(self.session_state) print_timeless(COLOR_REPORT + "\n-------- START: " + str(self.session_state.startTime) + " --------" + COLOR_ENDC) open_instagram(args.device) + sleeper.random_sleep() self.session_state.my_username, \ self.session_state.my_followers_count, \ self.session_state.my_following_count = get_my_profile_info(device_wrapper.get()) @@ -113,12 +125,16 @@ def on_action_callback(self, action): self.limits_mgr.update_state(action) def run(self): - args, device_wrapper = self.parse_args_and_get_device_wrapper() + args, device_wrapper, app_version = self.parse_args_and_get_device_wrapper() + if args is None or device_wrapper is None: + return - while True: - if args is None or device_wrapper is None: - return + if 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() + while True: self.set_session_args(args) action_runner = self.actions_mgr.select_action_runner(args) @@ -130,7 +146,7 @@ def run(self): self.limits_mgr.set_limits(args) try: - self.start_session(args, device_wrapper) + self.start_session(args, device_wrapper, app_version) self.storage = Storage(self.session_state.my_username, args) action_runner.run(device_wrapper, @@ -149,7 +165,7 @@ def run(self): raise ex else: print_timeless(COLOR_FAIL + f"\nCaught an exception:\n{ex}" + COLOR_ENDC) - save_crash(device_wrapper.get()) + save_crash(device_wrapper.get(), ex) if self.repeat is not None: self.repeat_session(args) diff --git a/insomniac/session_state.py b/insomniac/session_state.py index c00b59b..f9bead8 100644 --- a/insomniac/session_state.py +++ b/insomniac/session_state.py @@ -9,6 +9,7 @@ class SessionState: id = None args = {} + app_version = None my_username = None my_followers_count = None my_following_count = None @@ -25,6 +26,7 @@ class SessionState: def __init__(self): self.id = str(uuid.uuid4()) self.args = {} + self.app_version = None self.my_username = None self.my_followers_count = None self.my_following_count = None @@ -89,6 +91,7 @@ class SessionStateEncoder(JSONEncoder): def default(self, session_state: SessionState): return { "id": session_state.id, + "app_version": session_state.app_version, "total_interactions": sum(session_state.totalInteractions.values()), "successful_interactions": sum(session_state.successfulInteractions.values()), "total_followed": sum(session_state.totalFollowed.values()), diff --git a/insomniac/sleeper.py b/insomniac/sleeper.py new file mode 100644 index 0000000..b4056d5 --- /dev/null +++ b/insomniac/sleeper.py @@ -0,0 +1,77 @@ +from random import uniform +from time import sleep + +import speedtest + +from insomniac.utils import * + +MEGABIT = 1000000 +SPEED_GOOD = 25 * MEGABIT +SPEED_BAD = 10 * MEGABIT +SPEED_UGLY = 1 * MEGABIT +SPEED_ZERO = 0 + +# Dict of sleep ranges by Internet speed +SLEEP_RANGE_BY_SPEED = { + SPEED_GOOD: (1, 3), + SPEED_BAD: (2, 5), + SPEED_UGLY: (4, 8), + SPEED_ZERO: (7, 12) +} + + +class Sleeper: + sleep_range_start = 1.0 + sleep_range_end = 4.0 + + def __init__(self): + pass + + def random_sleep(self): + delay = uniform(self.sleep_range_start, self.sleep_range_end) + print(f"Sleep for {delay:.2f} seconds") + sleep(delay) + + def update_random_sleep_range(self): + speed = _get_internet_speed() + if SPEED_ZERO <= speed <= SPEED_UGLY: + s1 = SPEED_ZERO + s2 = SPEED_UGLY + elif SPEED_UGLY <= speed <= SPEED_BAD: + s1 = SPEED_UGLY + s2 = SPEED_BAD + elif SPEED_BAD <= speed <= SPEED_GOOD: + s1 = SPEED_BAD + s2 = SPEED_GOOD + else: + s1 = SPEED_GOOD + s2 = SPEED_GOOD + + start1, end1 = SLEEP_RANGE_BY_SPEED[s1] + start2, end2 = SLEEP_RANGE_BY_SPEED[s2] + x = (speed - s1) / (s2 - s1) # x is a value between [0, 1] + + self.sleep_range_start = start1 + x * (start2 - start1) + self.sleep_range_end = end1 + x * (end2 - end1) + + print(f"Sleep range will be from {self.sleep_range_start:.2f} to {self.sleep_range_end:.2f} seconds") + + +def _get_internet_speed(): + try: + s = speedtest.Speedtest() + s.get_best_server() + s.download(threads=1) + s.upload(threads=1) + results_dict = s.results.dict() + except speedtest.SpeedtestException: + print(COLOR_FAIL + "Failed to determine Internet speed, supposing it's zero" + COLOR_ENDC) + return SPEED_ZERO + + download_speed = results_dict['download'] + upload_speed = results_dict['upload'] + print(f"Download speed {(download_speed / MEGABIT):.2f} Mbit/s, upload speed {(upload_speed / MEGABIT):.2f} Mbit/s") + return (download_speed + upload_speed) / 2 + + +sleeper = Sleeper() diff --git a/insomniac/storage.py b/insomniac/storage.py index aa88338..053c019 100644 --- a/insomniac/storage.py +++ b/insomniac/storage.py @@ -17,7 +17,17 @@ FILENAME_FOLLOWERS = "followers.txt" +STORAGE_ARGS = { + "reinteract_after": { + "help": "set a time (in hours) to wait before re-interact with an already interacted profile, disabled by default (won't interact again). " + "It can be a number (e.g. 48) or a range (e.g. 50-80)", + "metavar": "150" + } +} + + class Storage: + reinteract_after = None interacted_users_path = None interacted_users = {} scrapped_users = {} @@ -28,6 +38,9 @@ class Storage: account_followers = {} def __init__(self, my_username, args): + if args.reinteract_after is not None: + self.reinteract_after = get_value(args.reinteract_after, "Re-interact after {} hours", 168) + scrape_for_account = args.__dict__.get('scrape_for_account', None) if my_username is None: print(COLOR_FAIL + "No username, thus the script won't get access to interacted users and sessions data" + @@ -37,30 +50,30 @@ def __init__(self, my_username, args): if not os.path.exists(my_username): os.makedirs(my_username) - self.interacted_users_path = my_username + "/" + FILENAME_INTERACTED_USERS + self.interacted_users_path = os.path.join(my_username, FILENAME_INTERACTED_USERS) if os.path.exists(self.interacted_users_path): with open(self.interacted_users_path, encoding="utf-8") as json_file: self.interacted_users = json.load(json_file) - self.scrapped_users_path = my_username + "/" + FILENAME_SCRAPPED_USERS + self.scrapped_users_path = os.path.join(my_username, FILENAME_SCRAPPED_USERS) if os.path.exists(self.scrapped_users_path): with open(self.scrapped_users_path, encoding="utf-8") as json_file: self.scrapped_users = json.load(json_file) - self.filtered_users_path = my_username + "/" + FILENAME_FILTERED_USERS + self.filtered_users_path = os.path.join(my_username, FILENAME_FILTERED_USERS) if os.path.exists(self.filtered_users_path): with open(self.filtered_users_path, encoding="utf-8") as json_file: self.filtered_users = json.load(json_file) - whitelist_path = my_username + "/" + FILENAME_WHITELIST + whitelist_path = os.path.join(my_username, FILENAME_WHITELIST) if os.path.exists(whitelist_path): with open(whitelist_path, encoding="utf-8") as file: self.whitelist = [line.rstrip() for line in file] - blacklist_path = my_username + "/" + FILENAME_BLACKLIST + blacklist_path = os.path.join(my_username, FILENAME_BLACKLIST) if os.path.exists(blacklist_path): with open(blacklist_path, encoding="utf-8") as file: self.blacklist = [line.rstrip() for line in file] - self.targets_path = my_username + "/" + FILENAME_TARGETS + self.targets_path = os.path.join(my_username, FILENAME_TARGETS) if os.path.exists(self.targets_path): with open(self.targets_path, encoding="utf-8") as file: self.targets = [line.rstrip() for line in file] @@ -68,26 +81,29 @@ def __init__(self, my_username, args): if scrape_for_account is not None: if not os.path.isdir(scrape_for_account): os.makedirs(scrape_for_account) - self.targets_path = scrape_for_account + "/" + FILENAME_TARGETS + self.targets_path = os.path.join(scrape_for_account, FILENAME_TARGETS) if os.path.exists(self.targets_path): with open(self.targets_path, encoding="utf-8") as file: self.targets = [line.rstrip() for line in file] - self.followers_path = scrape_for_account + "/" + FILENAME_FOLLOWERS + self.followers_path = os.path.join(scrape_for_account, FILENAME_FOLLOWERS) if os.path.exists(self.followers_path): with open(self.followers_path, encoding="utf-8") as json_file: self.account_followers = json.load(json_file) def check_user_was_interacted(self, username): - return not self.interacted_users.get(username) is None + if self.reinteract_after is None: + return not self.interacted_users.get(username) is None + + return self.check_user_was_interacted_recently(username, hours=self.reinteract_after) - def check_user_was_interacted_recently(self, username): + def check_user_was_interacted_recently(self, username, hours=72): user = self.interacted_users.get(username) if user is None: return False last_interaction = datetime.strptime(user[USER_LAST_INTERACTION], '%Y-%m-%d %H:%M:%S.%f') - return datetime.now() - last_interaction <= timedelta(days=3) + return datetime.now() - last_interaction <= timedelta(hours=hours) def check_user_was_scrapped(self, username): return not self.scrapped_users.get(username) is None diff --git a/insomniac/utils.py b/insomniac/utils.py index f86dece..e1b15fa 100644 --- a/insomniac/utils.py +++ b/insomniac/utils.py @@ -6,8 +6,8 @@ import urllib.request from datetime import datetime from random import randint -from time import sleep from urllib.error import URLError +import traceback from insomniac.__version__ import __version__ @@ -64,17 +64,10 @@ def check_adb_connection(is_device_id_provided): return is_ok -def random_sleep(): - delay = randint(1, 4) - print("Sleep for " + str(delay) + (delay == 1 and " second" or " seconds")) - sleep(delay) - - def open_instagram(device_id): print("Open Instagram app") os.popen("adb" + ("" if device_id is None else " -s " + device_id) + " shell am start -n com.instagram.android/com.instagram.mainactivity.MainActivity").close() - random_sleep() def close_instagram(device_id): @@ -83,33 +76,37 @@ def close_instagram(device_id): " shell am force-stop com.instagram.android").close() -def save_crash(device): +def save_crash(device, ex=None): global print_log directory_name = "Crash-" + datetime.now().strftime("%Y-%m-%d-%H-%M-%S") try: - os.makedirs("crashes/" + directory_name + "/", exist_ok=False) + os.makedirs(os.path.join("crashes", directory_name), exist_ok=False) except OSError: print(COLOR_FAIL + "Directory " + directory_name + " already exists." + COLOR_ENDC) return screenshot_format = ".png" if device.is_old() else ".jpg" try: - device.screenshot("crashes/" + directory_name + "/screenshot" + screenshot_format) + device.screenshot(os.path.join("crashes", directory_name, "screenshot" + screenshot_format)) except RuntimeError: print(COLOR_FAIL + "Cannot save screenshot." + COLOR_ENDC) view_hierarchy_format = ".xml" try: - device.dump_hierarchy("crashes/" + directory_name + "/view_hierarchy" + view_hierarchy_format) + device.dump_hierarchy(os.path.join("crashes", directory_name, "view_hierarchy" + view_hierarchy_format)) except RuntimeError: print(COLOR_FAIL + "Cannot save view hierarchy." + COLOR_ENDC) - with open("crashes/" + directory_name + "/logs.txt", 'w', encoding="utf-8") as outfile: + with open(os.path.join("crashes", directory_name, "logs.txt"), 'w', encoding="utf-8") as outfile: outfile.write(print_log) - shutil.make_archive("crashes/" + directory_name, 'zip', "crashes/" + directory_name + "/") - shutil.rmtree("crashes/" + directory_name + "/") + if ex: + outfile.write("\n") + outfile.write(''.join(traceback.format_exception(etype=type(ex), value=ex, tb=ex.__traceback__))) + + shutil.make_archive(os.path.join("crashes", directory_name), 'zip', os.path.join("crashes", directory_name)) + shutil.rmtree(os.path.join("crashes", directory_name)) print(COLOR_OKGREEN + "Crash saved as \"crashes/" + directory_name + ".zip\"." + COLOR_ENDC) print(COLOR_OKGREEN + "Please attach this file if you gonna report the crash at" + COLOR_ENDC) diff --git a/requirements.txt b/requirements.txt index 628c4b7..4c1918d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,3 +2,4 @@ uiautomator2 uiautomator colorama croniter +speedtest diff --git a/setup.py b/setup.py index 5b731ad..caf3919 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from insomniac.__version__ import __version__ -with open("README.md", "r") as fh: +with open("README.md", "r", errors='ignore') as fh: long_description = fh.read() with open("requirements.txt", "r") as file: