Files
proitlab c262b4677c fix(B27): gate warn notify on real Telegram delivery (V66a) + scan config.env bind
VOTE ULANG SEGERA never reached community channel though dashboard badge showed:
- scan service lacked NFS config.env bind -> empty TELEGRAM_* -> send_text silent no-op
- _notify_warn still record_notify'd -> fake dedup rows blocked future sends

send_text/notify_warn now return delivery bool; record_notify only on True.
scan service mounts config.env:/app/.env:ro.
2026-09-02 12:39:24 +07:00

175 lines
6.6 KiB
Python

"""Kirim notifikasi status distribusi + klaim + warn via Telegram Bot API (best-effort).
Dua kanal (V30/V36): KOMUNITAS (`TELEGRAM_COMMUNITY_CHAT_IDS`) hanya brief
mulai/selesai distribusi + VOTE ULANG SEGERA; INTERNAL (`TELEGRAM_INTERNAL_CHAT_IDS`)
detail distribusi + semua kegagalan + klaim reward (⊥ komunitas). Token tunggal
`TELEGRAM_BOT_TOKEN`. Tanpa token/daftar → kanal itu no-op senyap (⊥ ganggu
run). Gagal kirim hanya dicatat — ⊥ pernah crash distribusi/klaim.
V66: semua notifikasi kini punya emoticon prefix.
"""
import requests
from config import (TELEGRAM_BOT_TOKEN, TELEGRAM_COMMUNITY_CHAT_IDS,
TELEGRAM_INTERNAL_CHAT_IDS)
# Batas panjang pesan Telegram per kiriman (karakter)
_MAX_MSG_LEN = 4096
def send_text(text, chat_ids=None):
"""Kirim `text` ke daftar chat id. `chat_ids=None` → kanal komunitas.
Best-effort: tanpa token/daftar → no-op; gagal → log saja.
Return True bila berhasil ke SEMUA chat id (setidaknya satu terkirim),
False bila no-op (token/daftar kosong) atau ada yang gagal.
"""
if chat_ids is None:
chat_ids = TELEGRAM_COMMUNITY_CHAT_IDS
if not TELEGRAM_BOT_TOKEN or not chat_ids:
return False
url = f'https://api.telegram.org/bot{TELEGRAM_BOT_TOKEN}/sendMessage'
ok = True
for chat_id in chat_ids:
try:
resp = requests.post(url, json={'chat_id': chat_id, 'text': text},
timeout=10)
resp.raise_for_status()
except requests.RequestException as exc:
print(f'[Peringatan] Gagal kirim Telegram ke {chat_id}: {exc}',
flush=True)
ok = False
return ok
def send_internal(text):
"""Kirim ke kanal internal (detail + kegagalan + klaim)."""
send_text(text, TELEGRAM_INTERNAL_CHAT_IDS)
def notify_start(run_date, n_voters, total_share, balance):
"""Run nyata dimulai: komunitas brief + internal detail."""
send_text(f'🚀 DISTRIBUSI MULAI\n📅 Tanggal: {run_date}')
send_internal(
f'🚀 DISTRIBUSI MULAI\n'
f'📅 Tanggal: {run_date}\n'
f'👥 Pemilih: {n_voters}\n'
f'💎 Total share: {total_share} VEX\n'
f'💰 Saldo: {balance} VEX'
)
def notify_finish(status, sent, total, total_share, failed, run_date):
"""Run selesai: komunitas brief + internal detail (ok | partial)."""
if status == 'ok':
emoji = '✅'
label = 'Berhasil'
else:
emoji = '⚠️'
label = 'Sebagian'
send_text(
f'{emoji} DISTRIBUSI SELESAI\n'
f'📅 Tanggal: {run_date}\n'
f'📊 Status: {label}'
)
send_internal(
f'{emoji} DISTRIBUSI SELESAI\n'
f'📅 Tanggal: {run_date}\n'
f'📊 Status: {label}\n'
f'👥 Terkirim: {sent}/{total} pemilih\n'
f'💎 Total share: {total_share} VEX\n'
f'❌ Gagal: {len(failed)}'
)
def notify_failure(exc):
"""Run gagal total — kanal internal saja (ops detail)."""
send_internal(
f'🔴 DISTRIBUSI GAGAL\n'
f'❗ Error: {exc}'
)
def notify_claim(reward, fee, wallet):
"""Klaim reward BP sukses — kanal internal saja, ⊥ komunitas (V36)."""
send_internal(
f'💰 KLAIM REWARD SUKSES\n'
f'💎 Reward: {reward} VEX\n'
f'💸 Fee BP: {fee} VEX → {wallet}'
)
def notify_claim_unmeasured(txid_label):
"""Klaim mendarat tapi reward tak terukur (V45) — internal, ⊥ "sukses 0"."""
send_internal(
f'🔍 KLAIM MENDARAT · REWARD TAK TERUKUR\n'
f'⚠️ Periksa reward & fee BP manual (txid {txid_label})'
)
def notify_claim_failure(exc):
"""Kegagalan siklus klaim — kanal internal saja (V36)."""
send_internal(
f'🔴 KLAIM GAGAL\n'
f'❗ Error: {exc}'
)
# ——————————————————————————————————————————————————————————————————————
# V66: Notifikasi VOTE ULANG SEGERA — komunitas.
# voters = list of (owner, staked, urgency) di mana urgency 1=⚠️ 2=🔴 3=🚨.
# ——————————————————————————————————————————————————————————————————————
_URGENCY_EMOJI = {1: '⚠️', 2: '🔴', 3: '🚨'}
def notify_warn(voters):
"""VOTE ULANG SEGERA — komunitas, best-effort.
Kirim daftar akun yang vote-nya akan kedaluwarsa, diurutkan berdasarkan
urgensi (paling mendesak di atas). Maks 50 entri; sisanya arahkan ke
dashboard. Return True bila SEMUA pesan terkirim, False bila no-op/gagal.
"""
if not voters:
return True
# Urutkan: urgensi tinggi dulu (3 > 2 > 1)
sorted_voters = sorted(voters, key=lambda v: v[2], reverse=True)
truncated = len(sorted_voters) > 50
display = sorted_voters[:50]
# Emoji dari baris paling mendesak
top_emoji = _URGENCY_EMOJI.get(display[0][2], '⚠️')
lines = [f'{top_emoji} VOTE ULANG SEGERA', '',
f'{len(sorted_voters)} akun akan kedaluwarsa:', '']
for owner, staked, urgency in display:
emoji = _URGENCY_EMOJI.get(urgency, '⚠️')
lines.append(f'• {emoji} {owner} — {staked} VEX')
lines.append(f' ↳ https://mining.databisnis.id?q={owner}')
if truncated:
lines.append('')
lines.append('📋 Cek https://mining.databisnis.id untuk daftar lengkap')
lines.append('')
lines.append('Vote ulang sekarang agar tidak kehilangan hak bagian!')
text = '\n'.join(lines)
sent = True
# Telegram batas 4096 karakter — split bila perlu
if len(text) <= _MAX_MSG_LEN:
sent = send_text(text)
else:
# Kirim header dulu, lalu sisa per-chunk
header = f'{top_emoji} VOTE ULANG SEGERA\n\n{len(sorted_voters)} akun akan kedaluwarsa:\n'
sent = send_text(header) and sent
chunk = []
chunk_len = 0
for owner, staked, urgency in display:
emoji = _URGENCY_EMOJI.get(urgency, '⚠️')
line = f'• {emoji} {owner} — {staked} VEX\n ↳ https://mining.databisnis.id?q={owner}\n'
if chunk_len + len(line) > _MAX_MSG_LEN - 100:
sent = send_text('\n'.join(chunk)) and sent
chunk = []
chunk_len = 0
chunk.append(line.rstrip())
chunk_len += len(line)
if chunk:
sent = send_text('\n'.join(chunk)) and sent
sent = send_text('Vote ulang sekarang agar tidak kehilangan hak bagian!') and sent
return sent