Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Copy this file to .env and fill in your own secrets.
TELEGRAM_BOT_TOKEN=your-telegram-bot-token
BOT_TOKEN=your-telegram-bot-token
# Legacy fallback (optional):
# TELEGRAM_BOT_TOKEN=your-telegram-bot-token
# optional when using subscribers.json. Keep it for fallback/sanity tests.
TELEGRAM_CHAT_ID=your-target-chat-id
CRYPTOCOMPARE_API_KEY=optional-cryptocompare-api-key
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/subscriber-listener.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,12 @@ jobs:
with:
python-version: '3.12'

- run: pip install requests
- run: pip install -r requirements.txt

- name: Run listener to capture /start
env:
TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }}
run: python listen_updates.py
BOT_TOKEN: ${{ secrets.BOT_TOKEN }}
run: python -m src.signal_bot.ci_entry --mode prehandle

- name: Commit & push if changed
run: |
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/xrpbot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ jobs:

- name: Run bot
env:
TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }}
BOT_TOKEN: ${{ secrets.BOT_TOKEN }}
TELEGRAM_CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }}
CRYPTOCOMPARE_API_KEY: ${{ secrets.CRYPTOCOMPARE_API_KEY }}
run: python trigger_xrp_bot.py
run: python -m src.signal_bot.ci_entry --mode summary
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ cp .env.example .env
Fill in `.env` with your secrets:

