From ff6c85ad5d6c150ae13d0a817636b0468612c5bf Mon Sep 17 00:00:00 2001 From: Dennis Date: Sat, 10 Feb 2024 18:10:00 +0100 Subject: [PATCH 1/8] fix: add some tips for the user hopefully that will reduce the questions --- GramAddict/core/config.py | 12 ++++++++++++ GramAddict/core/filter.py | 13 +++++++++---- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/GramAddict/core/config.py b/GramAddict/core/config.py index a165c4c9..294802bf 100644 --- a/GramAddict/core/config.py +++ b/GramAddict/core/config.py @@ -1,6 +1,7 @@ import logging import os import sys +from datetime import datetime from typing import Optional import configargparse @@ -51,6 +52,7 @@ def __init__(self, first_run=False, **kwargs): f"You have to specify a *.yml / *.yaml config file path (For example 'accounts/your_account_name/config.yml')! \nYou entered: {file_name}, abort." ) sys.exit(1) + logger.warning(get_time_last_save(file_name)) with open(file_name, encoding="utf-8") as fin: # preserve order of yaml self.config_list = [line.strip() for line in fin] @@ -205,3 +207,13 @@ def _is_legacy_arg(arg): and not _is_legacy_arg(nitem) ): self.enabled.append(nitem) + + +def get_time_last_save(file_path) -> str: + try: + absolute_file_path = os.path.abspath(file_path) + timestamp = os.path.getmtime(absolute_file_path) + last_save = datetime.fromtimestamp(timestamp).strftime("%Y-%m-%d %H:%M:%S") + return f"{file_path} has been saved last time at {last_save}" + except FileNotFoundError: + return f"File {file_path} not found" diff --git a/GramAddict/core/filter.py b/GramAddict/core/filter.py index 20fb029d..b62427be 100644 --- a/GramAddict/core/filter.py +++ b/GramAddict/core/filter.py @@ -14,6 +14,7 @@ from colorama import Fore, Style from langdetect import detect +from GramAddict.core.config import Config, get_time_last_save from GramAddict.core.device_facade import Timeout from GramAddict.core.resources import ResourceID as resources from GramAddict.core.utils import random_sleep @@ -131,8 +132,11 @@ class Filter: def __init__(self, storage=None): filter_path = storage.filter_path if configs.args.disable_filters: - logger.warning("Filters are disabled!") + logger.warning( + "Filters are disabled! (The default values in the documentation have been chosen!)" + ) elif os.path.exists(filter_path) and filter_path.endswith(".yml"): + logger.warning(get_time_last_save(filter_path)) with open(filter_path, "r", encoding="utf-8") as stream: try: self.conditions = yaml.safe_load(stream) @@ -169,9 +173,10 @@ def __init__(self, storage=None): else: logger.info(f"{k:<35} {v}", extra={"color": f"{Fore.WHITE}"}) else: - logger.warning( - "The filters file doesn't exists in your account folder. Download it from https://github.com/GramAddict/bot/blob/08e1d7aff39ec47543fa78aadd7a2f034b9ae34d/config-examples/filters.yml and place it in your account folder!" - ) + if not args.disable_filters: + logger.warning( + f"The filters file doesn't exists in your account folder (can't find {filter_path}). Download it from https://github.com/GramAddict/bot/blob/08e1d7aff39ec47543fa78aadd7a2f034b9ae34d/config-examples/filters.yml and place it in your account folder!" + ) def is_num_likers_in_range(self, likes_on_post: str) -> bool: if self.conditions is not None and likes_on_post is not None: From b9f67c907126a9bbff1b6718aebfe3221652cad8 Mon Sep 17 00:00:00 2001 From: Dennis Date: Sat, 10 Feb 2024 18:14:31 +0100 Subject: [PATCH 2/8] fix: improve code --- GramAddict/core/config.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/GramAddict/core/config.py b/GramAddict/core/config.py index 294802bf..c28d0790 100644 --- a/GramAddict/core/config.py +++ b/GramAddict/core/config.py @@ -152,7 +152,7 @@ def _is_legacy_arg(arg): logger.debug("Arguments used:") if self.config: logger.debug(f"Config used: {self.config}") - if not len(self.args) > 0: + if len(self.args) == 0: self.parser.print_help() exit(0) else: @@ -160,15 +160,15 @@ def _is_legacy_arg(arg): logger.debug(f"Arguments used: {' '.join(sys.argv[1:])}") if self.config: logger.debug(f"Config used: {self.config}") - if not len(sys.argv) > 1: + if len(sys.argv) <= 1: self.parser.print_help() exit(0) if self.module: arg_str = "" for k, v in self.args.items(): new_key = k.replace("_", "-") - new_key = " --" + new_key - arg_str += new_key + " " + v + new_key = f" --{new_key}" + arg_str += f"{new_key} {v}" self.args, self.unknown_args = self.parser.parse_known_args(args=arg_str) else: self.args, self.unknown_args = self.parser.parse_known_args() From bf55a0d2a3647b8e97d67612c18a54afddd0ee14 Mon Sep 17 00:00:00 2001 From: Dennis Date: Sat, 10 Feb 2024 19:55:42 +0100 Subject: [PATCH 3/8] feat: removed pandas dep from telegram reports need some work to be nicer.. --- GramAddict/plugins/telegram.py | 288 ++++++++++++--------------------- pyproject.toml | 5 - 2 files changed, 100 insertions(+), 193 deletions(-) diff --git a/GramAddict/plugins/telegram.py b/GramAddict/plugins/telegram.py index 359ea29d..78a3433b 100644 --- a/GramAddict/plugins/telegram.py +++ b/GramAddict/plugins/telegram.py @@ -1,9 +1,6 @@ import json import logging -import sys -from datetime import datetime, timedelta -from textwrap import dedent - +from datetime import datetime import requests import yaml from colorama import Fore, Style @@ -12,13 +9,6 @@ logger = logging.getLogger(__name__) -try: - import pandas as pd -except ImportError: - logger.warning( - "If you want to use telegram_reports, please type in console: 'pip3 install gramaddict[telegram-reports]'" - ) - class TelegramReports(Plugin): """Generate reports at the end of the session and send them using telegram""" @@ -37,192 +27,114 @@ def __init__(self): def run(self, config, plugin, followers_now, following_now, time_left): username = config.args.username - modulename = "pandas" - if modulename not in sys.modules: - logger.error( - f"You can't use {plugin} without installing {modulename}. Type that in console: 'pip3 install gramaddict[telegram-reports]'" - ) + if username is None: + logger.error("You have to specify a username for getting reports!") return def telegram_bot_sendtext(text): - with open( - f"accounts/{username}/telegram.yml", "r", encoding="utf-8" - ) as stream: - try: + try: + with open( + f"accounts/{username}/telegram.yml", "r", encoding="utf-8" + ) as stream: config = yaml.safe_load(stream) - bot_api_token = config.get("telegram-api-token") - bot_chat_ID = config.get("telegram-chat-id") - except yaml.YAMLError as e: - logger.error(e) - if bot_api_token is not None and bot_chat_ID is not None: - method = "sendMessage" - parse_mode = "markdown" - params = { - "text": text, - "chat_id": bot_chat_ID, - "parse_mode": parse_mode, - } - url = f"https://api.telegram.org/bot{bot_api_token}/{method}" - response = requests.get(url, params=params) - return response.json() + bot_api_token = config.get("telegram-api-token") + bot_chat_ID = config.get("telegram-chat-id") + if bot_api_token and bot_chat_ID: + method = "sendMessage" + parse_mode = "markdown" + params = { + "text": text, + "chat_id": bot_chat_ID, + "parse_mode": parse_mode, + } + url = f"https://api.telegram.org/bot{bot_api_token}/{method}" + response = requests.get(url, params=params) + return response.json() + except Exception as e: + logger.error(f"Error sending Telegram message: {e}") + return None - if username is None: - logger.error("You have to specify an username for getting reports!") - return None - with open(f"accounts/{username}/sessions.json") as json_data: - activity = json.load(json_data) + try: + with open(f"accounts/{username}/sessions.json") as json_data: + sessions = json.load(json_data) + except FileNotFoundError: + logger.error("No session data found. Skipping report generation.") + return - aggActivity = [] - for session in activity: + aggregated_data = {} + + for session in sessions: + date = session["start_time"][:10] + if date not in aggregated_data: + aggregated_data[date] = { + "total_likes": 0, + "total_watched": 0, + "total_followed": 0, + "total_unfollowed": 0, + "total_comments": 0, + "total_pm": 0, + "duration": 0, + "followers": 0, + } try: - id = session["id"] - start = session["start_time"] - finish = session["finish_time"] - followed = session.get("total_followed", 0) - unfollowed = session.get("total_unfollowed", 0) - likes = session.get("total_likes", 0) - watched = session.get("total_watched", 0) - comments = session.get("total_comments", 0) - pm_sent = session.get("total_pm", 0) - followers = int(session.get("profile", 0).get("followers", 0)) - following = int(session.get("profile", 0).get("following", 0)) - aggActivity.append( - [ - start, - finish, - likes, - watched, - followed, - unfollowed, - comments, - pm_sent, - followers, - following, - ] + start_datetime = datetime.strptime( + session["start_time"], "%Y-%m-%d %H:%M:%S.%f" ) - except TypeError: - logger.error(f"The session {id} has malformed data, skip.") - continue - - df = pd.DataFrame( - aggActivity, - columns=[ - "start", - "finish", - "likes", - "watched", - "followed", - "unfollowed", - "comments", - "pm_sent", - "followers", - "following", - ], - ) - df["date"] = df.loc[:, "start"].str[:10] - df["duration"] = pd.to_datetime(df["finish"], errors="coerce") - pd.to_datetime( - df["start"], errors="coerce" - ) - df["duration"] = df["duration"].dt.total_seconds() / 60 - - if time_left is not None: - timeString = f'Next session will start at: {(datetime.now()+ timedelta(seconds=time_left)).strftime("%H:%M:%S (%Y/%m/%d)")}.' - else: - timeString = "There is no new session planned!" - - dailySummary = df.groupby(by="date").agg( - { - "likes": "sum", - "watched": "sum", - "followed": "sum", - "unfollowed": "sum", - "comments": "sum", - "pm_sent": "sum", - "followers": "max", - "following": "max", - "duration": "sum", - } - ) - if len(dailySummary.index) > 1: - dailySummary["followers_gained"] = dailySummary["followers"].astype( - int - ) - dailySummary["followers"].astype(int).shift(1) - else: - logger.info( - "First day of botting eh? Stats for the first day are meh because we don't have enough data to track how many followers you earned today from the bot activity." + finish_datetime = datetime.strptime( + session["finish_time"], "%Y-%m-%d %H:%M:%S.%f" + ) + duration = int((finish_datetime - start_datetime).total_seconds() / 60) + except ValueError: + logger.error(f"Failed to calculate session duration for {date}.") + duration = 0 + aggregated_data[date]["duration"] += duration + + for key in [ + "total_likes", + "total_watched", + "total_followed", + "total_unfollowed", + "total_comments", + "total_pm", + ]: + aggregated_data[date][key] += session.get(key, 0) + aggregated_data[date]["followers"] = session.get("profile", {}).get( + "followers", 0 ) - dailySummary["followers_gained"] = dailySummary["followers"].astype(int) - dailySummary.dropna(inplace=True) - dailySummary["followers_gained"] = dailySummary["followers_gained"].astype(int) - dailySummary["duration"] = dailySummary["duration"].astype(int) - numFollowers = int(dailySummary["followers"].iloc[-1]) - n = 1 - milestone = "" - try: - for x in range(10): - if numFollowers in range(x * 1000, n * 1000): - milestone = f"• {str(int(((n * 1000 - numFollowers)/dailySummary['followers_gained'].tail(7).mean())))} days until {n}k!" - break - n += 1 - except OverflowError: - logger.info("Not able to get milestone ETA..") - - def undentString(string): - return dedent(string[1:])[:-1] - - followers_before = int(df["followers"].iloc[-1]) - following_before = int(df["following"].iloc[-1]) - statString = f""" - *Stats for {username}*: - - *✨Overview after last activity* - • {followers_now} followers ({followers_now - followers_before:+}) - • {following_now} following ({following_now - following_before:+}) - - *🤖 Last session actions* - • {str(df["duration"].iloc[-1].astype(int))} minutes of botting - • {str(df["likes"].iloc[-1])} likes - • {str(df["followed"].iloc[-1])} follows - • {str(df["unfollowed"].iloc[-1])} unfollows - • {str(df["watched"].iloc[-1])} stories watched - • {str(df["comments"].iloc[-1])} comments done - • {str(df["pm_sent"].iloc[-1])} PM sent - - *📅 Today's total actions* - • {str(dailySummary["duration"].iloc[-1])} minutes of botting - • {str(dailySummary["likes"].iloc[-1])} likes - • {str(dailySummary["followed"].iloc[-1])} follows - • {str(dailySummary["unfollowed"].iloc[-1])} unfollows - • {str(dailySummary["watched"].iloc[-1])} stories watched - • {str(dailySummary["comments"].iloc[-1])} comments done - • {str(dailySummary["pm_sent"].iloc[-1])} PM sent - *📈 Trends* - • {str(dailySummary["followers_gained"].iloc[-1])} new followers today - • {str(dailySummary["followers_gained"].tail(3).sum())} new followers past 3 days - • {str(dailySummary["followers_gained"].tail(7).sum())} new followers past week - {milestone if not "" else ""} - - *🗓 7-Day Average* - • {str(round(dailySummary["followers_gained"].tail(7).mean(), 1))} followers / day - • {str(int(dailySummary["likes"].tail(7).mean()))} likes - • {str(int(dailySummary["followed"].tail(7).mean()))} follows - • {str(int(dailySummary["unfollowed"].tail(7).mean()))} unfollows - • {str(int(dailySummary["watched"].tail(7).mean()))} stories watched - • {str(int(dailySummary["comments"].tail(7).mean()))} comments done - • {str(int(dailySummary["pm_sent"].tail(7).mean()))} PM sent - • {str(int(dailySummary["duration"].tail(7).mean()))} minutes of botting - """ - try: - r = telegram_bot_sendtext(f"{undentString(statString)}\n\n{timeString}") - if r.get("ok"): - logger.info( - "Telegram message sent successfully.", - extra={"color": f"{Style.BRIGHT}{Fore.BLUE}"}, + # Calculate followers gained + dates_sorted = list(sorted(aggregated_data.keys()))[-2:] + previous_followers = None + for date in dates_sorted: + current_followers = aggregated_data[date]["followers"] + if previous_followers is not None: + aggregated_data[date]["followers_gained"] = ( + current_followers - previous_followers ) else: - logger.error( - f"Unable to send telegram report. Error code: {r.get('error_code')} - {r.get('description')}" - ) - except Exception as e: - logger.error(f"Telegram message failed to send. Error: {e}") + aggregated_data[date][ + "followers_gained" + ] = 0 # No data to compare for the first entry + previous_followers = current_followers + + # TODO: Add more stats + report = f"Stats for {username}:\n\n" + for date, data in list(aggregated_data.items())[-2:]: + report += f"Date: {date}\n" + report += f"Duration (min): {data['duration']:.2f}\n" + report += f"Likes: {data['total_likes']}, Watched: {data['total_watched']}, Followed: {data['total_followed']}, Unfollowed: {data['total_unfollowed']}\n" + report += ( + f"Comments: {data['total_comments']}, PM Sent: {data['total_pm']}\n" + ) + report += f"Followers Gained: {data['followers_gained']}\n\n" + + # Send the report via Telegram + response = telegram_bot_sendtext(report) + if response and response.get("ok"): + logger.info( + "Telegram message sent successfully.", + extra={"color": f"{Style.BRIGHT}{Fore.BLUE}"}, + ) + else: + error = response.get("description") if response else "Unknown error" + logger.error(f"Failed to send Telegram message: {error}") diff --git a/pyproject.toml b/pyproject.toml index 47bee439..ef2a04ac 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,14 +29,9 @@ dependencies = [ ] [project.optional-dependencies] -telegram-reports = [ - "pandas==1.2.4", - "pytest-cov", -] analytics = ["matplotlib==3.4.2"] dev = ["flit", "pre-commit", "black", "flake8", "isort"] - [project.urls] Documentation = "https://docs.gramaddict.org/#/" Source = "https://github.com/GramAddict/bot" From 33f3f2e912eab0004c0f5af4cd95552b11b34e5c Mon Sep 17 00:00:00 2001 From: Dennis Date: Sat, 10 Feb 2024 20:05:36 +0100 Subject: [PATCH 4/8] bump: new version --- CHANGELOG.md | 8 ++++++++ GramAddict/version.py | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 279d9261..c8dd61b1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,12 @@ # Changelog +## 3.2.9 (2024-02-10) +### Fix +- remove pandas as dependency for telegram reports +- show when config file and filter file have been saved +- better logging information +## 3.2.8 (2023-01-24) +### Fix +- removed the language check ## 3.2.7 (2023-09-30) ### Fix - using the monkey approach until this bug is fixed https://github.com/openatx/atx-agent/pull/111 diff --git a/GramAddict/version.py b/GramAddict/version.py index 74f5cd13..46f53016 100644 --- a/GramAddict/version.py +++ b/GramAddict/version.py @@ -1,2 +1,2 @@ # that file is deprecated, current version is now stored in GramAddict/__init__.py -__version__ = "3.2.8" +__version__ = "3.2.9" From dd3f991c37268b82efa1085311f7cc1f8a3fa912 Mon Sep 17 00:00:00 2001 From: Dennis Date: Sat, 10 Feb 2024 20:11:55 +0100 Subject: [PATCH 5/8] fix: add ruff as dev dep --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index ef2a04ac..caacde53 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,7 +30,7 @@ dependencies = [ [project.optional-dependencies] analytics = ["matplotlib==3.4.2"] -dev = ["flit", "pre-commit", "black", "flake8", "isort"] +dev = ["flit", "pre-commit", "black", "flake8", "isort", "ruff"] [project.urls] Documentation = "https://docs.gramaddict.org/#/" From 33854b8c2ed29db2d592ca7931256a13eca939cf Mon Sep 17 00:00:00 2001 From: Dennis Date: Sat, 10 Feb 2024 20:12:15 +0100 Subject: [PATCH 6/8] fix: remove unused imports --- GramAddict/core/filter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GramAddict/core/filter.py b/GramAddict/core/filter.py index b62427be..f14e9a2d 100644 --- a/GramAddict/core/filter.py +++ b/GramAddict/core/filter.py @@ -14,7 +14,7 @@ from colorama import Fore, Style from langdetect import detect -from GramAddict.core.config import Config, get_time_last_save +from GramAddict.core.config import get_time_last_save from GramAddict.core.device_facade import Timeout from GramAddict.core.resources import ResourceID as resources from GramAddict.core.utils import random_sleep From 61917a697560bda5a9d5053f26e6a967685f1e11 Mon Sep 17 00:00:00 2001 From: Dennis Date: Sat, 10 Feb 2024 20:13:10 +0100 Subject: [PATCH 7/8] fix: replace type with isinstance --- GramAddict/core/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/GramAddict/core/views.py b/GramAddict/core/views.py index d389950a..e91f77a3 100644 --- a/GramAddict/core/views.py +++ b/GramAddict/core/views.py @@ -1383,7 +1383,7 @@ def _isFollowing(self, container): resourceId=ResourceID.BUTTON, classNameMatches=ClassName.BUTTON_OR_TEXTVIEW_REGEX, ) - if type(text) != str: + if not isinstance(text, str): text = text.get_text() if text.exists() else "" return text in ["Following", "Requested"] From 00f792d94df7978d3b0ca3d20e38e0e00c325add Mon Sep 17 00:00:00 2001 From: Dennis Date: Sat, 10 Feb 2024 20:18:39 +0100 Subject: [PATCH 8/8] fix: reformat using black v 24 --- GramAddict/__init__.py | 1 + GramAddict/core/utils.py | 24 ++++++++++++++++++------ 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/GramAddict/__init__.py b/GramAddict/__init__.py index 290c496d..bad2445e 100644 --- a/GramAddict/__init__.py +++ b/GramAddict/__init__.py @@ -1,4 +1,5 @@ """Human-like Instagram bot powered by UIAutomator2""" + __version__ = "3.2.8" __tested_ig_version__ = "263.2.0.19.104" diff --git a/GramAddict/core/utils.py b/GramAddict/core/utils.py index 5d5ebdd4..5f8be6ad 100644 --- a/GramAddict/core/utils.py +++ b/GramAddict/core/utils.py @@ -204,20 +204,26 @@ def head_up_notifications(enabled: bool = False): """ Enable or disable head-up-notifications """ - cmd: str = f"adb{'' if configs.device_id is None else ' -s ' + configs.device_id} shell settings put global heads_up_notifications_enabled {0 if not enabled else 1}" + cmd: str = ( + f"adb{'' if configs.device_id is None else ' -s ' + configs.device_id} shell settings put global heads_up_notifications_enabled {0 if not enabled else 1}" + ) return subprocess.run(cmd, stdout=PIPE, stderr=PIPE, shell=True, encoding="utf8") def check_screen_timeout(): MIN_TIMEOUT = 5 * 6_000 - cmd: str = f"adb{'' if configs.device_id is None else f' -s {configs.device_id}'} shell settings get system screen_off_timeout" + cmd: str = ( + f"adb{'' if configs.device_id is None else f' -s {configs.device_id}'} shell settings get system screen_off_timeout" + ) resp = subprocess.run(cmd, stdout=PIPE, stderr=PIPE, shell=True, encoding="utf8") try: if int(resp.stdout.lstrip()) < MIN_TIMEOUT: logger.info( f"Setting timeout of the screen to {MIN_TIMEOUT/6_000:.0f} minutes." ) - cmd: str = f"adb{'' if configs.device_id is None else f' -s {configs.device_id}'} shell settings put system screen_off_timeout {MIN_TIMEOUT}" + cmd: str = ( + f"adb{'' if configs.device_id is None else f' -s {configs.device_id}'} shell settings put system screen_off_timeout {MIN_TIMEOUT}" + ) subprocess.run(cmd, stdout=PIPE, stderr=PIPE, shell=True, encoding="utf8") else: @@ -270,13 +276,17 @@ def call_ig(): random_sleep() logger.debug("Setting FastInputIME as default keyboard.") device.deviceV2.set_fastinput_ime(True) - cmd: str = f"adb{'' if configs.device_id is None else ' -s ' + configs.device_id} shell settings get secure default_input_method" + cmd: str = ( + f"adb{'' if configs.device_id is None else ' -s ' + configs.device_id} shell settings get secure default_input_method" + ) cmd_res = subprocess.run(cmd, stdout=PIPE, stderr=PIPE, shell=True, encoding="utf8") if cmd_res.stdout.replace(nl, "") != FastInputIME: logger.warning( f"FastInputIME is not the default keyboard! Default is: {cmd_res.stdout.replace(nl, '')}. Changing it via adb.." ) - cmd: str = f"adb{'' if configs.device_id is None else ' -s ' + configs.device_id} shell ime set {FastInputIME}" + cmd: str = ( + f"adb{'' if configs.device_id is None else ' -s ' + configs.device_id} shell ime set {FastInputIME}" + ) cmd_res = subprocess.run( cmd, stdout=PIPE, stderr=PIPE, shell=True, encoding="utf8" ) @@ -418,7 +428,9 @@ def print_telegram_reports( def kill_atx_agent(device): _restore_keyboard(device) logger.info("Kill atx agent.") - cmd: str = f"adb{'' if configs.device_id is None else ' -s ' + configs.device_id} shell pkill atx-agent" + cmd: str = ( + f"adb{'' if configs.device_id is None else ' -s ' + configs.device_id} shell pkill atx-agent" + ) subprocess.run(cmd, stdout=PIPE, stderr=PIPE, shell=True, encoding="utf8")