InstaAddict 1.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. GramAddict/__init__.py +10 -0
  2. GramAddict/__main__.py +161 -0
  3. GramAddict/core/__init__.py +0 -0
  4. GramAddict/core/bot_flow.py +425 -0
  5. GramAddict/core/config.py +219 -0
  6. GramAddict/core/decorators.py +144 -0
  7. GramAddict/core/device_facade.py +743 -0
  8. GramAddict/core/download_from_github.py +263 -0
  9. GramAddict/core/filter.py +759 -0
  10. GramAddict/core/handle_sources.py +890 -0
  11. GramAddict/core/interaction.py +1034 -0
  12. GramAddict/core/log.py +152 -0
  13. GramAddict/core/navigation.py +120 -0
  14. GramAddict/core/persistent_list.py +56 -0
  15. GramAddict/core/plugin_loader.py +46 -0
  16. GramAddict/core/report.py +206 -0
  17. GramAddict/core/resources.py +245 -0
  18. GramAddict/core/scroll_end_detector.py +77 -0
  19. GramAddict/core/session_state.py +323 -0
  20. GramAddict/core/storage.py +254 -0
  21. GramAddict/core/utils.py +804 -0
  22. GramAddict/core/views.py +2207 -0
  23. GramAddict/plugins/__init__.py +0 -0
  24. GramAddict/plugins/action_unfollow_followers.py +570 -0
  25. GramAddict/plugins/cloned_app.py +25 -0
  26. GramAddict/plugins/core_arguments.py +411 -0
  27. GramAddict/plugins/data_analytics.py +40 -0
  28. GramAddict/plugins/interact_blogger.py +258 -0
  29. GramAddict/plugins/interact_blogger_followers.py +196 -0
  30. GramAddict/plugins/interact_blogger_post_likers.py +184 -0
  31. GramAddict/plugins/interact_feed.py +147 -0
  32. GramAddict/plugins/interact_hashtag_likers.py +199 -0
  33. GramAddict/plugins/interact_hashtag_posts.py +192 -0
  34. GramAddict/plugins/interact_place_likers.py +194 -0
  35. GramAddict/plugins/interact_place_posts.py +185 -0
  36. GramAddict/plugins/like_from_urls.py +137 -0
  37. GramAddict/plugins/plugin.example +38 -0
  38. GramAddict/plugins/remove_followers.py +122 -0
  39. GramAddict/plugins/telegram.py +249 -0
  40. GramAddict/version.py +2 -0
  41. instaaddict-1.0.1.dist-info/METADATA +335 -0
  42. instaaddict-1.0.1.dist-info/RECORD +45 -0
  43. instaaddict-1.0.1.dist-info/WHEEL +4 -0
  44. instaaddict-1.0.1.dist-info/entry_points.txt +3 -0
  45. instaaddict-1.0.1.dist-info/licenses/LICENSE +23 -0
