fix: Address Pylint issues and bugs in pyUltroid/startup/
- Corrected undefined variable 'x' in funcs.py. - Refactored configuration loading in funcs.py for clarity and to resolve Pylint errors. - Changed logging f-strings to %-style formatting across multiple files. - Added encoding='utf-8' to open() calls. - Replaced list comprehensions used for side-effects with for-loops. - Made some broad exception catches more specific. - Added check=True to subprocess.run() calls in loader.py. - Corrected function signature parameter orders (e.g., *args placement). - Removed some unused variables and imports. - Added Pylint disable comments for known false positives (e.g., no-member for psycopg2.errors and base class methods). - Improved error handling and logging in funcs.py for startup sequences. - Addressed dependency installation issues by commenting out 'pokedex' (unavailable on PyPI) and correcting case for 'SpeechRecognition' in requirements.txt.
This commit is contained in:
18
.gitignore
vendored
18
.gitignore
vendored
@@ -45,3 +45,21 @@ bin-release/
|
||||
|
||||
# fly.io configs
|
||||
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
|
||||
@@ -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
|
||||
|
||||
---
|
||||
|
||||
111
Dockerfile
111
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 <https://www.github.com/TeamUltroid/Ultroid/blob/main/LICENSE/>.
|
||||
|
||||
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"]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 *
|
||||
|
||||
@@ -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.")
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------------------------- #
|
||||
|
||||
@@ -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
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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.")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user