```
TELEGRAM_BOT_TOKEN=bot-token-from-botfather
BOT_TOKEN=bot-token-from-botfather
# TELEGRAM_BOT_TOKEN=bot-token-from-botfather # optional legacy fallback
TELEGRAM_CHAT_ID=chat-or-channel-id (optional fallback)
CRYPTOCOMPARE_API_KEY=cryptocompare-api-key (optional)
# SUBSCRIBERS_DB_PATH=/absolute/path/to/subscribers.sqlite3 (optional override)
Expand Down
83 changes: 63 additions & 20 deletions listen_start.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,36 @@ class TimedOut(Exception):
load_dotenv(candidate_path)
break

TOKEN = os.getenv("TELEGRAM_BOT_TOKEN") or os.getenv("BOT_TOKEN")
if not TOKEN:
raise RuntimeError("TELEGRAM_BOT_TOKEN is required to read Telegram updates")
API = f"https://api.telegram.org/bot{TOKEN}"
_BOT_TOKEN_CACHE: str | None = None
_API_BASE: str | None = None


def _require_bot_token(*, force_refresh: bool = False) -> str:
global _BOT_TOKEN_CACHE
if force_refresh:
_BOT_TOKEN_CACHE = None
if _BOT_TOKEN_CACHE:
return _BOT_TOKEN_CACHE
token = os.getenv("BOT_TOKEN") or os.getenv("TELEGRAM_BOT_TOKEN")
if not token:
raise RuntimeError("BOT_TOKEN is required to read Telegram updates")
_BOT_TOKEN_CACHE = token
return token


def _get_api_base(*, force_refresh: bool = False) -> str:
global _API_BASE
if force_refresh:
_API_BASE = None
if _API_BASE:
return _API_BASE
_API_BASE = f"https://api.telegram.org/bot{_require_bot_token()}"
return _API_BASE


def _api_url(method: str) -> str:
return f"{_get_api_base()}/{method}"


BROADCAST_SLEEP_MS = int(os.getenv("BROADCAST_SLEEP_MS", "0"))

Expand Down Expand Up @@ -81,10 +107,27 @@ def get_updates(self, offset: int, timeout: int, allowed_updates=None):
return [_RequestsUpdate(item) for item in payload]


if TelegramBot is not None:
BOT = TelegramBot(TOKEN)
else: # pragma: no cover - fallback when dependency missing
BOT = _RequestsBot(TOKEN)
class _DeferredBot:
def __init__(self):
self._bot = None

def _ensure(self):
if self._bot is None:
token = _require_bot_token()
if TelegramBot is not None:
self._bot = TelegramBot(token)
else: # pragma: no cover - fallback when dependency missing
self._bot = _RequestsBot(token)
return self._bot

def get_updates(self, *args, **kwargs):
return self._ensure().get_updates(*args, **kwargs)

def reset(self): # pragma: no cover - convenience for tests
self._bot = None


BOT = _DeferredBot()

ROOT = Path(__file__).resolve().parent
SUBS_FILE = Path(os.getenv("SUBSCRIBERS_DB_PATH") or os.getenv("SUBSCRIBERS_PATH", str(ROOT / "subscribers.sqlite3"))
Expand Down Expand Up @@ -386,7 +429,7 @@ def _send_stars_invoice(state: dict, chat_id: int | str, amount: int) -> bool:
}

try:
response = requests.post(f"{API}/sendInvoice", json=invoice, timeout=20)
response = requests.post(_api_url("sendInvoice"), json=invoice, timeout=20)
response.raise_for_status()
except requests.HTTPError as exc:
_handle_invoice_error(chat_id, exc)
Expand Down Expand Up @@ -429,7 +472,7 @@ def handle_donate_stars_start(state: dict, chat_id: int | str) -> bool:
}

try:
response = requests.post(f"{API}/sendMessage", json=payload, timeout=20)
response = requests.post(_api_url("sendMessage"), json=payload, timeout=20)
response.raise_for_status()
except Exception as exc: # pragma: no cover - network failure
print(f"Failed to send donation options to {chat_id}: {exc}")
Expand Down Expand Up @@ -479,7 +522,7 @@ def handle_donate_custom_text(state: dict, chat_id: int | str, raw_text: str) ->
def handle_pre_checkout_query(query: dict) -> None:
payload = {"pre_checkout_query_id": query.get("id"), "ok": True}
try:
response = requests.post(f"{API}/answerPreCheckoutQuery", json=payload, timeout=20)
response = requests.post(_api_url("answerPreCheckoutQuery"), json=payload, timeout=20)
response.raise_for_status()
except Exception as exc: # pragma: no cover - best effort acknowledgement
print(f"Failed to answer pre-checkout query {query.get('id')}: {exc}")
Expand Down Expand Up @@ -574,7 +617,7 @@ def handle_refund_request(chat_id: int | str, raw_text: str) -> None:
charge_id = parts[1].strip()
payload = {"user_id": chat_id, "telegram_payment_charge_id": charge_id}
try:
response = requests.post(f"{API}/refundStarPayment", json=payload, timeout=20)
response = requests.post(_api_url("refundStarPayment"), json=payload, timeout=20)
response.raise_for_status()
except Exception as exc: # pragma: no cover - network failure
print(f"Refund request failed for {chat_id}: {exc}")
Expand Down Expand Up @@ -610,7 +653,7 @@ def send_start_prompt(chat_id: int | str, already_registered: bool = False) -> N
"one_time_keyboard": True,
}
payload = {"chat_id": chat_id, "text": text, "reply_markup": keyboard}
resp = requests.post(f"{API}/sendMessage", json=payload, timeout=20)
resp = requests.post(_api_url("sendMessage"), json=payload, timeout=20)
resp.raise_for_status()


Expand All @@ -624,7 +667,7 @@ def send_contact_confirmation(chat_id: int | str) -> None:
"text": text,
"reply_markup": {"remove_keyboard": True},
}
resp = requests.post(f"{API}/sendMessage", json=payload, timeout=20)
resp = requests.post(_api_url("sendMessage"), json=payload, timeout=20)
resp.raise_for_status()


Expand All @@ -649,7 +692,7 @@ def send_menu(chat_id: int | str, *, prepend_text: str | None = None) -> None:
"text": "\n".join(line for line in lines if line is not None),
"reply_markup": MENU_MARKUP,
}
resp = requests.post(f"{API}/sendMessage", json=payload, timeout=20)
resp = requests.post(_api_url("sendMessage"), json=payload, timeout=20)
resp.raise_for_status()


Expand All @@ -658,15 +701,15 @@ def send_unsubscribe_confirmation(chat_id: int | str) -> None:
"اشتراک شما غیرفعال شد. برای فعال‌سازی دوباره، هر زمان /start را بفرستید و شماره خود را ارسال کنید."
)
payload = {"chat_id": chat_id, "text": text, "reply_markup": {"remove_keyboard": True}}
resp = requests.post(f"{API}/sendMessage", json=payload, timeout=20)
resp = requests.post(_api_url("sendMessage"), json=payload, timeout=20)
resp.raise_for_status()


def answer_callback(callback_id: str, *, text: str | None = None) -> None:
payload = {"callback_query_id": callback_id}
if text:
payload["text"] = text
resp = requests.post(f"{API}/answerCallbackQuery", json=payload, timeout=20)
resp = requests.post(_api_url("answerCallbackQuery"), json=payload, timeout=20)
resp.raise_for_status()


Expand Down Expand Up @@ -733,7 +776,7 @@ def _send_ambiguous_options(chat_id: int | str, candidates: list[ResolutionCandi
"text": "چند گزینه مشابه یافت شد، لطفاً انتخاب کنید:",
"reply_markup": keyboard,
}
resp = requests.post(f"{API}/sendMessage", json=payload, timeout=20)
resp = requests.post(_api_url("sendMessage"), json=payload, timeout=20)
resp.raise_for_status()


Expand Down Expand Up @@ -803,7 +846,7 @@ def _send_remove_page(chat_id: int | str, pairs: list[str], page: int) -> None:
+ (f"\nصفحه {page_index + 1} از {total_pages}" if total_pages > 1 else ""),
"reply_markup": {"inline_keyboard": keyboard},
}
resp = requests.post(f"{API}/sendMessage", json=payload, timeout=20)
resp = requests.post(_api_url("sendMessage"), json=payload, timeout=20)
resp.raise_for_status()


Expand Down Expand Up @@ -861,7 +904,7 @@ def _send_remove_confirm(chat_id: int | str, pair: str) -> None:
"text": f"{pair}\nحذف شود؟ / Remove?",
"reply_markup": keyboard,
}
resp = requests.post(f"{API}/sendMessage", json=payload, timeout=20)
resp = requests.post(_api_url("sendMessage"), json=payload, timeout=20)
resp.raise_for_status()


Expand Down
4 changes: 4 additions & 0 deletions pytest.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
[pytest]
norecursedirs = lib bin
python_files = test_*.py
pythonpath = .
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
requests
pandas
numpy
python-dotenv
python-dotenv>=1.0.1
tzdata
APScheduler
SQLAlchemy>=1.4
Expand Down
4 changes: 2 additions & 2 deletions send_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@
if candidate_path.exists():
load_dotenv(candidate_path)
break
TOKEN = os.getenv("TELEGRAM_BOT_TOKEN")
TOKEN = os.getenv("BOT_TOKEN") or os.getenv("TELEGRAM_BOT_TOKEN")
CHAT_ID = os.getenv("TELEGRAM_CHAT_ID")

if not TOKEN or not CHAT_ID:
raise RuntimeError("Both TELEGRAM_BOT_TOKEN and TELEGRAM_CHAT_ID must be set for send_test.py")
raise RuntimeError("Both BOT_TOKEN and TELEGRAM_CHAT_ID must be set for send_test.py")

msg = "سلام هومن! ✅ تست ارسال پیام از ربات انجام شد."
url = f"https://api.telegram.org/bot{TOKEN}/sendMessage"
Expand Down
15 changes: 15 additions & 0 deletions sitecustomize.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from __future__ import annotations

import sys
from pathlib import Path

_REPO_ROOT = Path(__file__).resolve().parent
_LEGACY_SITE = _REPO_ROOT / "lib" / "python3.13" / "site-packages"

if _LEGACY_SITE.exists():
legacy_resolved = _LEGACY_SITE.resolve()
sys.path = [p for p in sys.path if Path(p).resolve() != legacy_resolved]

repo_str = str(_REPO_ROOT)
if repo_str not in sys.path:
sys.path.insert(0, repo_str)
1 change: 1 addition & 0 deletions tests/test_ci_entry.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import unittest
from unittest import mock

os.environ.setdefault("BOT_TOKEN", "test-token")
os.environ.setdefault("TELEGRAM_BOT_TOKEN", "test-token")

from trigger_xrp_bot import generate_snapshot_payload # noqa: E402
Expand Down
1 change: 1 addition & 0 deletions tests/test_listen_start.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from pathlib import Path
from unittest import mock

os.environ.setdefault("BOT_TOKEN", "test-token")
os.environ.setdefault("TELEGRAM_BOT_TOKEN", "test-token")

sys.path.append(str(Path(__file__).resolve().parents[1]))
Expand Down
1 change: 1 addition & 0 deletions tests/test_schedule_jobs.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import os
os.environ.setdefault("BOT_TOKEN", "test-token")
os.environ.setdefault("TELEGRAM_BOT_TOKEN", "test-token")
os.environ.setdefault("TELEGRAM_CHAT_ID", "111")
import unittest
Expand Down
1 change: 1 addition & 0 deletions tests/test_trigger_xrp_bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from pathlib import Path
from unittest import mock

os.environ.setdefault("BOT_TOKEN", "test-token")
os.environ.setdefault("TELEGRAM_BOT_TOKEN", "test-token")
os.environ.setdefault("TELEGRAM_CHAT_ID", "111")

Expand Down
7 changes: 5 additions & 2 deletions trigger_xrp_bot.backup.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,12 @@

# ====== تنظیمات از .env ======
load_dotenv(os.path.expanduser("~/xrpbot/.env"))
TELEGRAM_BOT_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN")
BOT_TOKEN = os.getenv("BOT_TOKEN") or os.getenv("TELEGRAM_BOT_TOKEN")
TELEGRAM_CHAT_ID = os.getenv("TELEGRAM_CHAT_ID")

if not BOT_TOKEN:
raise RuntimeError("BOT_TOKEN is required (set in .env or environment)")

SEND_ONLY_ON_TRIGGER = False # اگر True فقط موقع سیگنال "خرید" پیام می‌دهد

SYMBOL = "XRPUSDT"
Expand Down Expand Up @@ -71,7 +74,7 @@ def bullish_divergence(price, rsi_series):
def tehran_now(): return datetime.now(ZoneInfo("Asia/Tehran")).strftime("%Y-%m-%d %H:%M:%S")

def send_telegram(text):
url = f"https://api.telegram.org/bot{TELEGRAM_BOT_TOKEN}/sendMessage"
url = f"https://api.telegram.org/bot{BOT_TOKEN}/sendMessage"
payload = {"chat_id": TELEGRAM_CHAT_ID, "text": text, "parse_mode": "HTML", "disable_web_page_preview": True}
requests.post(url, json=payload, timeout=20)

Expand Down
20 changes: 16 additions & 4 deletions trigger_xrp_bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,14 +50,25 @@
if _candidate_path.exists():
load_dotenv(_candidate_path)
break
TELEGRAM_BOT_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN") or os.getenv("BOT_TOKEN")
TELEGRAM_CHAT_ID = os.getenv("TELEGRAM_CHAT_ID")
CRYPTOCOMPARE_API_KEY = os.getenv("CRYPTOCOMPARE_API_KEY")
BROADCAST_SLEEP_MS = int(os.getenv("BROADCAST_SLEEP_MS", "0"))
_BACKOFF_SCHEDULE = (0.5, 1.0, 2.0)

if not TELEGRAM_BOT_TOKEN:
raise RuntimeError("TELEGRAM_BOT_TOKEN is required (set in .env or environment)")
_BOT_TOKEN_CACHE: str | None = None


def _require_bot_token(*, force_refresh: bool = False) -> str:
global _BOT_TOKEN_CACHE
if force_refresh:
_BOT_TOKEN_CACHE = None
if _BOT_TOKEN_CACHE:
return _BOT_TOKEN_CACHE
token = os.getenv("BOT_TOKEN") or os.getenv("TELEGRAM_BOT_TOKEN")
if not token:
raise RuntimeError("BOT_TOKEN is required (set in .env or environment)")
_BOT_TOKEN_CACHE = token
return token


def resolve_chat_ids(subscribers: list[dict[str, str]], fallback_chat_id: str | None) -> list[str]:
Expand Down Expand Up @@ -281,7 +292,8 @@ def tehran_now():
return datetime.now(tz).strftime("%Y-%m-%d %H:%M:%S")

def send_telegram(chat_id: str, text: str):
url = f"https://api.telegram.org/bot{TELEGRAM_BOT_TOKEN}/sendMessage"
token = _require_bot_token()
url = f"https://api.telegram.org/bot{token}/sendMessage"
payload = {"chat_id": chat_id, "text": text, "parse_mode": "HTML", "disable_web_page_preview": True}

for attempt, backoff in enumerate(_BACKOFF_SCHEDULE, start=1):
Expand Down