GramAddict/__init__.py ADDED
@@ -0,0 +1,10 @@
1
+ """InstaAddict - Human-like Instagram bot powered by UIAutomator2"""
2
+
3
+ __version__ = "1.0.1"
4
+ __tested_ig_version__ = "438.0.0.28.88"
5
+
6
+ from GramAddict.core.bot_flow import start_bot
7
+
8
+
9
+ def run(**kwargs):
10
+ start_bot(**kwargs)
GramAddict/__main__.py ADDED
@@ -0,0 +1,161 @@
1
+ import argparse
2
+ from os import getcwd, path
3
+
4
+ from GramAddict import __version__
5
+ from GramAddict.core.bot_flow import start_bot
6
+ from GramAddict.core.download_from_github import download_from_github
7
+
8
+
9
+ def cmd_init(args):
10
+ if args.account_name is not None:
11
+ print(f"Script launched in {getcwd()}, files will be available there.")
12
+ for username in args.account_name:
13
+ if not path.exists("./run.py"):
14
+ print("Creating run.py ...")
15
+ download_from_github(
16
+ "https://github.com/joeahkim/InstaAddict/blob/master/run.py"
17
+ )
18
+ if not path.exists(f"./accounts/{username}"):
19
+ print(
20
+ f"Creating 'accounts/{username}' folder with a config starting point inside. You have to edit these files according with https://docs.gramaddict.org/#/configuration"
21
+ )
22
+ download_from_github(
23
+ "https://github.com/joeahkim/InstaAddict/tree/master/config-examples",
24
+ output_dir=f"accounts/{username}",
25
+ flatten=True,
26
+ )
27
+ else:
28
+ print(f"'accounts/{username}' folder already exists, skip.")
29
+ continue
30
+ with open(f"./accounts/{username}/config.yml", "r+", encoding="utf-8") as f:
31
+ config = f.read()
32
+ f.seek(0)
33
+ config_fixed = config.replace("myusername", username)
34
+ f.write(config_fixed)
35
+ else:
36
+ print("You have to provide at last one account name..")
37
+
38
+
39
+ def cmd_run(args):
40
+ start_bot()
41
+
42
+
43
+ def cmd_dump(args):
44
+ import os
45
+ import shutil
46
+ import time
47
+
48
+ import uiautomator2 as u2
49
+ from colorama import Fore, Style
50
+
51
+ if not args.no_kill:
52
+ os.popen("adb shell pkill atx-agent").close()
53
+ try:
54
+ d = u2.connect(args.device)
55
+ except RuntimeError as err:
56
+ raise SystemExit(err)
57
+
58
+ def dump_hierarchy(device, path):
59
+ xml_dump = device.dump_hierarchy()
60
+ with open(path, "w", encoding="utf-8") as outfile:
61
+ outfile.write(xml_dump)
62
+
63
+ def make_archive(name):
64
+ os.chdir("dump")
65
+ shutil.make_archive(base_name=f"screen_{name}", format="zip", root_dir="cur")
66
+ shutil.rmtree("cur")
67
+
68
+ os.makedirs("dump/cur", exist_ok=True)
69
+ d.screenshot("dump/cur/screenshot.png")
70
+ dump_hierarchy(d, "dump/cur/hierarchy.xml")
71
+ archive_name = int(time.time())
72
+ make_archive(archive_name)
73
+ print(
74
+ Fore.GREEN
75
+ + Style.BRIGHT
76
+ + "\nCurrent screen dump generated successfully! Please, send me this file:"
77
+ )
78
+ print(Fore.BLUE + Style.BRIGHT + f"{os.getcwd()}\\screen_{archive_name}.zip")
79
+
80
+
81
+ _commands = [
82
+ dict(
83
+ action=cmd_init,
84
+ command="init",
85
+ help="creates your account folder under accounts with files for configuration",
86
+ flags=[
87
+ dict(
88
+ args=["account_name"],
89
+ nargs="+",
90
+ help="instagram account name to initialize",
91
+ ),
92
+ ],
93
+ ),
94
+ dict(
95
+ action=cmd_run,
96
+ command="run",
97
+ help="start the bot!",
98
+ flags=[
99
+ dict(args=["--config"], nargs="?", help="provide the config.yml path"),
100
+ ],
101
+ ),
102
+ dict(
103
+ action=cmd_dump,
104
+ command="dump",
105
+ help="dump current screen",
106
+ flags=[
107
+ dict(
108
+ args=["--device"],
109
+ nargs=None,
110
+ default=None,
111
+ help="provide the device name if more then one connected",
112
+ ),
113
+ dict(
114
+ args=["--no-kill"],
115
+ action="store_true",
116
+ help="don't kill the uia2 demon",
117
+ ),
118
+ ],
119
+ ),
120
+ ]
121
+
122
+
123
+ def main() -> None:
124
+ parser = argparse.ArgumentParser(
125
+ prog="GramAddict",
126
+ description="free human-like Instagram bot",
127
+ )
128
+ parser.add_argument(
129
+ "-v", "--version", action="version", version=f"{parser.prog} {__version__}"
130
+ )
131
+ subparser = parser.add_subparsers(dest="subparser")
132
+ actions = {}
133
+ for c in _commands:
134
+ cmd_name = c["command"]
135
+ actions[cmd_name] = c["action"]
136
+ sp = subparser.add_parser(
137
+ cmd_name,
138
+ help=c.get("help"),
139
+ formatter_class=argparse.ArgumentDefaultsHelpFormatter,
140
+ )
141
+ for f in c.get("flags", []):
142
+ args = f.get("args")
143
+ if not args:
144
+ args = ["-" * min(2, len(n)) + n for n in f["name"]]
145
+ kwargs = f.copy()
146
+ kwargs.pop("name", None)
147
+ kwargs.pop("args", None)
148
+ kwargs.pop("run", None)
149
+ sp.add_argument(*args, **kwargs)
150
+
151
+ args = parser.parse_args()
152
+
153
+ if args.subparser:
154
+ actions[args.subparser](args)
155
+ return
156
+
157
+ parser.print_help()
158
+
159
+
160
+ if __name__ == "__main__":
161
+ main()
File without changes
@@ -0,0 +1,425 @@
1
+ import logging
2
+ import random
3
+ from datetime import datetime, timedelta
4
+ from time import sleep
5
+
6
+ from colorama import Fore, Style
7
+
8
+ from GramAddict import __tested_ig_version__
9
+ from GramAddict.core.config import Config
10
+ from GramAddict.core.device_facade import create_device, get_device_info
11
+ from GramAddict.core.filter import Filter
12
+ from GramAddict.core.filter import load_config as load_filter
13
+ from GramAddict.core.interaction import load_config as load_interaction
14
+ from GramAddict.core.log import (
15
+ configure_logger,
16
+ is_log_file_updated,
17
+ update_log_file_name,
18
+ )
19
+ from GramAddict.core.navigation import check_if_english
20
+ from GramAddict.core.persistent_list import PersistentList
21
+ from GramAddict.core.report import print_full_report
22
+ from GramAddict.core.session_state import SessionState, SessionStateEncoder
23
+ from GramAddict.core.storage import Storage
24
+ from GramAddict.core.utils import (
25
+ ask_for_a_donation,
26
+ can_repeat,
27
+ check_adb_connection,
28
+ check_if_updated,
29
+ check_screen_timeout,
30
+ close_instagram,
31
+ config_examples,
32
+ countdown,
33
+ get_instagram_version,
34
+ get_value,
35
+ head_up_notifications,
36
+ kill_atx_agent,
37
+ )
38
+ from GramAddict.core.utils import load_config as load_utils
39
+ from GramAddict.core.utils import (
40
+ move_usernames_to_accounts,
41
+ open_instagram,
42
+ pre_post_script,
43
+ print_telegram_reports,
44
+ restart_atx_agent,
45
+ save_crash,
46
+ set_time_delta,
47
+ show_ending_conditions,
48
+ stop_bot,
49
+ wait_for_next_session,
50
+ )
51
+ from GramAddict.core.views import AccountView, ProfileView, TabBarView, UniversalActions
52
+ from GramAddict.core.views import load_config as load_views
53
+
54
+
55
+ def start_bot(**kwargs):
56
+ # Logging initialization
57
+ logger = logging.getLogger(__name__)
58
+
59
+ # Pre-Load Config
60
+ configs = Config(first_run=True, **kwargs)
61
+ configure_logger(configs.debug, configs.username)
62
+ if not kwargs:
63
+ if "--config" not in configs.args:
64
+ logger.info(
65
+ "It's strongly recommend to use a config.yml file. Follow these links for more details: https://docs.gramaddict.org/#/configuration and https://github.com/joeahkim/Insta-Addict/tree/develop/config-examples",
66
+ extra={"color": f"{Fore.GREEN}{Style.BRIGHT}"},
67
+ )
68
+ sleep(3)
69
+
70
+ # Config-example hint
71
+ config_examples()
72
+
73
+ # Check for updates
74
+ check_if_updated()
75
+
76
+ # Move username folders to a main directory -> accounts
77
+ if "--move-folders-in-accounts" in configs.args:
78
+ move_usernames_to_accounts()
79
+
80
+ # Global Variables
81
+ sessions = PersistentList("sessions", SessionStateEncoder)
82
+
83
+ # Load Config
84
+ configs.load_plugins()
85
+ configs.parse_args()
86
+ # Some plugins need config values without being passed
87
+ # through. Because we do a weird config/argparse hybrid,
88
+ # we need to load the configs in a weird way
89
+ load_filter(configs)
90
+ load_interaction(configs)
91
+ load_utils(configs)
92
+ load_views(configs)
93
+
94
+ if not configs.args or not check_adb_connection():
95
+ return
96
+
97
+ if len(configs.enabled) < 1:
98
+ logger.error(
99
+ "You have to specify one of these actions: " + ", ".join(configs.actions)
100
+ )
101
+ return
102
+ device = create_device(configs.device_id, configs.app_id)
103
+ session_state = None
104
+ if str(configs.args.total_sessions) != "-1":
105
+ total_sessions = get_value(configs.args.total_sessions, None, -1)
106
+ else:
107
+ total_sessions = -1
108
+
109
+ # init
110
+ analytics_at_end = False
111
+ telegram_reports_at_end = False
112
+ followers_now = None
113
+ following_now = None
114
+
115
+ while True:
116
+ set_time_delta(configs.args)
117
+ inside_working_hours, time_left = SessionState.inside_working_hours(
118
+ configs.args.working_hours, configs.args.time_delta_session
119
+ )
120
+ if not inside_working_hours:
121
+ wait_for_next_session(time_left, session_state, sessions, device)
122
+ pre_post_script(path=configs.args.pre_script)
123
+ if configs.args.restart_atx_agent:
124
+ restart_atx_agent(device)
125
+ get_device_info(device)
126
+ session_state = SessionState(configs)
127
+ session_state.set_limits_session()
128
+ sessions.append(session_state)
129
+ check_screen_timeout()
130
+ device.wake_up()
131
+ head_up_notifications(enabled=False)
132
+ logger.info(
133
+ "-------- START: "
134
+ + str(session_state.startTime.strftime("%H:%M:%S - %Y/%m/%d"))
135
+ + " --------",
136
+ extra={"color": f"{Style.BRIGHT}{Fore.YELLOW}"},
137
+ )
138
+
139
+ if not device.get_info()["screenOn"]:
140
+ device.press_power()
141
+ if device.is_screen_locked():
142
+ device.unlock()
143
+ if device.is_screen_locked():
144
+ logger.error(
145
+ "Can't unlock your screen. There may be a passcode on it. If you would like your screen to be turned on and unlocked automatically, please remove the passcode."
146
+ )
147
+ stop_bot(device, sessions, session_state, was_sleeping=False)
148
+
149
+ logger.info("Device screen ON and unlocked.")
150
+ if open_instagram(device):
151
+ try:
152
+ running_ig_version = get_instagram_version()
153
+ logger.info(f"Instagram version: {running_ig_version}")
154
+ if tuple(running_ig_version.split(".")) > tuple(
155
+ __tested_ig_version__.split(".")
156
+ ):
157
+ logger.warning(
158
+ f"You have a newer version of IG then the one tested! (Tested version: {__tested_ig_version__}).",
159
+ extra={"color": f"{Style.BRIGHT}"},
160
+ )
161
+ logger.warning(
162
+ "Using an untested version of IG would cause unexpected behavior because some elements in the user interface may have been changed. Any crashes that occur with an untested version are not taken into account."
163
+ )
164
+ if not configs.args.allow_untested_ig_version:
165
+ logger.warning(
166
+ "If you press ENTER, you are aware of this and will not ask for support in case of a crash."
167
+ )
168
+ logger.warning(
169
+ "If you want to avoid pressing ENTER next run, add allow-untested-ig-version: true in your config.yml file. (read the docs for more info)"
170
+ )
171
+ input()
172
+
173
+ except Exception as e:
174
+ logger.error(f"Error retrieving the IG version. Exception: {e}")
175
+
176
+ UniversalActions.close_keyboard(device)
177
+ else:
178
+ break
179
+ profile_view = ProfileView(device)
180
+ account_view = AccountView(device)
181
+ tab_bar_view = TabBarView(device)
182
+ try:
183
+ account_view.navigate_to_main_account()
184
+ check_if_english(device)
185
+ if configs.args.username is not None:
186
+ success = account_view.changeToUsername(configs.args.username)
187
+ if not success:
188
+ logger.error(
189
+ f"Not able to change to {configs.args.username}, abort!"
190
+ )
191
+ save_crash(device)
192
+ device.back()
193
+ break
194
+ account_view.refresh_account()
195
+ (
196
+ session_state.my_username,
197
+ session_state.my_posts_count,
198
+ session_state.my_followers_count,
199
+ session_state.my_following_count,
200
+ ) = profile_view.getProfileInfo()
201
+ except Exception as e:
202
+ logger.error(f"Exception: {e}")
203
+ save_crash(device)
204
+ break
205
+
206
+ if (
207
+ session_state.my_username is None
208
+ or session_state.my_posts_count is None
209
+ or session_state.my_followers_count is None
210
+ or session_state.my_following_count is None
211
+ ):
212
+ logger.critical(
213
+ "Could not get one of the following from your profile: username, # of posts, # of followers, # of followings. This is typically due to a soft-ban. Review the crash screenshot to see if this is the case."
214
+ )
215
+ logger.critical(
216
+ f"Username: {session_state.my_username}, Posts: {session_state.my_posts_count}, Followers: {session_state.my_followers_count}, Following: {session_state.my_following_count}"
217
+ )
218
+ save_crash(device)
219
+ stop_bot(device, sessions, session_state)
220
+
221
+ if not is_log_file_updated():
222
+ try:
223
+ update_log_file_name(session_state.my_username)
224
+ except Exception as e:
225
+ logger.error(
226
+ f"Failed to update log file name. Will continue anyway. {e}"
227
+ )
228
+ report_string = f"Hello, @{session_state.my_username}! You have {session_state.my_followers_count} followers and {session_state.my_following_count} followings so far."
229
+ logger.info(report_string, extra={"color": f"{Style.BRIGHT}{Fore.GREEN}"})
230
+ if configs.args.repeat:
231
+ logger.info(
232
+ f"You have {total_sessions + 1 - len(sessions) if total_sessions > 0 else 'infinite'} session(s) left. You can stop the bot by pressing CTRL+C in console.",
233
+ extra={"color": f"{Style.BRIGHT}{Fore.BLUE}"},
234
+ )
235
+ sleep(3)
236
+ if configs.args.shuffle_jobs:
237
+ jobs_list = random.sample(configs.enabled, len(configs.enabled))
238
+ else:
239
+ jobs_list = configs.enabled
240
+
241
+ if "analytics" in jobs_list:
242
+ jobs_list.remove("analytics")
243
+ if configs.args.analytics:
244
+ analytics_at_end = True
245
+ if "telegram-reports" in jobs_list:
246
+ jobs_list.remove("telegram-reports")
247
+ if configs.args.telegram_reports:
248
+ telegram_reports_at_end = True
249
+ print_limits = True
250
+ unfollow_jobs = [x for x in jobs_list if "unfollow" in x]
251
+ logger.info(
252
+ f"There is/are {len(jobs_list)-len(unfollow_jobs)} active-job(s) and {len(unfollow_jobs)} unfollow-job(s) scheduled for this session."
253
+ )
254
+ storage = Storage(session_state.my_username)
255
+ filters = Filter(storage)
256
+ show_ending_conditions()
257
+ if not configs.args.debug:
258
+ countdown(10, "Bot will start in: ")
259
+ for plugin in jobs_list:
260
+ inside_working_hours, time_left = SessionState.inside_working_hours(
261
+ configs.args.working_hours, configs.args.time_delta_session
262
+ )
263
+ if not inside_working_hours:
264
+ logger.info(
265
+ "Outside of working hours. Ending session.",
266
+ extra={"color": f"{Fore.CYAN}"},
267
+ )
268
+ break
269
+ (
270
+ active_limits_reached,
271
+ unfollow_limit_reached,
272
+ actions_limit_reached,
273
+ ) = session_state.check_limit(
274
+ limit_type=session_state.Limit.ALL, output=print_limits
275
+ )
276
+ if actions_limit_reached:
277
+ logger.info(
278
+ "At last one of these limits has been reached: interactions/successful or scraped. Ending session.",
279
+ extra={"color": f"{Fore.CYAN}"},
280
+ )
281
+ break
282
+ if profile_view.getUsername() != session_state.my_username:
283
+ logger.debug("Not in your main profile.")
284
+ tab_bar_view.navigateToProfile()
285
+ if plugin in unfollow_jobs:
286
+ if configs.args.scrape_to_file is not None:
287
+ logger.warning(
288
+ "Scraping in unfollow-jobs doesn't make any sense. SKIP. "
289
+ )
290
+ continue
291
+ if unfollow_limit_reached:
292
+ logger.warning(
293
+ f"Can't perform {plugin} job because the unfollow limit has been reached. SKIP."
294
+ )
295
+ print_limits = None
296
+ continue
297
+ logger.info(
298
+ f"Current unfollow-job: {plugin}",
299
+ extra={"color": f"{Style.BRIGHT}{Fore.BLUE}"},
300
+ )
301
+ configs.actions[plugin].run(
302
+ device, configs, storage, sessions, filters, plugin
303
+ )
304
+ unfollow_jobs.remove(plugin)
305
+ print_limits = True
306
+ else:
307
+ if active_limits_reached:
308
+ logger.warning(
309
+ f"Can't perform {plugin} job because a limit for active-jobs has been reached."
310
+ )
311
+ print_limits = None
312
+ if unfollow_jobs:
313
+ continue
314
+ else:
315
+ logger.info(
316
+ "No other jobs can be done cause of limit reached. Ending session.",
317
+ extra={"color": f"{Fore.CYAN}"},
318
+ )
319
+ break
320
+
321
+ logger.info(
322
+ f"Current active-job: {plugin}",
323
+ extra={"color": f"{Style.BRIGHT}{Fore.BLUE}"},
324
+ )
325
+ if configs.args.scrape_to_file is not None:
326
+ logger.warning(
327
+ "You're in scraping mode! That means you're only collection data without interacting!"
328
+ )
329
+ configs.actions[plugin].run(
330
+ device, configs, storage, sessions, filters, plugin
331
+ )
332
+ print_limits = True
333
+
334
+ # save the session in sessions.json
335
+ session_state.finishTime = datetime.now()
336
+ sessions.persist(directory=session_state.my_username)
337
+
338
+ # print reports
339
+ if telegram_reports_at_end:
340
+ logger.info("Going back to your profile..")
341
+ profile_view.click_on_avatar()
342
+ if profile_view.getFollowingCount() is None:
343
+ profile_view.click_on_avatar()
344
+ account_view.refresh_account()
345
+ (
346
+ _,
347
+ _,
348
+ followers_now,
349
+ following_now,
350
+ ) = profile_view.getProfileInfo()
351
+
352
+ if analytics_at_end:
353
+ configs.actions["analytics"].run(
354
+ device, configs, storage, sessions, "analytics"
355
+ )
356
+
357
+ # turn off bot
358
+ close_instagram(device)
359
+ if configs.args.screen_sleep:
360
+ device.screen_off()
361
+ logger.info("Screen turned off for sleeping time.")
362
+
363
+ if configs.args.kill_atx_agent:
364
+ kill_atx_agent(device)
365
+ head_up_notifications(enabled=True)
366
+ logger.info(
367
+ "-------- FINISH: "
368
+ + str(session_state.finishTime.strftime("%H:%M:%S - %Y/%m/%d"))
369
+ + " --------",
370
+ extra={"color": f"{Style.BRIGHT}{Fore.YELLOW}"},
371
+ )
372
+ pre_post_script(pre=False, path=configs.args.post_script)
373
+
374
+ if configs.args.repeat and can_repeat(len(sessions), total_sessions):
375
+ print_full_report(sessions, configs.args.scrape_to_file)
376
+ inside_working_hours, time_left = SessionState.inside_working_hours(
377
+ configs.args.working_hours, configs.args.time_delta_session
378
+ )
379
+ if inside_working_hours:
380
+ time_left = (
381
+ get_value(configs.args.repeat, "Sleep for {} minutes.", 180) * 60
382
+ )
383
+ print_telegram_reports(
384
+ configs,
385
+ telegram_reports_at_end,
386
+ followers_now,
387
+ following_now,
388
+ time_left,
389
+ )
390
+ logger.info(
391
+ f'Next session will start at: {(datetime.now() + timedelta(seconds=time_left)).strftime("%H:%M:%S (%Y/%m/%d)")}.'
392
+ )
393
+ try:
394
+ sleep(time_left)
395
+ except KeyboardInterrupt:
396
+ stop_bot(
397
+ device,
398
+ sessions,
399
+ session_state,
400
+ was_sleeping=True,
401
+ )
402
+ else:
403
+ print_telegram_reports(
404
+ configs,
405
+ telegram_reports_at_end,
406
+ followers_now,
407
+ following_now,
408
+ time_left.total_seconds(),
409
+ )
410
+ wait_for_next_session(
411
+ time_left,
412
+ session_state,
413
+ sessions,
414
+ device,
415
+ )
416
+ else:
417
+ break
418
+ print_telegram_reports(
419
+ configs,
420
+ telegram_reports_at_end,
421
+ followers_now,
422
+ following_now,
423
+ )
424
+ print_full_report(sessions, configs.args.scrape_to_file)
425
+ ask_for_a_donation()