Compare commits
14 Commits
codex/fix-
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 53dad27b4f | |||
| 38211f8d54 | |||
| a540553e44 | |||
| 0a005fc338 | |||
|
|
b82621becb | ||
|
|
7e6d46d9c9 | ||
|
|
1cbb4513d7 | ||
|
|
3d81b8d6b8 | ||
|
|
63dc74e8a2 | ||
|
|
2d6c388a3e | ||
|
|
93b6cf7a63 | ||
|
|
66c5a5a797 | ||
|
|
160e792e8f | ||
|
|
b1cd5dfeb2 |
12
.ENV.example
Normal file
12
.ENV.example
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
# Sample environment for Meme Wrangler Docker stack.
|
||||||
|
# Copy to `.ENV` and adjust secrets before deploying.
|
||||||
|
TELEGRAM_BOT_TOKEN=
|
||||||
|
OWNER_ID=0
|
||||||
|
CHANNEL_ID=
|
||||||
|
POSTGRES_DB=meme_wrangler
|
||||||
|
POSTGRES_USER=meme
|
||||||
|
POSTGRES_PASSWORD=meme
|
||||||
|
POSTGRES_HOST=postgres
|
||||||
|
POSTGRES_PORT=5432
|
||||||
|
MEMEBOT_BACKUP_DIR=/app/backups
|
||||||
|
# MEMEBOT_BACKUP_PASSWORD_HASH=
|
||||||
@@ -5,3 +5,4 @@ __pycache__
|
|||||||
backups
|
backups
|
||||||
.env
|
.env
|
||||||
.env.*
|
.env.*
|
||||||
|
.ENV
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
# Copy this file to .env and fill in your actual values
|
# Copy this file to .env and fill in your actual values
|
||||||
|
|
||||||
TELEGRAM_BOT_TOKEN=your_bot_token_here
|
TELEGRAM_BOT_TOKEN=your_bot_token_here
|
||||||
|
# Comma-separated list of owner Telegram user IDs (e.g., 123456789,987654321)
|
||||||
OWNER_ID=your_telegram_user_id_here
|
OWNER_ID=your_telegram_user_id_here
|
||||||
CHANNEL_ID=@your_channel_or_id_here
|
CHANNEL_ID=@your_channel_or_id_here
|
||||||
# PostgreSQL connection string (format: postgresql://user:password@host:port/database)
|
# PostgreSQL connection string (format: postgresql://user:password@host:port/database)
|
||||||
|
|||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -2,4 +2,5 @@
|
|||||||
__pycache__/
|
__pycache__/
|
||||||
*.pyc
|
*.pyc
|
||||||
memes.db
|
memes.db
|
||||||
.env
|
.env
|
||||||
|
.ENV
|
||||||
|
|||||||
@@ -29,6 +29,6 @@
|
|||||||
- Link issues or TODOs, flag breaking changes in bold, and include screenshots only when UI-facing Telegram copy changes.
|
- Link issues or TODOs, flag breaking changes in bold, and include screenshots only when UI-facing Telegram copy changes.
|
||||||
|
|
||||||
## Deployment & Secret Tips
|
## Deployment & Secret Tips
|
||||||
- Keep `.env` out of version control; copy from `.env.example` and set `TELEGRAM_BOT_TOKEN`, `OWNER_ID`, `CHANNEL_ID`, `POSTGRES_*` credentials, and `DATABASE_URL` for local runs.
|
- Keep `.ENV` (or `.env`) out of version control; copy from `.ENV.example` and set `TELEGRAM_BOT_TOKEN`, `OWNER_ID`, `CHANNEL_ID`, `POSTGRES_*` credentials, and `DATABASE_URL` for local runs.
|
||||||
- Only override `MEMEBOT_BACKUP_PASSWORD_HASH` if you plan to replace the baked-in SHA-256 hash for backup commands.
|
- Only override `MEMEBOT_BACKUP_PASSWORD_HASH` if you plan to replace the baked-in SHA-256 hash for backup commands.
|
||||||
- Docker workflows mount backups in `./backups/` and keep the PostgreSQL cluster in the `pgdata` volume; prune carefully when resetting schedules. New memes automatically generate a fresh backup file in that directory.
|
- Docker workflows mount backups in `./backups/` and keep the PostgreSQL cluster in the `pgdata` volume; prune carefully when resetting schedules. New memes automatically generate a fresh backup file in that directory.
|
||||||
|
|||||||
@@ -37,8 +37,8 @@ sudo usermod -aG docker $USER
|
|||||||
Copy the example file and edit it:
|
Copy the example file and edit it:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cp .env.example .env
|
cp .ENV.example .ENV
|
||||||
nano .env # or use any text editor
|
nano .ENV # or use any text editor
|
||||||
```
|
```
|
||||||
|
|
||||||
Fill in your actual values:
|
Fill in your actual values:
|
||||||
@@ -49,16 +49,22 @@ CHANNEL_ID=@yourchannel
|
|||||||
POSTGRES_DB=meme_wrangler
|
POSTGRES_DB=meme_wrangler
|
||||||
POSTGRES_USER=meme
|
POSTGRES_USER=meme
|
||||||
POSTGRES_PASSWORD=meme
|
POSTGRES_PASSWORD=meme
|
||||||
|
POSTGRES_HOST=postgres
|
||||||
|
POSTGRES_PORT=5432
|
||||||
|
# Optional: adjust which env file Compose should read (defaults to .ENV)
|
||||||
|
# COMPOSE_ENV_FILE=staging.env
|
||||||
# Optional: hash (SHA-256) for replacing the baked-in backup secret
|
# Optional: hash (SHA-256) for replacing the baked-in backup secret
|
||||||
# MEMEBOT_BACKUP_PASSWORD_HASH=<your_sha256_hash>
|
# MEMEBOT_BACKUP_PASSWORD_HASH=<your_sha256_hash>
|
||||||
# Optional: adjust DATABASE_URL for non-compose workflows
|
# Optional: adjust DATABASE_URL for non-compose workflows
|
||||||
# DATABASE_URL=postgresql://meme:meme@postgres:5432/meme_wrangler
|
# DATABASE_URL=postgresql://meme:meme@postgres:5432/meme_wrangler
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Leave `POSTGRES_HOST=postgres` when running through Docker Compose or Portainer; it's the internal service name that the bot rewrites into any localhost-style URLs so the container connects to the bundled PostgreSQL instance. Override it only if your database lives elsewhere.
|
||||||
|
|
||||||
### 2. Build and Run with Docker Compose (Easiest)
|
### 2. Build and Run with Docker Compose (Easiest)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Build and start in background
|
# Build and start in background (stack reads .ENV automatically)
|
||||||
docker-compose up -d
|
docker-compose up -d
|
||||||
|
|
||||||
# View logs
|
# View logs
|
||||||
@@ -73,6 +79,10 @@ docker-compose restart
|
|||||||
|
|
||||||
That's it! Your bot is now running in Docker! 🎉
|
That's it! Your bot is now running in Docker! 🎉
|
||||||
|
|
||||||
|
#### Deploying with Portainer Stacks
|
||||||
|
|
||||||
|
When launching the stack from Portainer, upload your `.ENV` file through the **Environment variables** tab—Portainer saves it beside the stack as `/data/compose/<stack-id>/.ENV`, which matches the default expected by `docker-compose.yml`. Only set `COMPOSE_ENV_FILE` if you deliberately use a different name.
|
||||||
|
|
||||||
## Alternative: Using Docker Commands Directly
|
## Alternative: Using Docker Commands Directly
|
||||||
|
|
||||||
### Build the Image
|
### Build the Image
|
||||||
@@ -152,9 +162,9 @@ ssh -i /path/to/ssh_key username@server_ip
|
|||||||
# Navigate to bot directory
|
# Navigate to bot directory
|
||||||
cd ~/meme-wrangler
|
cd ~/meme-wrangler
|
||||||
|
|
||||||
# Create .env file
|
# Create .ENV file
|
||||||
cp .env.example .env
|
cp .ENV.example .ENV
|
||||||
nano .env # Fill in your credentials
|
nano .ENV # Fill in your credentials
|
||||||
|
|
||||||
# Build and run
|
# Build and run
|
||||||
docker-compose up -d
|
docker-compose up -d
|
||||||
@@ -308,8 +318,8 @@ scp -i /path/to/key -r \
|
|||||||
ssh -i /path/to/key username@server_ip
|
ssh -i /path/to/key username@server_ip
|
||||||
cd ~/memebot
|
cd ~/memebot
|
||||||
|
|
||||||
# Create .env file
|
# Create .ENV file
|
||||||
nano .env # Add your credentials
|
nano .ENV # Add your credentials
|
||||||
|
|
||||||
# Run with Docker Compose
|
# Run with Docker Compose
|
||||||
docker-compose up -d
|
docker-compose up -d
|
||||||
|
|||||||
11
README.md
11
README.md
@@ -10,10 +10,10 @@ The easiest way to run the bot is using Docker:
|
|||||||
|
|
||||||
1. **Set up environment variables:**
|
1. **Set up environment variables:**
|
||||||
```bash
|
```bash
|
||||||
cp .env.example .env
|
cp .ENV.example .ENV
|
||||||
nano .env # Edit with your bot credentials
|
nano .ENV # Edit with your bot credentials
|
||||||
```
|
```
|
||||||
The compose stack expects PostgreSQL credentials, so populate `POSTGRES_DB`, `POSTGRES_USER`, and `POSTGRES_PASSWORD` (defaults provided). Backups are protected by a built-in SHA-256 hash; optionally define `MEMEBOT_BACKUP_PASSWORD_HASH` to replace it.
|
The compose file now looks for `.ENV` by default so Portainer and other orchestrators can supply secrets without additional flags. Copy `.ENV.example` to `.ENV`, then populate `TELEGRAM_BOT_TOKEN`, `OWNER_ID`, `CHANNEL_ID`, and the Postgres settings. Leave `POSTGRES_HOST=postgres` (and `POSTGRES_PORT=5432` unless your database listens elsewhere) when you run inside Docker. The bot rewrites any localhost URLs with that host so the container connects to the bundled PostgreSQL service instead of looping back on itself. You can still point the stack at a different file by exporting `COMPOSE_ENV_FILE` (e.g. `COMPOSE_ENV_FILE=staging.env docker compose up -d`). Backups are protected by a built-in SHA-256 hash; optionally define `MEMEBOT_BACKUP_PASSWORD_HASH` to replace it.
|
||||||
|
|
||||||
2. **Run with Docker Compose:**
|
2. **Run with Docker Compose:**
|
||||||
```bash
|
```bash
|
||||||
@@ -25,6 +25,8 @@ The easiest way to run the bot is using Docker:
|
|||||||
docker-compose logs -f
|
docker-compose logs -f
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Deploying via Portainer? Upload your `.ENV` file under **Environment variables**—Portainer stores it beside the stack so the compose file picks it up automatically. Override `COMPOSE_ENV_FILE` only when you use a differently named file.
|
||||||
|
|
||||||
4. **Stop the bot:**
|
4. **Stop the bot:**
|
||||||
```bash
|
```bash
|
||||||
docker-compose down
|
docker-compose down
|
||||||
@@ -51,6 +53,9 @@ export TELEGRAM_BOT_TOKEN=123:ABC
|
|||||||
export OWNER_ID=123456789
|
export OWNER_ID=123456789
|
||||||
export CHANNEL_ID=@yourchannel # or -1001234567890
|
export CHANNEL_ID=@yourchannel # or -1001234567890
|
||||||
export DATABASE_URL=postgresql://meme:meme@localhost:5432/meme_wrangler
|
export DATABASE_URL=postgresql://meme:meme@localhost:5432/meme_wrangler
|
||||||
|
# Optional pieces that mirror the Docker variables
|
||||||
|
# export POSTGRES_HOST=localhost
|
||||||
|
# export POSTGRES_PORT=5432
|
||||||
# Optional: where JSON backups are written
|
# Optional: where JSON backups are written
|
||||||
# export MEMEBOT_BACKUP_DIR=/path/to/backups
|
# export MEMEBOT_BACKUP_DIR=/path/to/backups
|
||||||
# export MEMEBOT_BACKUP_PASSWORD_HASH=<sha256 hash of your backup secret>
|
# export MEMEBOT_BACKUP_PASSWORD_HASH=<sha256 hash of your backup secret>
|
||||||
|
|||||||
92
bot.py
92
bot.py
@@ -10,6 +10,7 @@ import io
|
|||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
from urllib.parse import urlparse, urlunparse
|
||||||
from datetime import datetime, time, timedelta
|
from datetime import datetime, time, timedelta
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from types import SimpleNamespace
|
from types import SimpleNamespace
|
||||||
@@ -89,9 +90,74 @@ def _ensure_ist(dt: datetime) -> datetime:
|
|||||||
return _ist_localize(dt)
|
return _ist_localize(dt)
|
||||||
return dt.astimezone(IST)
|
return dt.astimezone(IST)
|
||||||
|
|
||||||
DATABASE_URL = os.environ.get("DATABASE_URL") or os.environ.get("MEMEBOT_DB")
|
def _build_database_url() -> Optional[str]:
|
||||||
|
"""Derive the database URL from explicit env vars or component pieces."""
|
||||||
|
|
||||||
|
raw_url = os.environ.get("DATABASE_URL") or os.environ.get("MEMEBOT_DB")
|
||||||
|
if not raw_url:
|
||||||
|
user = os.environ.get("POSTGRES_USER")
|
||||||
|
password = os.environ.get("POSTGRES_PASSWORD")
|
||||||
|
db_name = os.environ.get("POSTGRES_DB")
|
||||||
|
host = os.environ.get("POSTGRES_HOST", "localhost")
|
||||||
|
port = os.environ.get("POSTGRES_PORT", "5432")
|
||||||
|
if user and password and db_name:
|
||||||
|
raw_url = (
|
||||||
|
f"postgresql://{quote(user, safe='')}:{quote(password, safe='')}"
|
||||||
|
f"@{host}:{port}/{db_name}"
|
||||||
|
)
|
||||||
|
if raw_url:
|
||||||
|
return _normalize_database_url(raw_url)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_database_url(url: str) -> str:
|
||||||
|
"""Replace localhost hosts with the configured Postgres host for container runs."""
|
||||||
|
|
||||||
|
host_override = os.environ.get("POSTGRES_HOST")
|
||||||
|
if not host_override:
|
||||||
|
return url
|
||||||
|
|
||||||
|
host_override = host_override.strip()
|
||||||
|
if not host_override or host_override in {"localhost", "127.0.0.1", "::1"}:
|
||||||
|
return url
|
||||||
|
|
||||||
|
parsed = urlparse(url)
|
||||||
|
if parsed.hostname not in {"localhost", "127.0.0.1", "::1"}:
|
||||||
|
return url
|
||||||
|
|
||||||
|
if "@" in parsed.netloc:
|
||||||
|
auth_prefix, _, _ = parsed.netloc.rpartition("@")
|
||||||
|
auth_segment = f"{auth_prefix}@"
|
||||||
|
else:
|
||||||
|
auth_segment = ""
|
||||||
|
|
||||||
|
if parsed.port is not None:
|
||||||
|
port_fragment = f":{parsed.port}"
|
||||||
|
else:
|
||||||
|
port_env = os.environ.get("POSTGRES_PORT")
|
||||||
|
port_fragment = f":{port_env}" if port_env else ""
|
||||||
|
|
||||||
|
target_host = host_override
|
||||||
|
if ":" in target_host and not target_host.startswith("["):
|
||||||
|
target_host = f"[{target_host}]"
|
||||||
|
|
||||||
|
new_netloc = f"{auth_segment}{target_host}{port_fragment}"
|
||||||
|
rebuilt = parsed._replace(netloc=new_netloc)
|
||||||
|
return urlunparse(rebuilt)
|
||||||
|
|
||||||
|
|
||||||
|
DATABASE_URL = _build_database_url()
|
||||||
BOT_TOKEN = os.environ.get("TELEGRAM_BOT_TOKEN")
|
BOT_TOKEN = os.environ.get("TELEGRAM_BOT_TOKEN")
|
||||||
OWNER_ID = int(os.environ.get("OWNER_ID", "0"))
|
|
||||||
|
# Support multiple owner IDs (comma-separated)
|
||||||
|
_owner_ids_raw = os.environ.get("OWNER_ID", "0")
|
||||||
|
try:
|
||||||
|
OWNER_IDS = set(int(oid.strip()) for oid in _owner_ids_raw.split(",") if oid.strip())
|
||||||
|
logger.info(f"Configured owner IDs: {OWNER_IDS}")
|
||||||
|
except ValueError as e:
|
||||||
|
logger.error(f"Failed to parse OWNER_ID env var '{_owner_ids_raw}': {e}")
|
||||||
|
OWNER_IDS = {0}
|
||||||
|
|
||||||
CHANNEL_ID = os.environ.get("CHANNEL_ID") # @channelusername or -100<id>
|
CHANNEL_ID = os.environ.get("CHANNEL_ID") # @channelusername or -100<id>
|
||||||
BACKUP_DIR = Path(os.environ.get("MEMEBOT_BACKUP_DIR", "backups"))
|
BACKUP_DIR = Path(os.environ.get("MEMEBOT_BACKUP_DIR", "backups"))
|
||||||
_HARDCODED_BACKUP_PASSWORD_HASH = "16c5b5ddf1b27f16ad5f801bb83595d00e666cc53085e53a4b1e67b715016251"
|
_HARDCODED_BACKUP_PASSWORD_HASH = "16c5b5ddf1b27f16ad5f801bb83595d00e666cc53085e53a4b1e67b715016251"
|
||||||
@@ -254,7 +320,7 @@ async def pop_due_memes_and_post(context: ContextTypes.DEFAULT_TYPE):
|
|||||||
posting_log.pop(0)
|
posting_log.pop(0)
|
||||||
async def scheduled(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
async def scheduled(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||||
user_id = update.effective_user.id
|
user_id = update.effective_user.id
|
||||||
if user_id != OWNER_ID:
|
if user_id not in OWNER_IDS:
|
||||||
await update.message.reply_text("Only the owner can use this command.")
|
await update.message.reply_text("Only the owner can use this command.")
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -340,7 +406,7 @@ async def scheduled(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|||||||
await update.message.reply_text(caption)
|
await update.message.reply_text(caption)
|
||||||
async def unschedule(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
async def unschedule(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||||
user_id = update.effective_user.id
|
user_id = update.effective_user.id
|
||||||
if user_id != OWNER_ID:
|
if user_id not in OWNER_IDS:
|
||||||
await update.message.reply_text("Only the owner can use this command.")
|
await update.message.reply_text("Only the owner can use this command.")
|
||||||
return
|
return
|
||||||
if not context.args or not all(arg.isdigit() for arg in context.args):
|
if not context.args or not all(arg.isdigit() for arg in context.args):
|
||||||
@@ -359,7 +425,7 @@ async def unschedule(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|||||||
async def preview(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
async def preview(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||||
"""Preview a scheduled meme by id. Tries direct send, then downloads and reuploads as a document if needed."""
|
"""Preview a scheduled meme by id. Tries direct send, then downloads and reuploads as a document if needed."""
|
||||||
user_id = update.effective_user.id
|
user_id = update.effective_user.id
|
||||||
if user_id != OWNER_ID:
|
if user_id not in OWNER_IDS:
|
||||||
await update.message.reply_text("Only the owner can use this command.")
|
await update.message.reply_text("Only the owner can use this command.")
|
||||||
return
|
return
|
||||||
if not context.args or not context.args[0].isdigit():
|
if not context.args or not context.args[0].isdigit():
|
||||||
@@ -420,7 +486,7 @@ async def preview(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|||||||
|
|
||||||
async def logcmd(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
async def logcmd(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||||
user_id = update.effective_user.id
|
user_id = update.effective_user.id
|
||||||
if user_id != OWNER_ID:
|
if user_id not in OWNER_IDS:
|
||||||
await update.message.reply_text("Only the owner can use this command.")
|
await update.message.reply_text("Only the owner can use this command.")
|
||||||
return
|
return
|
||||||
if not posting_log:
|
if not posting_log:
|
||||||
@@ -431,7 +497,7 @@ async def logcmd(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|||||||
|
|
||||||
async def backup(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
async def backup(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||||
user_id = update.effective_user.id
|
user_id = update.effective_user.id
|
||||||
if user_id != OWNER_ID:
|
if user_id not in OWNER_IDS:
|
||||||
await update.message.reply_text("Only the owner can use this command.")
|
await update.message.reply_text("Only the owner can use this command.")
|
||||||
return
|
return
|
||||||
if not _verify_backup_password(context.args):
|
if not _verify_backup_password(context.args):
|
||||||
@@ -500,7 +566,7 @@ async def create_backup(send_document_to: Optional[int] = None, bot: Optional[An
|
|||||||
|
|
||||||
async def restore(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
async def restore(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||||
user_id = update.effective_user.id
|
user_id = update.effective_user.id
|
||||||
if user_id != OWNER_ID:
|
if user_id not in OWNER_IDS:
|
||||||
await update.message.reply_text("Only the owner can use this command.")
|
await update.message.reply_text("Only the owner can use this command.")
|
||||||
return
|
return
|
||||||
if not _verify_backup_password(context.args):
|
if not _verify_backup_password(context.args):
|
||||||
@@ -627,7 +693,7 @@ async def helpcmd(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|||||||
<i>Example:</i> <code>/scheduleat ids: 5-10 2025-10-19</code>
|
<i>Example:</i> <code>/scheduleat ids: 5-10 2025-10-19</code>
|
||||||
|
|
||||||
<b>Notes:</b>
|
<b>Notes:</b>
|
||||||
• <b>Only the owner</b> (set by OWNER_ID) can use admin commands.
|
• <b>Only owners</b> (set by OWNER_ID) can use admin commands.
|
||||||
• All times are in <b>IST (Asia/Kolkata)</b>.
|
• All times are in <b>IST (Asia/Kolkata)</b>.
|
||||||
• Meme IDs are shown in <b>/scheduled</b> previews.
|
• Meme IDs are shown in <b>/scheduled</b> previews.
|
||||||
• Use <b>/preview</b> to check a meme before posting.
|
• Use <b>/preview</b> to check a meme before posting.
|
||||||
@@ -640,7 +706,7 @@ async def helpcmd(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|||||||
async def handle_media(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
async def handle_media(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||||
msg: Message = update.message
|
msg: Message = update.message
|
||||||
user_id = msg.from_user.id
|
user_id = msg.from_user.id
|
||||||
if user_id != OWNER_ID:
|
if user_id not in OWNER_IDS:
|
||||||
await msg.reply_text("Sorry, only the owner can send memes to schedule.")
|
await msg.reply_text("Sorry, only the owner can send memes to schedule.")
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -681,7 +747,7 @@ async def handle_media(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|||||||
|
|
||||||
async def postnow(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
async def postnow(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||||
user_id = update.effective_user.id
|
user_id = update.effective_user.id
|
||||||
if user_id != OWNER_ID:
|
if user_id not in OWNER_IDS:
|
||||||
await update.message.reply_text("Only the owner can use this command.")
|
await update.message.reply_text("Only the owner can use this command.")
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -725,7 +791,7 @@ import re
|
|||||||
|
|
||||||
async def scheduleat(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
async def scheduleat(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||||
user_id = update.effective_user.id
|
user_id = update.effective_user.id
|
||||||
if user_id != OWNER_ID:
|
if user_id not in OWNER_IDS:
|
||||||
await update.message.reply_text("Only the owner can use this command.")
|
await update.message.reply_text("Only the owner can use this command.")
|
||||||
return
|
return
|
||||||
if not context.args or len(context.args) < 2:
|
if not context.args or len(context.args) < 2:
|
||||||
@@ -793,7 +859,7 @@ async def scheduleat(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|||||||
def main():
|
def main():
|
||||||
if not BOT_TOKEN:
|
if not BOT_TOKEN:
|
||||||
raise SystemExit("Please set TELEGRAM_BOT_TOKEN environment variable")
|
raise SystemExit("Please set TELEGRAM_BOT_TOKEN environment variable")
|
||||||
if not OWNER_ID or OWNER_ID == 0:
|
if not OWNER_IDS or 0 in OWNER_IDS:
|
||||||
raise SystemExit("Please set OWNER_ID environment variable to your Telegram user id")
|
raise SystemExit("Please set OWNER_ID environment variable to your Telegram user id")
|
||||||
if not CHANNEL_ID:
|
if not CHANNEL_ID:
|
||||||
raise SystemExit("Please set CHANNEL_ID to target channel (username or id)")
|
raise SystemExit("Please set CHANNEL_ID to target channel (username or id)")
|
||||||
|
|||||||
22
deploy.sh
22
deploy.sh
@@ -26,16 +26,20 @@ if [ -z "$SERVER" ]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "Step 1: Checking if .env file exists..."
|
ENV_FILE=".ENV"
|
||||||
if [ ! -f ".env" ]; then
|
echo "Step 1: Checking for environment file (.ENV or .env)..."
|
||||||
echo "Error: .env file not found!"
|
if [ -f "$ENV_FILE" ]; then
|
||||||
echo "Please create .env file with your credentials:"
|
echo "✓ .ENV file found"
|
||||||
echo " cp .env.example .env"
|
elif [ -f ".env" ]; then
|
||||||
echo " nano .env"
|
ENV_FILE=".env"
|
||||||
|
echo "✓ .env file found"
|
||||||
|
else
|
||||||
|
echo "Error: No environment file found!"
|
||||||
|
echo "Please create one with your credentials:"
|
||||||
|
echo " cp .ENV.example .ENV"
|
||||||
|
echo " nano .ENV"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "✓ .env file found"
|
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
echo "Step 2: Uploading files to server..."
|
echo "Step 2: Uploading files to server..."
|
||||||
@@ -45,7 +49,7 @@ scp -i "$SSH_KEY" \
|
|||||||
docker-compose.yml \
|
docker-compose.yml \
|
||||||
bot.py \
|
bot.py \
|
||||||
requirements.txt \
|
requirements.txt \
|
||||||
.env \
|
"$ENV_FILE" \
|
||||||
"$SERVER":~/meme-wrangler/
|
"$SERVER":~/meme-wrangler/
|
||||||
|
|
||||||
echo "✓ Files uploaded"
|
echo "✓ Files uploaded"
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
|
x-env-file: &compose_env_file ${COMPOSE_ENV_FILE:-.env}
|
||||||
|
|
||||||
services:
|
services:
|
||||||
postgres:
|
postgres:
|
||||||
image: postgres:15
|
image: postgres:15
|
||||||
container_name: meme-wrangler-db
|
container_name: meme-wrangler-db
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- *compose_env_file
|
||||||
environment:
|
environment:
|
||||||
- POSTGRES_DB=${POSTGRES_DB:-meme_wrangler}
|
- POSTGRES_DB=${POSTGRES_DB:-meme_wrangler}
|
||||||
- POSTGRES_USER=${POSTGRES_USER:-meme}
|
- POSTGRES_USER=${POSTGRES_USER:-meme}
|
||||||
@@ -26,7 +28,7 @@ services:
|
|||||||
postgres:
|
postgres:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- *compose_env_file
|
||||||
environment:
|
environment:
|
||||||
- TELEGRAM_BOT_TOKEN=${TELEGRAM_BOT_TOKEN:-}
|
- TELEGRAM_BOT_TOKEN=${TELEGRAM_BOT_TOKEN:-}
|
||||||
- OWNER_ID=${OWNER_ID:-}
|
- OWNER_ID=${OWNER_ID:-}
|
||||||
@@ -34,8 +36,10 @@ services:
|
|||||||
- POSTGRES_DB=${POSTGRES_DB:-meme_wrangler}
|
- POSTGRES_DB=${POSTGRES_DB:-meme_wrangler}
|
||||||
- POSTGRES_USER=${POSTGRES_USER:-meme}
|
- POSTGRES_USER=${POSTGRES_USER:-meme}
|
||||||
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-meme}
|
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-meme}
|
||||||
- DATABASE_URL=postgresql://${POSTGRES_USER:-meme}:${POSTGRES_PASSWORD:-meme}@postgres:5432/${POSTGRES_DB:-meme_wrangler}
|
- POSTGRES_HOST=${POSTGRES_HOST:-postgres}
|
||||||
- MEMEBOT_BACKUP_DIR=/app/backups
|
- POSTGRES_PORT=${POSTGRES_PORT:-5432}
|
||||||
|
- DATABASE_URL=${DATABASE_URL:-postgresql://${POSTGRES_USER:-meme}:${POSTGRES_PASSWORD:-meme}@${POSTGRES_HOST:-postgres}:${POSTGRES_PORT:-5432}/${POSTGRES_DB:-meme_wrangler}}
|
||||||
|
- MEMEBOT_BACKUP_DIR=${MEMEBOT_BACKUP_DIR:-/app/backups}
|
||||||
volumes:
|
volumes:
|
||||||
- ./backups:/app/backups
|
- ./backups:/app/backups
|
||||||
logging:
|
logging:
|
||||||
|
|||||||
Reference in New Issue
Block a user