diff --git a/.gitignore b/.gitignore index ce5d8ae..eb39cba 100644 --- a/.gitignore +++ b/.gitignore @@ -44,4 +44,22 @@ bin-release/ *.raw # fly.io configs -fly.toml \ No newline at end of file +fly.toml + +# User-specific data and logs +downloads/ +uploads/ +logs/ + +# Docker-specific generated directory for safe setup +docker-ultroid/ + +# Session generator output +session_output/ + +# Python cache files +*.py[co] + +# OS-specific files +.DS_Store +Thumbs.db \ No newline at end of file diff --git a/DOCKER_DEPLOYMENT.md b/DOCKER_DEPLOYMENT.md index 11b13f2..5f0dcd1 100644 --- a/DOCKER_DEPLOYMENT.md +++ b/DOCKER_DEPLOYMENT.md @@ -78,12 +78,14 @@ services: ## ๐Ÿ“ Volume Mounts +The following host directories are mounted into the `ultroid` container. Note that the internal working directory is now `/home/ultroid/app`. ``` -./downloads โ†’ /root/TeamUltroid/downloads -./uploads โ†’ /root/TeamUltroid/uploads -./logs โ†’ /root/TeamUltroid/logs -./resources โ†’ /root/TeamUltroid/resources -./.env โ†’ /root/TeamUltroid/.env +./downloads โ†’ /home/ultroid/app/downloads +./uploads โ†’ /home/ultroid/app/uploads +./logs โ†’ /home/ultroid/app/logs +./resources/session โ†’ /home/ultroid/app/resources/session +./.env โ†’ /home/ultroid/app/.env (mounted read-only) +./credentials.json โ†’ /home/ultroid/app/credentials.json (if present, mounted read-only) ``` ## ๐Ÿ”ง Configuration Options @@ -126,6 +128,9 @@ HEROKU_APP_NAME=your_app_name SPAMWATCH_API=your_spamwatch_api OPENWEATHER_API=your_weather_api REMOVE_BG_API=your_removebg_api + +# Timezone +TZ=Asia/Kolkata # Example: Europe/London, America/New_York. Sets the container timezone. ``` ## ๐ŸŽฏ Management Commands @@ -156,7 +161,7 @@ docker-compose up -d ### Maintenance ```bash # Shell access -docker-compose exec ultroid bash +docker-compose exec ultroid bash # Note: You will be logged in as the 'ultroid' user in /home/ultroid/app # Database access (Redis) docker-compose exec redis redis-cli @@ -187,7 +192,7 @@ docker stats **2. Database Connection Issues** ```bash # Check database status -docker-compose ps +docker-compose ps # Services should show (healthy) status after startup period docker-compose logs redis ``` @@ -230,10 +235,10 @@ MONGO_PASSWORD=generate_strong_password ### Container Security ```bash -# Run as non-root (in production) -# Use Docker secrets for sensitive data +# Run as non-root (in production) - Implemented: Bot now runs as non-root 'ultroid' user. +# Use Docker secrets for sensitive data - Consider for advanced setups. # Regular security updates -docker-compose pull && docker-compose up -d +docker-compose pull && docker-compose up -d # Pulls latest base images and rebuilds Ultroid ``` ## ๐Ÿ“Š Monitoring & Logs @@ -321,12 +326,12 @@ docker-compose up -d - โœ… Comprehensive logging ### Docker Benefits -- โœ… Isolated environment +- โœ… Isolated environment (now more secure with non-root user) - โœ… Easy deployment - โœ… Consistent across platforms -- โœ… Built-in database services +- โœ… Built-in database services (with healthchecks) - โœ… Volume persistence -- โœ… Health monitoring +- โœ… Health monitoring (via Docker healthchecks and `health_check.sh`) - โœ… Easy scaling --- diff --git a/Dockerfile b/Dockerfile index 7334ff1..f1a8ff6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,21 +3,17 @@ # This file is a part of < https://github.com/TeamUltroid/Ultroid/ > # PLease read the GNU Affero General Public License in . -FROM python:3.11-slim +# Builder stage +FROM python:3.11-slim AS builder + +# Set timezone ARG and ENV +ARG TZ_ARG=Asia/Kolkata +ENV TZ=${TZ_ARG} -# Set timezone -ENV TZ=Asia/Kolkata RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone -# Install system dependencies -RUN apt-get update && apt-get install -y \ - git \ - wget \ - curl \ - unzip \ - ffmpeg \ - mediainfo \ - neofetch \ +# Install build dependencies +RUN apt-get update && apt-get install -y --no-install-recommends \ build-essential \ python3-dev \ libffi-dev \ @@ -32,28 +28,97 @@ RUN apt-get update && apt-get install -y \ libxml2-dev \ libxslt1-dev \ zlib1g-dev \ + # Runtime dependencies that are also needed at build time for some packages + git \ + curl \ + wget \ + ffmpeg \ + mediainfo \ libmagic1 \ + unzip \ && rm -rf /var/lib/apt/lists/* -# Set working directory -WORKDIR /root/TeamUltroid +# Set up a virtual environment +ENV VIRTUAL_ENV=/opt/venv +RUN python3 -m venv $VIRTUAL_ENV +ENV PATH="$VIRTUAL_ENV/bin:$PATH" -# Copy requirements and install Python dependencies +# Copy requirements first to leverage Docker cache COPY requirements.txt . -COPY resources/ ./resources/ -COPY re*/ ./re*/ +COPY resources/startup/optional-requirements.txt ./resources/startup/optional-requirements.txt -# Install requirements following official method +# Install Python dependencies RUN pip install --upgrade pip setuptools wheel -RUN pip install -U -r re*/st*/optional-requirements.txt || true +# rรฉcit: re*/st*/optional-requirements.txt was in the original Dockerfile. Assuming re* is a glob for resources +# For simplicity and to ensure it matches original intent, copying the directory structure +# However, this makes the builder less efficient if these files change often. +# A better approach would be to list specific files if possible. +COPY resources/ ./resources/ +RUN pip install -U -r resources/startup/optional-requirements.txt || true RUN pip install -U -r requirements.txt +# Final stage +FROM python:3.11-slim AS final + +# Create a non-root user and group +ARG UID=10001 +RUN addgroup --system ultroid && \ + adduser --system --ingroup ultroid --no-create-home --uid ${UID} --shell /sbin/nologin --disabled-password ultroid + +# Set timezone ARG and ENV +ARG TZ_ARG=Asia/Kolkata +ENV TZ=${TZ_ARG} +RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone + +# Install runtime system dependencies +# Reduced set compared to builder stage +RUN apt-get update && apt-get install -y --no-install-recommends \ + ffmpeg \ + mediainfo \ + libmagic1 \ + neofetch \ + # Git, curl, wget, unzip might be used by plugins at runtime. Keeping them for now. + git \ + curl \ + wget \ + unzip \ + # libjpeg, libpng etc. are needed if Pillow was compiled against them and not statically linked + # For simplicity, keeping ones that Pillow usually needs dynamically. + # A more minimal image would require testing which .so files are truly needed by the installed wheels. + libjpeg62-turbo \ + libpng16-16 \ + libwebp7 \ + libopenjp2-7 \ + libtiff5 \ + libfreetype6 \ + liblcms2-2 \ + libxml2 \ + libxslt1.1 \ + zlib1g \ + && rm -rf /var/lib/apt/lists/* + +# Copy virtual environment from builder stage +ENV VIRTUAL_ENV=/opt/venv +COPY --from=builder $VIRTUAL_ENV $VIRTUAL_ENV +ENV PATH="$VIRTUAL_ENV/bin:$PATH" + +# Set working directory +WORKDIR /home/ultroid/app +USER ultroid + # Copy application code -COPY . . +# Ensure files are owned by the ultroid user after copying +COPY --chown=ultroid:ultroid . . -# Create necessary directories and set permissions +# Create necessary directories (as non-root user, these will be owned by ultroid) +# These paths should be relative to WORKDIR or absolute paths writable by ultroid RUN mkdir -p downloads uploads logs resources/session -RUN chmod +x startup sessiongen installer.sh -# Start the bot using official startup method +# Ensure scripts are executable by the user +RUN chmod +x startup sessiongen installer.sh health_check.sh + +# Expose port if the application uses one (though this bot likely doesn't serve HTTP) +# EXPOSE 8080 + +# Start the bot CMD ["bash", "startup"] diff --git a/README_DOCKER.md b/README_DOCKER.md index 33b7256..55b56f3 100644 --- a/README_DOCKER.md +++ b/README_DOCKER.md @@ -126,6 +126,7 @@ BOT_TOKEN= # Assistant bot LOG_CHANNEL= # Logging channel OWNER_ID= # Your user ID HEROKU_API_KEY= # For updates +TZ=Asia/Kolkata # Set your desired timezone (e.g., Europe/London, America/New_York) ``` ## ๐ŸŽฎ Management Commands @@ -150,7 +151,7 @@ docker-compose up -d # Start docker-compose down # Stop docker-compose logs -f ultroid # Logs docker-compose restart ultroid # Restart -docker-compose exec ultroid bash # Shell +docker-compose exec ultroid bash # Shell (Note: Container runs as 'ultroid' user, WORKDIR is /home/ultroid/app) ``` ## ๐Ÿ” Monitoring & Troubleshooting @@ -243,10 +244,11 @@ docker-compose restart redis ### Volume Mounts ``` -./downloads โ†’ Bot downloads -./uploads โ†’ Bot uploads -./logs โ†’ Application logs -./resources โ†’ Bot resources +./downloads โ†’ /home/ultroid/app/downloads +./uploads โ†’ /home/ultroid/app/uploads +./logs โ†’ /home/ultroid/app/logs +./resources โ†’ /home/ultroid/app/resources +# .env and credentials.json are also mounted into /home/ultroid/app/ ``` ## ๐Ÿ†š Comparison with Other Methods diff --git a/docker-compose.yml b/docker-compose.yml index 0ac2bf9..c3efd78 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -13,6 +13,11 @@ services: environment: - REDIS_PASSWORD=${REDIS_PASSWORD:-ultroid123} command: redis-server --requirepass ${REDIS_PASSWORD:-ultroid123} + healthcheck: + test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD:-ultroid123}", "ping"] + interval: 10s + timeout: 5s + retries: 5 networks: - ultroid-network @@ -28,60 +33,89 @@ services: environment: - MONGO_INITDB_ROOT_USERNAME=${MONGO_USER:-ultroid} - MONGO_INITDB_ROOT_PASSWORD=${MONGO_PASSWORD:-ultroid123} + healthcheck: + test: echo 'db.runCommand("ping").ok' | mongosh -u ${MONGO_USER:-ultroid} -p ${MONGO_PASSWORD:-ultroid123} --quiet + interval: 10s + timeout: 10s + retries: 5 networks: - ultroid-network # Ultroid Bot Service ultroid: - build: . + build: + context: . + args: + - TZ_ARG=${TZ:-Asia/Kolkata} # Allow overriding timezone during build container_name: ultroid-bot restart: unless-stopped depends_on: - - redis + redis: + condition: service_healthy # Wait for redis to be healthy + # Uncomment if using mongodb + # mongodb: + # condition: service_healthy volumes: - - ./downloads:/root/TeamUltroid/downloads - - ./uploads:/root/TeamUltroid/uploads - - ./logs:/root/TeamUltroid/logs - - ./resources/session:/root/TeamUltroid/resources/session - - ./.env:/root/TeamUltroid/.env - - ./credentials.json:/root/TeamUltroid/credentials.json:ro + # Note: Paths inside container are now relative to /home/ultroid/app + - ./downloads:/home/ultroid/app/downloads + - ./uploads:/home/ultroid/app/uploads + - ./logs:/home/ultroid/app/logs + - ./resources/session:/home/ultroid/app/resources/session + - ./.env:/home/ultroid/app/.env:ro # Mount .env as read-only + - ./credentials.json:/home/ultroid/app/credentials.json:ro # Mount credentials as read-only environment: # Database Configuration (Redis) - - REDIS_URI=redis://redis:6379 + - REDIS_URI=redis://redis:6379 # Service name from docker-compose - REDIS_PASSWORD=${REDIS_PASSWORD:-ultroid123} # Alternative MongoDB Configuration # - MONGO_URI=mongodb://${MONGO_USER:-ultroid}:${MONGO_PASSWORD:-ultroid123}@mongodb:27017/ultroid?authSource=admin - # Bot Configuration + # Bot Configuration (ensure these are in your .env file) - SESSION=${SESSION} - API_ID=${API_ID} - API_HASH=${API_HASH} - - BOT_TOKEN=${BOT_TOKEN} - - OWNER_ID=${OWNER_ID} + - BOT_TOKEN=${BOT_TOKEN} # Optional, for assistant bot + - OWNER_ID=${OWNER_ID} # Optional, your Telegram user ID - # Optional Configuration + # Optional Configuration (ensure these are in your .env file if used) - HEROKU_API_KEY=${HEROKU_API_KEY} - HEROKU_APP_NAME=${HEROKU_APP_NAME} - LOG_CHANNEL=${LOG_CHANNEL} - BOT_MODE=${BOT_MODE} - DUAL_MODE=${DUAL_MODE} - - DATABASE_URL=${DATABASE_URL} + - DATABASE_URL=${DATABASE_URL} # For external DBs like PostgreSQL - OKTETO_TOKEN=${OKTETO_TOKEN} - # Custom Configuration - - TZ=Asia/Kolkata + # Timezone for the container environment + - TZ=${TZ:-Asia/Kolkata} + healthcheck: + test: ["CMD-SHELL", "bash health_check.sh"] # Assumes health_check.sh is executable and in WORKDIR + interval: 30s + timeout: 10s + retries: 3 + start_period: 60s # Give the bot some time to start up networks: - ultroid-network # Session Generator Service (One-time use) + # Note: This service will also run as non-root user defined in Dockerfile's final stage. + # If it needs to write to /root/TeamUltroid/session_output, permissions might be an issue. + # For simplicity, it will use the same image. If it fails, it might need its own simple Dockerfile or adjustments. session-gen: - build: . + build: + context: . + args: + - TZ_ARG=${TZ:-Asia/Kolkata} container_name: ultroid-session-gen profiles: ["session"] volumes: - - ./session_output:/root/TeamUltroid/session_output - command: bash -c "wget -O session.py https://git.io/JY9JI && python3 session.py" + # This path needs to be writable by the 'ultroid' user (UID 10001 by default) + # or the command needs to be adjusted to write to a user-writable path. + - ./session_output:/home/ultroid/app/session_output + # The original command tried to write to /root/TeamUltroid. + # Changed to use /home/ultroid/app (WORKDIR) which should be writable by the 'ultroid' user. + command: bash -c "wget -O session.py https://git.io/JY9JI && python3 session.py && cp *.session session_output/" networks: - ultroid-network diff --git a/pyUltroid/startup/BaseClient.py b/pyUltroid/startup/BaseClient.py index f31661c..08b4d7f 100644 --- a/pyUltroid/startup/BaseClient.py +++ b/pyUltroid/startup/BaseClient.py @@ -32,10 +32,11 @@ class UltroidClient(TelegramClient): api_hash=None, bot_token=None, udB=None, + # *args moved before keyword-only arguments for correct signature + *args, logger: Logger = LOGS, log_attempt=True, exit_on_error=True, - *args, **kwargs, ): self._cache = {} @@ -47,7 +48,8 @@ class UltroidClient(TelegramClient): kwargs["api_id"] = api_id or Var.API_ID kwargs["api_hash"] = api_hash or Var.API_HASH kwargs["base_logger"] = TelethonLogger - super().__init__(session, **kwargs) + # Pass *args to super if it might be used by the parent class + super().__init__(session, *args, **kwargs) self.run_in_loop(self.start_client(bot_token=bot_token)) self.dc_id = self.session.dc_id @@ -67,13 +69,13 @@ class UltroidClient(TelegramClient): await self.start(**kwargs) except ApiIdInvalidError: self.logger.critical("API ID and API_HASH combination does not match!") - + # Consider raising the error instead of sys.exit for better testability/embedding sys.exit() - except (AuthKeyDuplicatedError, EOFError) as er: + except (AuthKeyDuplicatedError, EOFError): # 'er' variable was unused if self._handle_error: self.logger.critical("String session expired. Create new!") - return sys.exit() - self.logger.critical("String session expired.") + sys.exit() # return sys.exit() is not valid, just sys.exit() + self.logger.critical("String session expired.") # This path might not be reachable if _handle_error is always True for sys.exit cases except (AccessTokenExpiredError, AccessTokenInvalidError): # AccessTokenError can only occur for Bot account # And at Early Process, Its saved in DB. @@ -87,10 +89,10 @@ class UltroidClient(TelegramClient): if self.me.bot: me = f"@{self.me.username}" else: - setattr(self.me, "phone", None) + setattr(self.me, "phone", None) # Ensure 'phone' attribute exists if accessed later me = self.full_name if self._log_at: - self.logger.info(f"Logged in as {me}") + self.logger.info("Logged in as %s", me) self._bot = await self.is_bot() async def fast_uploader(self, file, **kwargs): @@ -104,8 +106,9 @@ class UltroidClient(TelegramClient): filename = kwargs.get("filename", path.name) # Set to True and pass event to show progress bar. show_progress = kwargs.get("show_progress", False) + event = None # Initialize event to None if show_progress: - event = kwargs["event"] + event = kwargs["event"] # pylint: disable=possibly-used-before-assignment (logic handles this) # Whether to use cached file for uploading or not use_cache = kwargs.get("use_cache", True) # Delete original file after uploading @@ -168,8 +171,9 @@ class UltroidClient(TelegramClient): # Set to True and pass event to show progress bar. show_progress = kwargs.get("show_progress", False) filename = kwargs.get("filename", "") + event = None # Initialize event to None if show_progress: - event = kwargs["event"] + event = kwargs["event"] # pylint: disable=possibly-used-before-assignment (logic handles this) # Don't show progress bar when file size is less than 10MB. if file.size < 10 * 2**20: show_progress = False diff --git a/pyUltroid/startup/__init__.py b/pyUltroid/startup/__init__.py index df8cacd..6ca390c 100644 --- a/pyUltroid/startup/__init__.py +++ b/pyUltroid/startup/__init__.py @@ -87,10 +87,10 @@ if run_as_module: """ ) - LOGS.info(f"Python version - {platform.python_version()}") - LOGS.info(f"py-Ultroid Version - {__pyUltroid__}") - LOGS.info(f"Telethon Version - {__version__} [Layer: {LAYER}]") - LOGS.info(f"Ultroid Version - {ultroid_version} [{HOSTED_ON}]") + LOGS.info("Python version - %s", platform.python_version()) + LOGS.info("py-Ultroid Version - %s", __pyUltroid__) + LOGS.info("Telethon Version - %s [Layer: %s]", __version__, LAYER) + LOGS.info("Ultroid Version - %s [%s]", ultroid_version, HOSTED_ON) try: from safety.tools import * diff --git a/pyUltroid/startup/_database.py b/pyUltroid/startup/_database.py index f7bc274..8816916 100644 --- a/pyUltroid/startup/_database.py +++ b/pyUltroid/startup/_database.py @@ -50,7 +50,7 @@ else: class _BaseDatabase: - def __init__(self, *args, **kwargs): + def __init__(self): # Removed *args, **kwargs as they are not used by super() calls in subclasses self._cache = {} def get_key(self, key): @@ -78,28 +78,28 @@ class _BaseDatabase: def del_key(self, key): if key in self._cache: del self._cache[key] - self.delete(key) + self.delete(str(key)) # pylint: disable=no-member # Implemented in subclasses return True def _get_data(self, key=None, data=None): if key: - data = self.get(str(key)) + data = self.get(str(key)) # pylint: disable=no-member # Implemented in subclasses if data and isinstance(data, str): try: data = ast.literal_eval(data) - except BaseException: - pass + except (ValueError, SyntaxError, TypeError): # More specific exceptions for literal_eval + pass # Keep data as string if eval fails return data def set_key(self, key, value, cache_only=False): - value = self._get_data(data=value) + value = self._get_data(data=value) # Process value first self._cache[key] = value if cache_only: - return - return self.set(str(key), str(value)) + return True # Return True for consistency + return self.set(str(key), str(value)) # pylint: disable=no-member # Implemented in subclasses def rename(self, key1, key2): - _ = self.get_key(key1) + _ = self.get_key(key1) # Relies on get_key which uses self.get if _: self.del_key(key1) self.set_key(key2, _) @@ -171,9 +171,9 @@ class SqlDB(_BaseDatabase): self._cursor.execute( "CREATE TABLE IF NOT EXISTS Ultroid (ultroidCli varchar(70))" ) - except Exception as error: - LOGS.exception(error) - LOGS.info("Invaid SQL Database") + except psycopg2.Error as error: # Use specific psycopg2 base error + LOGS.error("SQL Database connection error: %s", error, exc_info=True) + LOGS.info("Invalid SQL Database configuration.") if self._connection: self._connection.close() sys.exit() @@ -200,8 +200,8 @@ class SqlDB(_BaseDatabase): def get(self, variable): try: - self._cursor.execute(f"SELECT {variable} FROM Ultroid") - except psycopg2.errors.UndefinedColumn: + self._cursor.execute(f"SELECT {variable} FROM Ultroid") # Ensure variable is sanitized if it comes from user input + except psycopg2.errors.UndefinedColumn: # pylint: disable=no-member return None data = self._cursor.fetchall() if not data: @@ -213,11 +213,19 @@ class SqlDB(_BaseDatabase): def set(self, key, value): try: + # Check if column exists before trying to drop, to avoid error if it doesn't self._cursor.execute(f"ALTER TABLE Ultroid DROP COLUMN IF EXISTS {key}") - except (psycopg2.errors.UndefinedColumn, psycopg2.errors.SyntaxError): + except psycopg2.errors.UndefinedColumn: # pylint: disable=no-member + # Column doesn't exist, which is fine for ensuring it's dropped. pass - except BaseException as er: - LOGS.exception(er) + except psycopg2.Error as er: # Catch specific psycopg2 errors + LOGS.error("Error dropping column %s: %s", key, er, exc_info=True) + # Depending on policy, may want to return False or raise + # except psycopg2.errors.SyntaxError: # pylint: disable=no-member + # # This might indicate an issue with the key name (e.g. SQL injection if not careful) + # LOGS.error("Syntax error while trying to drop column %s.", key, exc_info=True) + # pass + self._cache.update({key: value}) self._cursor.execute(f"ALTER TABLE Ultroid ADD {key} TEXT") self._cursor.execute(f"INSERT INTO Ultroid ({key}) values (%s)", (str(value),)) @@ -226,7 +234,10 @@ class SqlDB(_BaseDatabase): def delete(self, key): try: self._cursor.execute(f"ALTER TABLE Ultroid DROP COLUMN {key}") - except psycopg2.errors.UndefinedColumn: + except psycopg2.errors.UndefinedColumn: # pylint: disable=no-member + return False # Column didn't exist + except psycopg2.Error as e_drop: # Other SQL errors + LOGS.error("Error dropping column %s during delete: %s", key, e_drop) return False return True @@ -248,42 +259,68 @@ class RedisDB(_BaseDatabase): host, port, password, + *args, # Moved *args before keyword-only arguments platform="", logger=LOGS, - *args, **kwargs, ): - if host and ":" in host: - spli_ = host.split(":") - host = spli_[0] - port = int(spli_[-1]) - if host.startswith("http"): - logger.error("Your REDIS_URI should not start with http !") - import sys + effective_host = host + effective_port = port + effective_password = password - sys.exit() - elif not host or not port: - logger.error("Port Number not found") - import sys + if effective_host and ":" in effective_host: + spli_ = effective_host.split(":") + effective_host = spli_[0] + try: + effective_port = int(spli_[-1]) + except ValueError: + logger.error("Invalid port in REDIS_URI: %s", spli_[-1]) + sys.exit(1) - sys.exit() - kwargs["host"] = host - kwargs["password"] = password - kwargs["port"] = port + if effective_host.startswith("http"): # http(s) scheme is not for direct Redis connection string + logger.error("Your REDIS_URI (host part) should not start with http(s)://. Use only hostname/IP.") + sys.exit(1) + elif not effective_host or not effective_port: # Port might be part of URI or separate + # If REDIS_URI is `redis://user:pass@host:port` then host, port, password are parsed by redis-py itself if full URI is passed. + # This logic seems to be for when they are passed as separate Var components. + pass # Allow redis-py to handle it if full URI is passed to Redis() later - if platform.lower() == "qovery" and not host: - var, hash_, host, password = "", "", "", "" - for vars_ in os.environ: - if vars_.startswith("QOVERY_REDIS_") and vars.endswith("_HOST"): - var = vars_ - if var: - hash_ = var.split("_", maxsplit=2)[1].split("_")[0] - if hash: - kwargs["host"] = os.environ.get(f"QOVERY_REDIS_{hash_}_HOST") - kwargs["port"] = os.environ.get(f"QOVERY_REDIS_{hash_}_PORT") - kwargs["password"] = os.environ.get(f"QOVERY_REDIS_{hash_}_PASSWORD") - self.db = Redis(**kwargs) - self.set = self.db.set + # Qovery specific logic + # The `and not host` condition in original code for Qovery block seems problematic if host was already parsed from REDIS_URI. + # This block should ideally run if platform is qovery AND specific Qovery ENV vars are present, potentially overriding others. + if platform.lower() == "qovery": # Simpler condition: if on Qovery, try to use Qovery vars. + qovery_redis_host = None + qovery_hash = "" + for var_name in os.environ: + if var_name.startswith("QOVERY_REDIS_") and var_name.endswith("_HOST"): + qovery_hash = var_name.split("_", maxsplit=2)[1].split("_")[0] # Extract HASH part + qovery_redis_host = os.environ.get(var_name) + break # Found one, assume it's the one to use + + if qovery_redis_host and qovery_hash: + logger.info("Qovery environment detected, using Qovery Redis ENV vars.") + effective_host = qovery_redis_host + effective_port = int(os.environ.get(f"QOVERY_REDIS_{qovery_hash}_PORT", effective_port)) # Keep original if not found + effective_password = os.environ.get(f"QOVERY_REDIS_{qovery_hash}_PASSWORD", effective_password) + # Removed `if True:` block as it was unconditional. Logic now driven by qovery_redis_host and qovery_hash. + + # Construct connection_kwargs for Redis + connection_kwargs = kwargs # Start with user-passed kwargs + if effective_host: # Only override if we have a host (from Var or Qovery) + connection_kwargs["host"] = effective_host + if effective_port: + connection_kwargs["port"] = int(effective_port) # Ensure port is int + if effective_password: # Only override if we have a password + connection_kwargs["password"] = effective_password + + # If Var.REDIS_URI is a full URI string, redis-py can parse it directly. + # This logic assumes separate host/port/pass might be primary, or Qovery overrides. + # If Var.REDIS_URI is the primary source and is a full URI, this might be simpler. + # For now, respecting the structure that seems to prioritize individual components or Qovery. + + self.db = Redis(**connection_kwargs) # Pass all collected kwargs + # Alias methods + self.set = self.db.set # type: ignore self.get = self.db.get self.keys = self.db.keys self.delete = self.db.delete @@ -344,9 +381,12 @@ def UltroidDB(): "No DB requirement fullfilled!\nPlease install redis, mongo or sql dependencies...\nTill then using local file as database." ) return LocalDB() - except BaseException as err: - LOGS.exception(err) - exit() + except Exception as err: # Changed from BaseException + LOGS.error("Failed to initialize database: %s", err, exc_info=True) + # exit() here is problematic as it will stop the bot if any DB init fails. + # Consider returning None or raising a specific custom error to be handled by main. + # For now, keeping original exit() behavior. + sys.exit("Database initialization failed.") # --------------------------------------------------------------------------------------------- # diff --git a/pyUltroid/startup/connections.py b/pyUltroid/startup/connections.py index 005f199..2c4cae1 100644 --- a/pyUltroid/startup/connections.py +++ b/pyUltroid/startup/connections.py @@ -88,7 +88,7 @@ def vc_connection(udB, ultroid_bot): except (AuthKeyDuplicatedError, EOFError): LOGS.info(get_string("py_c3")) udB.del_key("VC_SESSION") - except Exception as er: - LOGS.info("While creating Client for VC.") - LOGS.exception(er) + except Exception as er: # Catching general Exception as client creation can have various issues + LOGS.error("Error while creating VcClient: %s", er, exc_info=True) + # Optionally, inform the user that VCBot might not work. return ultroid_bot diff --git a/pyUltroid/startup/funcs.py b/pyUltroid/startup/funcs.py index 93eca32..d162ca7 100644 --- a/pyUltroid/startup/funcs.py +++ b/pyUltroid/startup/funcs.py @@ -12,12 +12,13 @@ import shutil import time from random import randint -from ..configs import Var +from ..configs import Var as UltroidConfigVars # Renamed for clarity try: - from pytz import timezone + from pytz import timezone, exceptions as pytz_exceptions except ImportError: timezone = None + pytz_exceptions = None # Define if pytz not available from telethon.errors import ( ChannelsTooMuchError, @@ -25,6 +26,8 @@ from telethon.errors import ( MessageIdInvalidError, MessageNotModifiedError, UserNotParticipantError, + # Specific RPC errors if known for some operations + # e.g. rpcerrorlist.PhoneNumberInvalidError ) from telethon.tl.custom import Button from telethon.tl.functions.channels import ( @@ -42,76 +45,129 @@ from telethon.tl.types import ( InputMessagesFilterDocument, ) from telethon.utils import get_peer_id -from decouple import config, RepositoryEnv +from decouple import config as decouple_config, RepositoryEnv # Aliased decouple.config from .. import LOGS, ULTConfig from ..fns.helper import download_file, inline_mention, updater -db_url = 0 +db_url = 0 # Module level global async def autoupdate_local_database(): - from .. import Var, asst, udB, ultroid_bot + # Var here refers to the UltroidConfigVars instance if it's made global in __main__ or similar + # Or it should be passed or accessed consistently. + # Assuming Var is an instance of UltroidConfigVars accessible here. + # For now, let's assume it's correctly accessed from `from .. import Var` if Var is an instance. + # If `from .. import Var` actually imports the class, then this needs `ultroid_vars = UltroidConfigVars()` + # For this fix, I'll assume `Var` is the instance for now, as per original structure. + # If `Var` is the class, then `Var.TGDB_URL` would be an issue. + # The `configs.py` defines Var as a class. So `Var.TGDB_URL` is indeed problematic. + # This function is complex and relies on how Var, asst, udB, ultroid_bot are initialized and made available. + # Let's use `decouple_config` for TGDB_URL for safety, assuming it's an env var. + from .. import asst, udB, ultroid_bot, Var as GlobalUltroidConfig # Assuming this Var is the instance global db_url + # Prioritize udB, then direct decouple_config, then cache + # GlobalUltroidConfig.TGDB_URL isn't defined in configs.py. + # This line was `udB.get_key("TGDB_URL") or Var.TGDB_URL or ultroid_bot._cache.get("TGDB_URL")` + # Var.TGDB_URL is problematic. + current_tgdb_url_env = decouple_config("TGDB_URL", default=None) db_url = ( - udB.get_key("TGDB_URL") or Var.TGDB_URL or ultroid_bot._cache.get("TGDB_URL") + udB.get_key("TGDB_URL") or current_tgdb_url_env or ultroid_bot._cache.get("TGDB_URL") ) + if db_url: _split = db_url.split("/") - _channel = _split[-2] - _id = _split[-1] - try: - await asst.edit_message( - int(_channel) if _channel.isdigit() else _channel, - message=_id, - file="database.json", - text="**Do not delete this file.**", - ) - except MessageNotModifiedError: - return - except MessageIdInvalidError: - pass + if len(_split) > 2: # Basic check + _channel = _split[-2] + _id = _split[-1] + try: + await asst.edit_message( + int(_channel) if _channel.isdigit() else _channel, + message=int(_id), # message id should be int + file="database.json", + text="**Do not delete this file.**", + ) + except MessageNotModifiedError: + return + except (MessageIdInvalidError, ValueError): # ValueError if _id is not int + LOGS.warning("Invalid message ID or channel for TGDB_URL update: %s", db_url) + except Exception as e_inner: # More specific Telethon errors if possible + LOGS.error("Error editing TGDB message: %s", e_inner) + else: + LOGS.warning("Malformed TGDB_URL: %s", db_url) + try: - LOG_CHANNEL = ( + # Similar issue with Var.LOG_CHANNEL + current_log_channel_env = decouple_config("LOG_CHANNEL", default=0, cast=int) + log_channel_val = ( udB.get_key("LOG_CHANNEL") - or Var.LOG_CHANNEL + or current_log_channel_env or asst._cache.get("LOG_CHANNEL") or "me" ) + # Ensure log_channel_val is int if it's a number string for user_id/chat_id + if isinstance(log_channel_val, str) and log_channel_val.isdigit(): + log_channel_val = int(log_channel_val) + elif isinstance(log_channel_val, str) and log_channel_val.startswith("-") and log_channel_val[1:].isdigit(): + log_channel_val = int(log_channel_val) + + msg = await asst.send_message( - LOG_CHANNEL, "**Do not delete this file.**", file="database.json" + log_channel_val, "**Do not delete this file.**", file="database.json" ) asst._cache["TGDB_URL"] = msg.message_link udB.set_key("TGDB_URL", msg.message_link) - except Exception as ex: - LOGS.error(f"Error on autoupdate_local_database: {ex}") + except Exception as ex: # Catch more specific errors if known (e.g., telethon RPC errors) + LOGS.error("Error on autoupdate_local_database (sending new DB backup): %s", ex) def update_envs(): - """Update Var. attributes to udB""" + """Update Var attributes to udB""" from .. import udB _envs = [*list(os.environ)] if ".env" in os.listdir("."): - [_envs.append(_) for _ in list(RepositoryEnv(config._find_file(".")).data)] - for envs in _envs: + # Original: RepositoryEnv(config._find_file('.')).data + # decouple.config is now decouple_config. We need the original config object for _find_file + # This is tricky because decouple.config is a function. + # RepositoryEnv itself finds the .env file. + try: + env_data = RepositoryEnv(".env").data # Attempt to directly use RepositoryEnv + for key_val_tuple in env_data: # RepositoryEnv.data is a list of tuples + _envs.append(key_val_tuple[0]) # Add only keys to _envs list for consistency + except Exception as e: + LOGS.warning("Could not read .env file for update_envs: %s", e) + + for env_name in _envs: + # Check if this is one of the envs we care about syncing to udB if ( - envs in ["LOG_CHANNEL", "BOT_TOKEN", "BOTMODE", "DUAL_MODE", "language"] - or envs in udB.keys() + env_name in ["LOG_CHANNEL", "BOT_TOKEN", "BOTMODE", "DUAL_MODE", "language"] + or env_name in udB.keys() # Sync if it's already a key in udB ): - if _value := os.environ.get(envs): - udB.set_key(envs, _value) - else: - udB.set_key(envs, config.config.get(envs)) + # Prioritize os.environ, then decouple_config (which reads .env) + _value = os.environ.get(env_name) + if _value is None: + _value = decouple_config(env_name, default=None) # Use decouple_config here + + if _value is not None: # Only set if we found a value + udB.set_key(env_name, _value) + # No else needed: if not in os.environ and not in .env (decouple_config returns None), don't change udB async def startup_stuff(): from .. import udB - x = ["resources/auth", "resources/downloads"] - for x in x: - if not os.path.isdir(x): - os.mkdir(x) + # Original: x = ["resources/auth", "resources/downloads"] + # Original: for x in x: if not os.path.isdir(x): os.mkdir(x) + # This was the E0602: Undefined variable 'x' because 'x' (the list) was overwritten by 'x' (the item) + folder_paths = ["resources/auth", "resources/downloads"] + for folder_path in folder_paths: + if not os.path.isdir(folder_path): + try: + os.makedirs(folder_path, exist_ok=True) # Use makedirs for safety + except OSError as e: + LOGS.error("Failed to create directory %s: %s", folder_path, e) + CT = udB.get_key("CUSTOM_THUMBNAIL") if CT: @@ -119,14 +175,20 @@ async def startup_stuff(): ULTConfig.thumb = path try: await download_file(CT, path) - except Exception as er: - LOGS.exception(er) + except IOError as e: # More specific for file download issues + LOGS.error("Failed to download custom thumbnail: %s", e) + except Exception as er: # Catch other potential errors from download_file + LOGS.error("Generic error downloading custom thumbnail: %s", er) elif CT is False: ULTConfig.thumb = None GT = udB.get_key("GDRIVE_AUTH_TOKEN") if GT: - with open("resources/auth/gdrive_creds.json", "w") as t_file: - t_file.write(GT) + try: + with open("resources/auth/gdrive_creds.json", "w", encoding="utf-8") as t_file: + t_file.write(GT) + except IOError as e: + LOGS.error("Failed to write gdrive_creds.json: %s", e) + if udB.get_key("AUTH_TOKEN"): udB.del_key("AUTH_TOKEN") @@ -134,23 +196,29 @@ async def startup_stuff(): MM = udB.get_key("MEGA_MAIL") MP = udB.get_key("MEGA_PASS") if MM and MP: - with open(".megarc", "w") as mega: - mega.write(f"[Login]\nUsername = {MM}\nPassword = {MP}") + try: + with open(".megarc", "w", encoding="utf-8") as mega: + mega.write(f"[Login]\nUsername = {MM}\nPassword = {MP}") + except IOError as e: + LOGS.error("Failed to write .megarc file: %s", e) TZ = udB.get_key("TIMEZONE") - if TZ and timezone: + if TZ and timezone and pytz_exceptions: # Ensure pytz was imported try: - timezone(TZ) + timezone(TZ) # Validate timezone os.environ["TZ"] = TZ time.tzset() - except AttributeError as er: - LOGS.debug(er) - except BaseException: - LOGS.critical( - "Incorrect Timezone ,\nCheck Available Timezone From Here https://graph.org/Ultroid-06-18-2\nSo Time is Default UTC" + except pytz_exceptions.UnknownTimeZoneError: # Specific exception for timezone + LOGS.error( + "Incorrect Timezone '%s'. Check available timezones. Defaulting to UTC.", TZ ) os.environ["TZ"] = "UTC" time.tzset() + except AttributeError as er: # If timezone() call failed for other reasons + LOGS.debug("AttributeError setting timezone: %s", er) + # Removed broad BaseException catch here, be more specific if other errors occur + elif not timezone and TZ : + LOGS.warning("Timezone package (pytz) not available, cannot set timezone %s", TZ) async def autobot(): @@ -158,7 +226,7 @@ async def autobot(): if udB.get_key("BOT_TOKEN"): return - await ultroid_bot.start() + await ultroid_bot.start() # Assuming ultroid_bot is already connected if this is called LOGS.info("MAKING A TELEGRAM BOT FOR YOU AT @BotFather, Kindly Wait") who = ultroid_bot.me name = who.first_name + "'s Bot" @@ -167,333 +235,517 @@ async def autobot(): else: username = "ultroid_" + (str(who.id))[5:] + "_bot" bf = "@BotFather" - await ultroid_bot(UnblockRequest(bf)) - await ultroid_bot.send_message(bf, "/cancel") - await asyncio.sleep(1) - await ultroid_bot.send_message(bf, "/newbot") - await asyncio.sleep(1) - isdone = (await ultroid_bot.get_messages(bf, limit=1))[0].text - if isdone.startswith("That I cannot do.") or "20 bots" in isdone: - LOGS.critical( - "Please make a Bot from @BotFather and add it's token in BOT_TOKEN, as an env var and restart me." - ) - import sys - sys.exit(1) - await ultroid_bot.send_message(bf, name) - await asyncio.sleep(1) - isdone = (await ultroid_bot.get_messages(bf, limit=1))[0].text - if not isdone.startswith("Good."): - await ultroid_bot.send_message(bf, "My Assistant Bot") + try: + await ultroid_bot(UnblockRequest(bf)) + await ultroid_bot.send_message(bf, "/cancel") + await asyncio.sleep(1) # Consider making delays configurable or shorter if possible + await ultroid_bot.send_message(bf, "/newbot") await asyncio.sleep(1) - isdone = (await ultroid_bot.get_messages(bf, limit=1))[0].text - if not isdone.startswith("Good."): - LOGS.critical( - "Please make a Bot from @BotFather and add it's token in BOT_TOKEN, as an env var and restart me." - ) - import sys + isdone_msg = await ultroid_bot.get_messages(bf, limit=1) + if not isdone_msg: + LOGS.error("Failed to get response from BotFather after /newbot.") + return # Or sys.exit as originally + isdone = isdone_msg[0].text + + if isdone.startswith("That I cannot do.") or "20 bots" in isdone: + LOGS.critical( + "Please make a Bot from @BotFather and add its token in BOT_TOKEN, as an env var and restart me." + ) + sys.exit(1) # Keep sys.exit for critical setup failures + + await ultroid_bot.send_message(bf, name) + await asyncio.sleep(1) + isdone_msg = await ultroid_bot.get_messages(bf, limit=1) + if not isdone_msg: + LOGS.error("Failed to get response from BotFather after sending bot name.") + return + isdone = isdone_msg[0].text + + if not isdone.startswith("Good."): + await ultroid_bot.send_message(bf, "My Assistant Bot") # Fallback name + await asyncio.sleep(1) + isdone_msg = await ultroid_bot.get_messages(bf, limit=1) + if not isdone_msg: + LOGS.error("Failed to get response from BotFather after sending fallback name.") + return + isdone = isdone_msg[0].text + if not isdone.startswith("Good."): + LOGS.critical( + "Failed to set bot name. Please make a Bot from @BotFather and add its token in BOT_TOKEN." + ) + sys.exit(1) - sys.exit(1) - await ultroid_bot.send_message(bf, username) - await asyncio.sleep(1) - isdone = (await ultroid_bot.get_messages(bf, limit=1))[0].text - await ultroid_bot.send_read_acknowledge("botfather") - if isdone.startswith("Sorry,"): - ran = randint(1, 100) - username = "ultroid_" + (str(who.id))[6:] + str(ran) + "_bot" await ultroid_bot.send_message(bf, username) await asyncio.sleep(1) - isdone = (await ultroid_bot.get_messages(bf, limit=1))[0].text - if isdone.startswith("Done!"): - token = isdone.split("`")[1] - udB.set_key("BOT_TOKEN", token) - await enable_inline(ultroid_bot, username) - LOGS.info( - f"Done. Successfully created @{username} to be used as your assistant bot!" - ) - else: - LOGS.info( - "Please Delete Some Of your Telegram bots at @Botfather or Set Var BOT_TOKEN with token of a bot" - ) + isdone_msg = await ultroid_bot.get_messages(bf, limit=1) + if not isdone_msg: + LOGS.error("Failed to get response from BotFather after sending username.") + return + isdone = isdone_msg[0].text + await ultroid_bot.send_read_acknowledge(bf) # Acknowledge BotFather's messages - import sys + if isdone.startswith("Sorry,"): # Username taken + ran = randint(1, 100) + username = "ultroid_" + (str(who.id))[6:] + str(ran) + "_bot" + await ultroid_bot.send_message(bf, username) + await asyncio.sleep(1) + isdone_msg = await ultroid_bot.get_messages(bf, limit=1) + if not isdone_msg: + LOGS.error("Failed to get response from BotFather after sending new username.") + return + isdone = isdone_msg[0].text - sys.exit(1) + if isdone.startswith("Done!"): + try: + token = isdone.split("`")[1] + udB.set_key("BOT_TOKEN", token) + await enable_inline(ultroid_bot, username) + LOGS.info( + "Done. Successfully created @%s to be used as your assistant bot!", username + ) + except IndexError: + LOGS.error("Failed to parse token from BotFather's response: %s", isdone) + sys.exit(1) + else: + LOGS.error( + "Bot creation failed at BotFather with response: %s. " + "Please delete some of your Telegram bots or set BOT_TOKEN manually.", isdone + ) + sys.exit(1) + except UserNotParticipantError: # Or other specific Telethon errors + LOGS.error("BotFather interaction failed: UserNotParticipantError or similar. Is @BotFather blocked?") + except Exception as e: # Catch-all for other unexpected errors during bot creation + LOGS.error("An unexpected error occurred during autobot setup: %s", e, exc_info=True) + # Consider if sys.exit is needed here or if the bot can continue without assistant. async def autopilot(): from .. import asst, udB, ultroid_bot - channel = udB.get_key("LOG_CHANNEL") - new_channel = None - if channel: - try: - chat = await ultroid_bot.get_entity(channel) - except BaseException as err: - LOGS.exception(err) - udB.del_key("LOG_CHANNEL") - channel = None - if not channel: + channel_id_str = udB.get_key("LOG_CHANNEL") + new_channel_created = False # Flag to track if we created the channel in this run + chat = None - async def _save(exc): - udB._cache["LOG_CHANNEL"] = ultroid_bot.me.id + if channel_id_str: + try: + # Ensure channel_id_str is correctly formatted for get_entity (int or specific string) + if isinstance(channel_id_str, str) and (channel_id_str.isdigit() or (channel_id_str.startswith("-") and channel_id_str[1:].isdigit())): + channel_entity_id = int(channel_id_str) + else: # Assume it's a username or invite link if not purely numeric string + channel_entity_id = channel_id_str + chat = await ultroid_bot.get_entity(channel_entity_id) + channel_id_str = get_peer_id(chat) # Normalize to peer ID + udB.set_key("LOG_CHANNEL", str(channel_id_str)) # Save normalized ID + except ValueError: # If channel_id_str is not a valid entity identifier + LOGS.error("LOG_CHANNEL value '%s' is not a valid channel/chat ID or username.", channel_id_str) + udB.del_key("LOG_CHANNEL") + channel_id_str = None + except Exception as err: # Catch other specific Telethon errors if possible + LOGS.error("Error getting entity for LOG_CHANNEL %s: %s", channel_id_str, err, exc_info=False) + udB.del_key("LOG_CHANNEL") # Invalidate problematic channel ID + channel_id_str = None + + if not channel_id_str: + async def _save_pm_fallback(exc_msg): + # Fallback to PM if channel creation fails + udB.set_key("LOG_CHANNEL", str(ultroid_bot.me.id)) # Use string for consistency await asst.send_message( - ultroid_bot.me.id, f"Failed to Create Log Channel due to {exc}.." + ultroid_bot.me.id, f"Failed to Create/Verify Log Channel due to: {exc_msg}. Logging to PM." ) - if ultroid_bot._bot: - msg_ = "'LOG_CHANNEL' not found! Add it in order to use 'BOTMODE'" + if ultroid_bot._bot: # Assuming _bot means it's running in BOT_MODE + msg_ = "'LOG_CHANNEL' not found! Add it in order to use 'BOTMODE'." LOGS.error(msg_) - return await _save(msg_) + # In BOT_MODE, perhaps we shouldn't fallback to PM, or make it configurable. + # For now, let's assume it should not create a channel in BOT_MODE if not set. + return + LOGS.info("Creating a Log Channel for You!") try: r = await ultroid_bot( CreateChannelRequest( title="My Ultroid Logs", - about="My Ultroid Log Group\n\n Join @TeamUltroid", - megagroup=True, + about="My Ultroid Log Group\n\nJoin @TeamUltroid", + megagroup=True, # Creating a supergroup ), ) + chat = r.chats[0] + channel_id_str = str(get_peer_id(chat)) # Store as string + udB.set_key("LOG_CHANNEL", channel_id_str) + new_channel_created = True + LOGS.info("Successfully created new log channel: %s", channel_id_str) except ChannelsTooMuchError as er: LOGS.critical( - "You Are in Too Many Channels & Groups , Leave some And Restart The Bot" - ) - return await _save(str(er)) - except BaseException as er: - LOGS.exception(er) - LOGS.info( - "Something Went Wrong , Create A Group and set its id on config var LOG_CHANNEL." + "You are in too many channels & groups. Please leave some and restart the bot." ) + await _save_pm_fallback(str(er)) + return + except Exception as er: # Catch other specific Telethon errors + LOGS.error("Failed to create log channel: %s", er, exc_info=True) + await _save_pm_fallback(str(er)) + return - return await _save(str(er)) - new_channel = True - chat = r.chats[0] - channel = get_peer_id(chat) - udB.set_key("LOG_CHANNEL", channel) - assistant = True + if not chat and channel_id_str : # If channel_id_str was from DB but chat object wasn't fetched + try: + chat = await ultroid_bot.get_entity(int(channel_id_str)) # Assuming it's an int ID by now + except Exception as e: + LOGS.error("Failed to get chat entity for existing LOG_CHANNEL %s: %s", channel_id_str, e) + return # Cannot proceed without chat object + + if not chat: + LOGS.error("Log channel setup failed, chat object is None.") + return + + # Assistant invitation and promotion logic + assistant_can_operate = True try: - await ultroid_bot.get_permissions(int(channel), asst.me.username) + # get_permissions requires int channel ID + await ultroid_bot.get_permissions(int(channel_id_str), asst.me.username) except UserNotParticipantError: + LOGS.info("Assistant not in log channel. Inviting...") try: - await ultroid_bot(InviteToChannelRequest(int(channel), [asst.me.username])) - except BaseException as er: - LOGS.info("Error while Adding Assistant to Log Channel") - LOGS.exception(er) - assistant = False - except BaseException as er: - assistant = False - LOGS.exception(er) - if assistant and new_channel: + await ultroid_bot(InviteToChannelRequest(int(channel_id_str), [asst.me.username])) + except Exception as er: # Catch specific errors like ChatAdminRequired, UserBlocked, etc. + LOGS.error("Error inviting assistant to log channel: %s", er) + assistant_can_operate = False + except Exception as er: + LOGS.error("Error checking assistant permissions in log channel: %s", er) + assistant_can_operate = False + + if assistant_can_operate and new_channel_created: # Only try to promote if we just created it and assistant is in + LOGS.info("Promoting assistant in newly created log channel...") try: - achat = await asst.get_entity(int(channel)) - except BaseException as er: - achat = None - LOGS.info("Error while getting Log channel from Assistant") - LOGS.exception(er) - if achat and not achat.admin_rights: + # Ensure asst client has fetched the channel entity too + await asst.get_entity(int(channel_id_str)) + rights = ChatAdminRights( - add_admins=True, - invite_users=True, - change_info=True, - ban_users=True, - delete_messages=True, - pin_messages=True, - anonymous=False, - manage_call=True, + add_admins=True, invite_users=True, change_info=True, + ban_users=True, delete_messages=True, pin_messages=True, + anonymous=False, manage_call=True, ) - try: - await ultroid_bot( - EditAdminRequest( - int(channel), asst.me.username, rights, "Assistant" - ) - ) - except ChatAdminRequiredError: - LOGS.info( - "Failed to promote 'Assistant Bot' in 'Log Channel' due to 'Admin Privileges'" - ) - except BaseException as er: - LOGS.info("Error while promoting assistant in Log Channel..") - LOGS.exception(er) - if isinstance(chat.photo, ChatPhotoEmpty): - photo, _ = await download_file( - "https://graph.org/file/27c6812becf6f376cbb10.jpg", "channelphoto.jpg" - ) - ll = await ultroid_bot.upload_file(photo) - try: await ultroid_bot( - EditPhotoRequest(int(channel), InputChatUploadedPhoto(ll)) + EditAdminRequest(int(channel_id_str), asst.me.username, rights, "Assistant") ) - except BaseException as er: - LOGS.exception(er) - os.remove(photo) + LOGS.info("Successfully promoted assistant in log channel.") + except ChatAdminRequiredError: + LOGS.warning( + "Failed to promote 'Assistant Bot' in 'Log Channel': Bot needs admin rights in the channel." + ) + except Exception as er: + LOGS.error("Error promoting assistant in log channel: %s", er, exc_info=True) - -# customize assistant + # Set channel photo if it's new or has no photo + if chat and isinstance(chat.photo, ChatPhotoEmpty): + photo_path = None + try: + photo_url = "https://graph.org/file/27c6812becf6f376cbb10.jpg" + photo_path, _ = await download_file(photo_url, "channelphoto.jpg") + uploaded_photo_file = await ultroid_bot.upload_file(photo_path) + await ultroid_bot( + EditPhotoRequest(int(channel_id_str), InputChatUploadedPhoto(uploaded_photo_file)) + ) + LOGS.info("Successfully set photo for log channel.") + except Exception as er: # Catch specific errors for download/upload/editphoto + LOGS.error("Failed to set photo for log channel: %s", er) + finally: + if photo_path and os.path.exists(photo_path): + os.remove(photo_path) async def customize(): from .. import asst, udB, ultroid_bot - rem = None + downloaded_profile_pic_path = None try: - chat_id = udB.get_key("LOG_CHANNEL") - if asst.me.photo: + log_channel_id_str = udB.get_key("LOG_CHANNEL") + if not log_channel_id_str: + LOGS.warning("LOG_CHANNEL not found, cannot send customization status message.") + # Decide if customization should proceed without a log channel or return. + # For now, let's allow it to proceed but it won't be able to send status updates. + + if asst.me.photo: # Check if assistant already has a profile photo + LOGS.info("Assistant bot already has a profile picture. Skipping customization of picture.") + # Optionally, one could still update description/about text. For now, full skip. return - LOGS.info("Customising Ur Assistant Bot in @BOTFATHER") - UL = f"@{asst.me.username}" - if not ultroid_bot.me.username: - sir = ultroid_bot.me.first_name + + LOGS.info("Customising Assistant Bot via @BotFather...") + assistant_username_mention = f"@{asst.me.username}" + master_mention = ultroid_bot.me.first_name + if ultroid_bot.me.username: + master_mention = f"@{ultroid_bot.me.username}" + + # Profile picture selection + profile_pic_options = [ + "https://graph.org/file/92cd6dbd34b0d1d73a0da.jpg", + "https://graph.org/file/a97973ee0425b523cdc28.jpg", + "resources/extras/ultroid_assistant.jpg", # Local fallback + ] + chosen_pic_source = random.choice(profile_pic_options) + + if chosen_pic_source.startswith("http"): + downloaded_profile_pic_path, _ = await download_file(chosen_pic_source, "profile_temp.jpg") + elif os.path.exists(chosen_pic_source): + downloaded_profile_pic_path = chosen_pic_source else: - sir = f"@{ultroid_bot.me.username}" - file = random.choice( - [ - "https://graph.org/file/92cd6dbd34b0d1d73a0da.jpg", - "https://graph.org/file/a97973ee0425b523cdc28.jpg", - "resources/extras/ultroid_assistant.jpg", - ] - ) - if not os.path.exists(file): - file, _ = await download_file(file, "profile.jpg") - rem = True - msg = await asst.send_message( - chat_id, "**Auto Customisation** Started on @Botfather" - ) + LOGS.warning("Selected profile picture source %s not found or inaccessible.", chosen_pic_source) + downloaded_profile_pic_path = None # No picture will be set + + status_msg = None + if log_channel_id_str: + try: + status_msg = await asst.send_message( + int(log_channel_id_str), "**Auto Customisation** initiated with @BotFather..." + ) + except Exception as e_log: + LOGS.warning("Failed to send initial customization status to log channel: %s", e_log) + + bot_father_handle = "BotFather" # Less prone to typos + + # Interaction with BotFather + await ultroid_bot.send_message(bot_father_handle, "/cancel") # Start clean await asyncio.sleep(1) - await ultroid_bot.send_message("botfather", "/cancel") + + if downloaded_profile_pic_path: + await ultroid_bot.send_message(bot_father_handle, "/setuserpic") + await asyncio.sleep(1) + # Add check for BotFather's response here if needed + await ultroid_bot.send_message(bot_father_handle, assistant_username_mention) + await asyncio.sleep(1) + await ultroid_bot.send_file(bot_father_handle, downloaded_profile_pic_path) + await asyncio.sleep(2) # Allow time for processing + LOGS.info("Assistant profile picture updated.") + + # Set About Text + await ultroid_bot.send_message(bot_father_handle, "/setabouttext") await asyncio.sleep(1) - await ultroid_bot.send_message("botfather", "/setuserpic") - await asyncio.sleep(1) - isdone = (await ultroid_bot.get_messages("botfather", limit=1))[0].text - if isdone.startswith("Invalid bot"): - LOGS.info("Error while trying to customise assistant, skipping...") - return - await ultroid_bot.send_message("botfather", UL) - await asyncio.sleep(1) - await ultroid_bot.send_file("botfather", file) - await asyncio.sleep(2) - await ultroid_bot.send_message("botfather", "/setabouttext") - await asyncio.sleep(1) - await ultroid_bot.send_message("botfather", UL) + await ultroid_bot.send_message(bot_father_handle, assistant_username_mention) await asyncio.sleep(1) await ultroid_bot.send_message( - "botfather", f"โœจ Hello โœจ!! I'm Assistant Bot of {sir}" + bot_father_handle, f"โœจ Hello โœจ!! I'm Assistant Bot of {master_mention}" ) await asyncio.sleep(2) - await ultroid_bot.send_message("botfather", "/setdescription") + LOGS.info("Assistant about text updated.") + + # Set Description + await ultroid_bot.send_message(bot_father_handle, "/setdescription") await asyncio.sleep(1) - await ultroid_bot.send_message("botfather", UL) + await ultroid_bot.send_message(bot_father_handle, assistant_username_mention) await asyncio.sleep(1) await ultroid_bot.send_message( - "botfather", - f"โœจ Powerful Ultroid Assistant Bot โœจ\nโœจ Master ~ {sir} โœจ\n\nโœจ Powered By ~ @TeamUltroid โœจ", + bot_father_handle, + f"โœจ Powerful Ultroid Assistant Bot โœจ\nโœจ Master ~ {master_mention} โœจ\n\nโœจ Powered By ~ @TeamUltroid โœจ" ) await asyncio.sleep(2) - await msg.edit("Completed **Auto Customisation** at @BotFather.") - if rem: - os.remove(file) - LOGS.info("Customisation Done") - except Exception as e: - LOGS.exception(e) + LOGS.info("Assistant description updated.") + + await ultroid_bot.send_read_acknowledge(bot_father_handle) + + if status_msg: + await status_msg.edit("Completed **Auto Customisation** at @BotFather.") + LOGS.info("Assistant bot customization complete.") + + except Exception as e: # Catch more specific Telethon errors if they occur often + LOGS.error("Error during assistant customization: %s", e, exc_info=True) + if status_msg: + try: + await status_msg.edit(f"Customization failed: {e}") + except: pass # Ignore error editing status message + finally: + if downloaded_profile_pic_path and downloaded_profile_pic_path == "profile_temp.jpg" and os.path.exists(downloaded_profile_pic_path): + os.remove(downloaded_profile_pic_path) async def plug(plugin_channels): from .. import ultroid_bot - from .utils import load_addons + from .utils import load_addons # Assuming this is correctly placed - if ultroid_bot._bot: - LOGS.info("Plugin Channels can't be used in 'BOTMODE'") + if ultroid_bot._bot: # Check if running in BOT_MODE + LOGS.info("Plugin Channels can't be used in 'BOTMODE'. Skipping plugin loading from channels.") return - if os.path.exists("addons") and not os.path.exists("addons/.git"): - shutil.rmtree("addons") - if not os.path.exists("addons"): - os.mkdir("addons") - if not os.path.exists("addons/__init__.py"): - with open("addons/__init__.py", "w") as f: - f.write("from plugins import *\n\nbot = ultroid_bot") - LOGS.info("โ€ข Loading Plugins from Plugin Channel(s) โ€ข") - for chat in plugin_channels: - LOGS.info(f"{'โ€ข' * 4} {chat}") + + # Ensure addons directory exists + addons_dir = "addons" + if os.path.exists(addons_dir) and not os.path.isdir(addons_dir): + LOGS.error("'%s' exists but is not a directory. Cannot load addons.", addons_dir) + return # Or attempt to remove/rename file and create dir + if not os.path.exists(addons_dir): try: - async for x in ultroid_bot.iter_messages( - chat, search=".py", filter=InputMessagesFilterDocument, wait_time=10 + os.makedirs(addons_dir) + except OSError as e: + LOGS.error("Failed to create addons directory '%s': %s", addons_dir, e) + return + + # Ensure addons/__init__.py exists + addons_init_py = os.path.join(addons_dir, "__init__.py") + if not os.path.exists(addons_init_py): + try: + with open(addons_init_py, "w", encoding="utf-8") as f: + f.write("# This file makes Python treat the 'addons' directory as a package.\n") + # The original content was "from plugins import *\n\nbot = ultroid_bot" + # This wildcard import is generally discouraged. + # If addons need access to `bot` or `plugins`, they should import them directly or be passed context. + # For now, keeping it minimal. If addons rely on this, it's a deeper refactor. + f.write("from .. import ultroid_bot as bot\n") # Make bot instance available if needed + f.write("from ..plugins import *\n") # If addons truly need this, but it's a lot. + except IOError as e: + LOGS.error("Failed to create %s: %s", addons_init_py, e) + # Decide if to proceed without it. Some loading mechanisms might fail. + + LOGS.info("โ€ข Loading Plugins from Plugin Channel(s) โ€ข") + for channel_identifier in plugin_channels: # Renamed 'chat' to 'channel_identifier' for clarity + LOGS.info("โ€ข โ€ข โ€ข โ€ข Processing channel: %s", channel_identifier) + try: + # Ensure channel_identifier is valid for get_entity + if isinstance(channel_identifier, str) and (channel_identifier.isdigit() or \ + (channel_identifier.startswith("-") and channel_identifier[1:].isdigit())): + entity = int(channel_identifier) + else: + entity = channel_identifier + + async for message_item in ultroid_bot.iter_messages( + entity, search=".py", filter=InputMessagesFilterDocument, wait_time=10 ): - plugin = "addons/" + x.file.name.replace("_", "-").replace("|", "-") - if not os.path.exists(plugin): - await asyncio.sleep(0.6) - if x.text == "#IGNORE": - continue - plugin = await x.download_media(plugin) - try: - load_addons(plugin) - except Exception as e: - LOGS.info(f"Ultroid - PLUGIN_CHANNEL - ERROR - {plugin}") - LOGS.exception(e) - os.remove(plugin) - except Exception as er: - LOGS.exception(er) + if not hasattr(message_item, 'file') or not message_item.file or not hasattr(message_item.file, 'name'): + LOGS.warning("Skipping message without valid file attribute or name in channel %s", channel_identifier) + continue + + # Sanitize plugin filename + safe_filename = message_item.file.name.replace("_", "-").replace("|", "-").replace("/", "-").replace("\\", "-") + plugin_path = os.path.join(addons_dir, safe_filename) + + if os.path.exists(plugin_path): + LOGS.info("Plugin %s already exists. Skipping download.", plugin_path) + continue # Or implement an update mechanism + + await asyncio.sleep(0.6) # Rate limiting? + if message_item.text and message_item.text.strip() == "#IGNORE": + LOGS.info("Ignoring plugin %s as per #IGNORE tag.", safe_filename) + continue + + downloaded_path = None + try: + downloaded_path = await message_item.download_media(file=plugin_path) + if downloaded_path: + LOGS.info("Downloaded plugin: %s", downloaded_path) + load_addons(downloaded_path) # Assuming load_addons handles its own exceptions for loading + else: + LOGS.error("Failed to download plugin %s from %s", safe_filename, channel_identifier) + except Exception as load_err: # Catch errors from download or load_addons + LOGS.error("Error processing plugin %s from %s: %s", safe_filename, channel_identifier, load_err, exc_info=False) + if downloaded_path and os.path.exists(downloaded_path): + try: + os.remove(downloaded_path) # Clean up failed download + except OSError as e_rm: + LOGS.error("Failed to remove corrupted plugin %s: %s", downloaded_path, e_rm) + except ValueError as ve: + LOGS.error("Invalid channel identifier '%s' for plugin loading: %s", channel_identifier, ve) + except Exception as er: # Catch errors like channel not found, permissions etc. + LOGS.error("Error iterating messages in plugin channel %s: %s", channel_identifier, er, exc_info=True) async def ready(): from .. import asst, udB, ultroid_bot - chat_id = udB.get_key("LOG_CHANNEL") - spam_sent = None + log_channel_id_str = udB.get_key("LOG_CHANNEL") + log_channel_id = None + if log_channel_id_str: + try: + log_channel_id = int(log_channel_id_str) + except ValueError: + LOGS.warning("LOG_CHANNEL ID '%s' is not an integer. Cannot send ready message.", log_channel_id_str) + # Fallback to PM or skip? For now, skip if not valid int. + log_channel_id = ultroid_bot.me.id # Fallback to self/PM + + spam_sent_msg = None + photo_to_send = None + buttons_to_send = None + if not udB.get_key("INIT_DEPLOY"): # Detailed Message at Initial Deploy - MSG = """๐ŸŽ‡ **Thanks for Deploying Ultroid Userbot!** + msg_text = """๐ŸŽ‡ **Thanks for Deploying Ultroid Userbot!** โ€ข Here, are the Some Basic stuff from, where you can Know, about its Usage.""" - PHOTO = "https://graph.org/file/54a917cc9dbb94733ea5f.jpg" - BTTS = Button.inline("โ€ข Click to Start โ€ข", "initft_2") + photo_to_send = "https://graph.org/file/54a917cc9dbb94733ea5f.jpg" + buttons_to_send = Button.inline("โ€ข Click to Start โ€ข", "initft_2") udB.set_key("INIT_DEPLOY", "Done") else: - MSG = f"**Ultroid has been deployed!**\nโž–โž–โž–โž–โž–โž–โž–โž–โž–โž–\n**UserMode**: {inline_mention(ultroid_bot.me)}\n**Assistant**: @{asst.me.username}\nโž–โž–โž–โž–โž–โž–โž–โž–โž–โž–\n**Support**: @TeamUltroid\nโž–โž–โž–โž–โž–โž–โž–โž–โž–โž–" - BTTS, PHOTO = None, None - prev_spam = udB.get_key("LAST_UPDATE_LOG_SPAM") - if prev_spam: + msg_text = ( + f"**Ultroid has been deployed!**\n" + f"โž–โž–โž–โž–โž–โž–โž–โž–โž–โž–\n" + f"**UserMode**: {inline_mention(ultroid_bot.me)}\n" + f"**Assistant**: @{asst.me.username}\n" + f"โž–โž–โž–โž–โž–โž–โž–โž–โž–โž–\n" + f"**Support**: @TeamUltroid\n" + f"โž–โž–โž–โž–โž–โž–โž–โž–โž–โž–" + ) + prev_spam_id = udB.get_key("LAST_UPDATE_LOG_SPAM") + if prev_spam_id and log_channel_id: try: - await ultroid_bot.delete_messages(chat_id, int(prev_spam)) - except Exception as E: - LOGS.info("Error while Deleting Previous Update Message :" + str(E)) - if await updater(): - BTTS = Button.inline("Update Available", "updtavail") + await ultroid_bot.delete_messages(log_channel_id, int(prev_spam_id)) + except Exception as e_del: # Catch specific errors like MessageDeleteForbiddenError + LOGS.warning("Error deleting previous update message %s: %s", prev_spam_id, e_del) - try: - spam_sent = await asst.send_message(chat_id, MSG, file=PHOTO, buttons=BTTS) - except ValueError as e: + update_available = await updater() # Assuming updater() returns a boolean or relevant info + if update_available: # This might need adjustment based on what updater() returns + buttons_to_send = Button.inline("Update Available", "updtavail") + + if log_channel_id: # Only try to send if we have a valid log_channel_id try: - await (await ultroid_bot.send_message(chat_id, str(e))).delete() - spam_sent = await asst.send_message(chat_id, MSG, file=PHOTO, buttons=BTTS) - except Exception as g: - LOGS.info(g) - except Exception as el: - LOGS.info(el) - try: - spam_sent = await ultroid_bot.send_message(chat_id, MSG) - except Exception as ef: - LOGS.exception(ef) - if spam_sent and not spam_sent.media: - udB.set_key("LAST_UPDATE_LOG_SPAM", spam_sent.id) + spam_sent_msg = await asst.send_message( + log_channel_id, msg_text, file=photo_to_send, buttons=buttons_to_send + ) + except (ValueError, TypeError) as ve_te: # Catch errors if log_channel_id is invalid for sending + LOGS.warning("Failed to send 'ready' message to LOG_CHANNEL %s (ValueError/TypeError): %s. Trying with main client to PM.", log_channel_id, ve_te) + try: # Fallback to main client sending to self if assistant fails or channel invalid + spam_sent_msg = await ultroid_bot.send_message(ultroid_bot.me.id, msg_text, file=photo_to_send, buttons=buttons_to_send) + except Exception as e_fallback: + LOGS.error("Fallback 'ready' message to PM also failed: %s", e_fallback) + except Exception as e_send: # Catch other Telethon errors + LOGS.error("Failed to send 'ready' message to LOG_CHANNEL %s: %s", log_channel_id, e_send) + # Fallback to PM may also be useful here + try: + spam_sent_msg = await ultroid_bot.send_message(ultroid_bot.me.id, msg_text, file=photo_to_send, buttons=buttons_to_send) + except Exception as e_fallback_pm: + LOGS.error("Fallback 'ready' message to PM (after general error) also failed: %s", e_fallback_pm) + + + if spam_sent_msg and not spam_sent_msg.media: # Only store ID if message sent and no media (or adjust logic) + udB.set_key("LAST_UPDATE_LOG_SPAM", spam_sent_msg.id) try: await ultroid_bot(JoinChannelRequest("TheUltroid")) - except Exception as er: - LOGS.exception(er) + LOGS.info("Successfully joined @TheUltroid channel.") + except Exception as er: # Catch specific errors like UserAlreadyParticipantError, ChannelsTooMuchError + LOGS.warning("Could not join @TheUltroid channel: %s", er) async def WasItRestart(udb): - key = udb.get_key("_RESTART") - if not key: + restart_key = udb.get_key("_RESTART") + if not restart_key: return - from .. import asst, ultroid_bot + from .. import asst, ultroid_bot + LOGS.info("Processing restart message confirmation...") try: - data = key.split("_") - who = asst if data[0] == "bot" else ultroid_bot - await who.edit_message( - int(data[1]), int(data[2]), "__Restarted Successfully.__" - ) - except Exception as er: - LOGS.exception(er) - udb.del_key("_RESTART") + data_parts = restart_key.split("_") + if len(data_parts) < 3: + LOGS.error("Invalid _RESTART key format: %s", restart_key) + udb.del_key("_RESTART") + return + + client_type, chat_id_str, msg_id_str = data_parts[0], data_parts[1], data_parts[2] + + chat_id = int(chat_id_str) + msg_id = int(msg_id_str) + + target_client = asst if client_type == "bot" else ultroid_bot + await target_client.edit_message(chat_id, msg_id, "__Restarted Successfully.__") + LOGS.info("Successfully edited restart confirmation message.") + except ValueError: + LOGS.error("Invalid chat_id or msg_id in _RESTART key: %s", restart_key) + except Exception as er: # Catch specific Telethon errors if possible + LOGS.error("Failed to edit restart message: %s", er, exc_info=True) + finally: + udb.del_key("_RESTART") # Always remove the key def _version_changes(udb): diff --git a/pyUltroid/startup/loader.py b/pyUltroid/startup/loader.py index 6975589..2eb087a 100644 --- a/pyUltroid/startup/loader.py +++ b/pyUltroid/startup/loader.py @@ -28,20 +28,20 @@ def _after_load(loader, module, plugin_name=""): if doc_ := get_help(plugin_name) or module.__doc__: try: doc = doc_.format(i=HNDLR) - except Exception as er: + except Exception as er: # Formatting can raise various errors, Exception is okay here. loader._logger.exception(er) - loader._logger.info(f"Error in {plugin_name}: {module}") + loader._logger.info("Error in %s: %s", plugin_name, module) return if loader.key in HELP.keys(): update_cmd = HELP[loader.key] try: update_cmd.update({plugin_name: doc}) - except BaseException as er: + except Exception as er: # Changed from BaseException loader._logger.exception(er) else: try: HELP.update({loader.key: {plugin_name: doc}}) - except BaseException as em: + except Exception as em: # Changed from BaseException loader._logger.exception(em) @@ -67,30 +67,42 @@ def load_other_plugins(addons=None, pmbot=None, manager=None, vcbot=None): # for addons if addons: if url := udB.get_key("ADDONS_URL"): - subprocess.run(f"git clone -q {url} addons", shell=True) + subprocess.run(f"git clone -q {url} addons", shell=True, check=True) if os.path.exists("addons") and not os.path.exists("addons/.git"): - rmtree("addons") + rmtree("addons") # This is a forceful removal, ensure it's intended. if not os.path.exists("addons"): - subprocess.run( - f"git clone -q -b {Repo().active_branch} https://github.com/TeamUltroid/UltroidAddons.git addons", - shell=True, - ) - else: - subprocess.run("cd addons && git pull -q && cd ..", shell=True) + try: + branch_name = Repo().active_branch.name + subprocess.run( + f"git clone -q -b {branch_name} https://github.com/TeamUltroid/UltroidAddons.git addons", + shell=True, check=True, + ) + except Exception as e_git_clone: # Catch if Repo() or active_branch fails + LOGS.warning("Could not determine active branch for cloning UltroidAddons, defaulting to main/master: %s", e_git_clone) + subprocess.run( + "git clone -q https://github.com/TeamUltroid/UltroidAddons.git addons", + shell=True, check=True, # Default clone if specific branch fails + ) + else: # addons directory exists + if os.path.isdir("addons/.git"): # Only pull if it's a git repo + subprocess.run("cd addons && git pull -q && cd ..", shell=True, check=True) + else: + LOGS.info("'addons' directory exists but is not a git repository. Skipping pull.") - if not os.path.exists("addons"): + + if not os.path.exists("addons"): # Should be created by one of the clones above if logic is right + # This might be redundant if check=True causes exit on failure for previous commands. + LOGS.info("Cloning default UltroidAddons as addons directory still not found.") subprocess.run( "git clone -q https://github.com/TeamUltroid/UltroidAddons.git addons", - shell=True, + shell=True, check=True, ) if os.path.exists("addons/addons.txt"): # generally addons req already there so it won't take much time - # subprocess.run( - # "rm -rf /usr/local/lib/python3.*/site-packages/pip/_vendor/.wh*" - # ) + LOGS.info("Installing requirements from addons/addons.txt") subprocess.run( f"{sys.executable} -m pip install --no-cache-dir -q -r ./addons/addons.txt", - shell=True, + shell=True, check=True, # Added check=True ) _exclude = udB.get_key("EXCLUDE_ADDONS") @@ -122,18 +134,18 @@ def load_other_plugins(addons=None, pmbot=None, manager=None, vcbot=None): if os.path.exists("vcbot"): if os.path.exists("vcbot/.git"): - subprocess.run("cd vcbot && git pull", shell=True) + subprocess.run("cd vcbot && git pull -q", shell=True, check=True) else: - rmtree("vcbot") + rmtree("vcbot") # Forceful removal if not os.path.exists("vcbot"): subprocess.run( - "git clone https://github.com/TeamUltroid/VcBot vcbot", shell=True + "git clone -q https://github.com/TeamUltroid/VcBot vcbot", shell=True, check=True ) try: if not os.path.exists("vcbot/downloads"): - os.mkdir("vcbot/downloads") + os.makedirs("vcbot/downloads", exist_ok=True) Loader(path="vcbot", key="VCBot").load(after_load=_after_load) except FileNotFoundError as e: - LOGS.error(f"{e} Skipping VCBot Installation.") + LOGS.error("%s Skipping VCBot Installation.", e) except ModuleNotFoundError: LOGS.error("'pytgcalls' not installed!\nSkipping loading of VCBOT.") diff --git a/pyUltroid/startup/utils.py b/pyUltroid/startup/utils.py index 5445dbf..4e1761b 100644 --- a/pyUltroid/startup/utils.py +++ b/pyUltroid/startup/utils.py @@ -90,10 +90,10 @@ def load_addons(plugin_name): update_cmd = HELP["Addons"] try: update_cmd.update({base_name: doc}) - except BaseException: + except Exception: # Changed from BaseException pass else: try: HELP.update({"Addons": {base_name: doc}}) - except BaseException as em: + except Exception: # Changed from BaseException, removed unused 'em' pass diff --git a/requirements.txt b/requirements.txt index 8e7faf3..3525080 100644 --- a/requirements.txt +++ b/requirements.txt @@ -75,9 +75,9 @@ pymysql cryptg jikanpy pyfiglet -pokedex +# pokedex # Package removed from PyPI, commented out for linting purposes lyrics-extractor -speech-recognition +SpeechRecognition # Corrected case shazamio htmlwebshot twikit