import asyncio from telethon import TelegramClient, events, types from telethon.tl.types import InputPeerUser, InputPeerChannel, InputPeerChat from telethon.errors import SessionPasswordNeededError, FloodWaitError import logging import json import os import re import requests import random import subprocess import sys import time from urllib.parse import urlparse, parse_qs, urlencode logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') logger = logging.getLogger(__name__) # ─── IPC queue files shared with whatsapp/forwarder.js ─────────────────────── _WA_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'whatsapp') WA_TO_TG_QUEUE = os.path.join(_WA_DIR, 'wa_to_tg_queue.json') TG_TO_WA_QUEUE = os.path.join(_WA_DIR, 'tg_to_wa_queue.json') def _drain_wa_to_tg_queue(): """Read and clear all pending WA→TG messages. Returns list of entries.""" try: if not os.path.exists(WA_TO_TG_QUEUE): return [] with open(WA_TO_TG_QUEUE, 'r', encoding='utf-8') as f: data = json.load(f) if not isinstance(data, list) or not data: return [] # Clear immediately after reading with open(WA_TO_TG_QUEUE, 'w', encoding='utf-8') as f: json.dump([], f) return data except Exception: return [] def _push_tg_to_wa_queue(text): """Append a Telegram bot reply to the TG→WA queue for forwarder.js.""" try: os.makedirs(_WA_DIR, exist_ok=True) queue = [] if os.path.exists(TG_TO_WA_QUEUE): with open(TG_TO_WA_QUEUE, 'r', encoding='utf-8') as f: queue = json.load(f) queue.append({'text': text, 'timestamp': int(time.time())}) with open(TG_TO_WA_QUEUE, 'w', encoding='utf-8') as f: json.dump(queue, f, indent=2, ensure_ascii=False) except Exception as e: logger.error(f"Failed to write tg_to_wa_queue: {e}") class TelegramForwarder: def __init__(self, api_id, api_hash, phone_number): self.api_id = api_id self.api_hash = api_hash self.phone_number = phone_number self.client = TelegramClient(f'session_{phone_number}', api_id, api_hash) # Amazon affiliate tags to rotate between self.affiliate_tags = ['autoforwarder-21', 'autoforwarder-21'] async def connect(self): await self.client.start(phone=self.phone_number) async def list_chats(self): await self.connect() dialogs = await self.client.get_dialogs() chats = [] for dialog in dialogs: chat_info = { "id": dialog.id, "title": dialog.title, "type": "Channel" if dialog.is_channel else "Group" if dialog.is_group else "User" } chats.append(chat_info) logger.info(f"Chat ID: {dialog.id}, Title: {dialog.title}, Type: {chat_info['type']}") with open(f"chats_of_{self.phone_number}.json", "w") as f: json.dump(chats, f, indent=4) logger.info(f"Chat list saved to chats_of_{self.phone_number}.json") def process_amazon_link(self, original_url, message_text): try: response = requests.get(original_url, allow_redirects=True) final_url = response.url parsed_url = urlparse(final_url) if 'amazon' in parsed_url.netloc: query_params = parse_qs(parsed_url.query) # Randomly select one of the affiliate tags selected_tag = random.choice(self.affiliate_tags) query_params['tag'] = [selected_tag] new_query = urlencode(query_params, doseq=True) affiliate_url = parsed_url._replace(query=new_query).geturl() message_text = message_text.replace(original_url, f'Buy Now') logger.info(f"Applied affiliate tag: {selected_tag} to Amazon link") return message_text return message_text except Exception as e: logger.error(f"Error processing Amazon link: {str(e)}") return message_text async def forward_messages(self, source_chat_ids, destination_chat_id, channel_link, use_bot=True, keywords=None): await self.connect() try: destination_entity = await self.client.get_entity(channel_link) original_destination_entity = None if use_bot and destination_chat_id: original_destination_entity = await self.client.get_entity(destination_chat_id) logger.info(f"Successfully resolved destination entities") except Exception as e: logger.error(f"An error occurred while resolving destination chats: {str(e)}") return # ── Background task: poll wa_to_tg_queue and send to Telegram bot ────── async def poll_wa_to_tg_queue(): logger.info("WA→TG queue polling task started") iteration = 0 while True: await asyncio.sleep(5) iteration += 1 try: # Confirm the file path being checked if iteration % 12 == 1: # log path every ~1 min logger.info(f"WA→TG queue file: {WA_TO_TG_QUEUE} | exists: {os.path.exists(WA_TO_TG_QUEUE)}") entries = _drain_wa_to_tg_queue() if not entries: continue logger.info(f"WA→TG queue: found {len(entries)} item(s) to send") for entry in entries: text = entry.get('text', '') if not text: logger.warning("WA→TG queue: entry has empty text, skipping") continue logger.info(f"WA→TG: Attempting to send: {text[:80]!r}") if use_bot and original_destination_entity: await self.client.send_message(original_destination_entity, text) logger.info(f"WA→TG: ✅ Sent to Telegram bot ({destination_chat_id})") else: await self.client.send_message(destination_entity, text) logger.info(f"WA→TG: ✅ Sent to Telegram channel") except Exception as e: logger.error(f"WA→TG queue error: {e}", exc_info=True) # Use Telethon's own loop so the task is scheduled correctly self.client.loop.create_task(poll_wa_to_tg_queue()) # ── Inject a test entry on first startup to verify the pipeline works ── _test_flag = os.path.join(_WA_DIR, '.queue_tested') if not os.path.exists(_test_flag): try: import time as _time _q = [] if os.path.exists(WA_TO_TG_QUEUE): with open(WA_TO_TG_QUEUE, 'r', encoding='utf-8') as _f: _q = json.load(_f) _q.append({'text': '✅ [WA→TG pipeline test] If you see this in the bot, the queue is working!', 'timestamp': int(_time.time())}) with open(WA_TO_TG_QUEUE, 'w', encoding='utf-8') as _f: json.dump(_q, _f, indent=2, ensure_ascii=False) # Create flag so we don't inject on every restart open(_test_flag, 'w').close() logger.info("WA→TG pipeline test message injected into queue") except Exception as _e: logger.warning(f"Could not inject test message: {_e}") # ── Listen for replies from the Telegram bot ────────────────────────── if use_bot and original_destination_entity: @self.client.on(events.NewMessage(chats=[original_destination_entity])) async def bot_reply_handler(event): try: reply_text = event.message.text if event.message.text else "" if not reply_text: return urls = re.findall(r'(https?://\S+)', reply_text) if urls: logger.info(f"Bot replied with link(s) — queuing for WhatsApp Genuine Deals group") _push_tg_to_wa_queue(reply_text) except Exception as e: logger.error(f"Error in bot reply handler: {e}") @self.client.on(events.NewMessage(chats=source_chat_ids)) async def handler(event): try: message = event.message message_text = message.text if message.text else "" if message_text: message_text = re.sub(r'https://t\.me/\S+', '', message_text) urls = re.findall(r'(https?://\S+)', message_text) amazon_urls = [url for url in urls if 'amzn.to' in url or 'amazon' in url] other_urls = [url for url in urls if 'amzn.to' not in url and 'amazon' not in url] if amazon_urls: for url in amazon_urls: message_text = self.process_amazon_link(url, message_text) # Send media with caption if exists, otherwise just send text if message.media: await self.client.send_file(destination_entity, message.media, caption=message_text, parse_mode='html', link_preview=False) else: await self.client.send_message(destination_entity, message_text, parse_mode='html', link_preview=False) logger.info(f"Amazon deal forwarded to channel") elif other_urls: # Has non-Amazon links → send to Telegram bot for processing if use_bot and original_destination_entity: if message.media: await message.forward_to(original_destination_entity) else: await self.client.send_message(original_destination_entity, message_text) logger.info(f"Non-Amazon link message forwarded to bot: {other_urls}") else: # No bot configured — send direct to channel if message.media: await self.client.send_file(destination_entity, message.media) if message_text: await self.client.send_message(destination_entity, message_text) else: # No links at all — ignore (do not forward) pass except FloodWaitError as e: logger.warning(f"FloodWaitError: Waiting for {e.seconds} seconds") await asyncio.sleep(e.seconds) except Exception as e: logger.error(f"Error forwarding message: {str(e)}") logger.info(f"Started forwarding messages from {len(source_chat_ids)} sources") await self.client.run_until_disconnected() def read_credentials(): try: with open("credentials.json", "r") as file: creds = json.load(file) return creds["api_id"], creds["api_hash"], creds["phone_number"] except FileNotFoundError: logger.error("Credentials file not found.") return None, None, None def write_credentials(api_id, api_hash, phone_number): creds = { "api_id": api_id, "api_hash": api_hash, "phone_number": phone_number } with open("credentials.json", "w") as file: json.dump(creds, file, indent=4) def read_source_list(): try: with open("SourceList", "r") as file: return [int(line.strip()) for line in file if line.strip()] except FileNotFoundError: logger.info("SourceList file not found. Creating a new one.") return [] def write_source_list(source_ids): with open("SourceList", "w") as file: for source_id in source_ids: file.write(f"{source_id}\n") def read_destination_config(): try: with open("destination_config.json", "r") as file: config = json.load(file) return config.get("destination_chat_id"), config.get("channel_link"), config.get("Amazon_Affiliate_ID") except FileNotFoundError: logger.info("Destination config file not found.") return None, None, None def write_destination_config(destination_chat_id, channel_link, amazon_affiliate_id=""): config = { "destination_chat_id": destination_chat_id, "channel_link": channel_link, "Amazon_Affiliate_ID": amazon_affiliate_id } with open("destination_config.json", "w") as file: json.dump(config, file, indent=4) def is_valid_url(url): """Validate if the provided string is a valid URL""" try: result = urlparse(url) return all([result.scheme, result.netloc]) and result.scheme in ['http', 'https'] except: return False def get_valid_channel_link(): """Prompt user for a valid channel link, with retry logic""" while True: channel_link = input("Enter Your Channel Link (Mandatory): ").strip() if not channel_link: print("Channel link cannot be empty.") continue_input = input("Do you want to continue? (y/n): ").lower() if continue_input != 'y': print("Exiting script...") exit() continue if is_valid_url(channel_link): return channel_link else: print("Provide a valid URL.") continue_input = input("Do you want to continue? (y/n): ").lower() if continue_input != 'y': print("Exiting script...") exit() def _check_node_and_install(whatsapp_dir): """Check Node.js availability and install npm packages if needed. Returns False to skip.""" try: node_version = subprocess.check_output( ['node', '--version'], stderr=subprocess.STDOUT, shell=True ).decode().strip() logger.info(f"Node.js found: {node_version}") except (subprocess.CalledProcessError, FileNotFoundError): print("\n❌ Node.js is not installed or not in PATH.") print(" Download from https://nodejs.org (v18 or higher recommended)") print(" Skipping WhatsApp and continuing with Telegram...\n") return False node_modules = os.path.join(whatsapp_dir, 'node_modules') if not os.path.exists(node_modules): print("\n📦 Installing WhatsApp dependencies (first time only)...") result = subprocess.run(['npm', 'install'], cwd=whatsapp_dir, shell=True) if result.returncode != 0: print("❌ npm install failed. Skipping WhatsApp step...\n") return False print("✅ Dependencies installed.\n") return True def run_whatsapp_setup(): """ Runs the interactive WhatsApp setup (blocking): - QR login (or reuse saved session) - Lists all groups + JIDs - Asks user to pick source group JIDs → saves to whatsapp/WASourceList - Asks for Amazon Affiliate ID → saves to whatsapp/wa_config.json """ whatsapp_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'whatsapp') setup_script = os.path.join(whatsapp_dir, 'setup.js') if not _check_node_and_install(whatsapp_dir): return print("\n" + "="*52) print(" WHATSAPP SETUP") print("="*52 + "\n") # Blocking call — user interacts with setup.js in terminal subprocess.run(['node', setup_script], cwd=whatsapp_dir, shell=True) print("\n" + "="*52) print(" WhatsApp setup done. Starting forwarder & Telegram...") print("="*52 + "\n") def start_whatsapp_forwarder(): """ Launches whatsapp/forwarder.js as a background process. It runs alongside the Telegram forwarder indefinitely. Returns the Popen handle (or None if skipped). """ whatsapp_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'whatsapp') forwarder_script = os.path.join(whatsapp_dir, 'forwarder.js') source_list = os.path.join(whatsapp_dir, 'WASourceList') # Only start if WASourceList exists and has entries if not os.path.exists(source_list): print("⚠️ WASourceList not found. WhatsApp forwarder will not start.\n") return None with open(source_list) as f: entries = [l.strip() for l in f if l.strip()] if not entries: print("⚠️ WASourceList is empty. WhatsApp forwarder will not start.\n") return None print("🚀 Starting WhatsApp forwarder in background...\n") log_file = open(os.path.join(whatsapp_dir, 'forwarder.log'), 'a') proc = subprocess.Popen( ['node', forwarder_script], cwd=whatsapp_dir, shell=True, stdout=log_file, stderr=log_file ) print(f" (WhatsApp forwarder output is logged to whatsapp/forwarder.log)\n") return proc async def main(): # ── Step 1: WhatsApp interactive setup (blocking) ─────────────────────────── run_whatsapp_setup() # ── Step 2: Start WhatsApp forwarder in background ────────────────────────── wa_proc = start_whatsapp_forwarder() # ── Step 3: Telegram forwarding ───────────────────────────────────────────── try: api_id, api_hash, phone_number = read_credentials() if api_id is None or api_hash is None or phone_number is None: api_id = input("Enter your API ID: ") api_hash = input("Enter your API Hash: ") phone_number = input("Enter your phone number: ") write_credentials(api_id, api_hash, phone_number) forwarder = TelegramForwarder(api_id, api_hash, phone_number) # Automatically start Forward Messages functionality source_ids = read_source_list() print("Current source list:", source_ids) # Warning if already 2 or more source IDs if len(source_ids) >= 2: print("\n⚠️ WARNING: Forwarding too many messages may lead to your group being banned for spamming.") continue_adding = input("Do you want to add more groups? (y/n): ").lower() if continue_adding != 'y': print("Proceeding with current source list.") else: print("Enter source chat IDs (press Enter on a blank line to finish):") while True: source_id = input().strip() if not source_id: break try: source_id = int(source_id) if source_id not in source_ids: source_ids.append(source_id) else: print(f"ID {source_id} is already in the list. Skipping.") except ValueError: print("Invalid input. Please enter a valid integer ID.") else: print("Enter source chat IDs (press Enter on a blank line to finish):") while True: source_id = input().strip() if not source_id: break try: source_id = int(source_id) if source_id not in source_ids: source_ids.append(source_id) # Check if we reached 2 sources and warn if len(source_ids) >= 2: print("\n⚠️ WARNING: Forwarding too many messages may lead to your group being banned for spamming.") continue_adding = input("Do you want to add more groups? (y/n): ").lower() if continue_adding != 'y': break else: print(f"ID {source_id} is already in the list. Skipping.") except ValueError: print("Invalid input. Please enter a valid integer ID.") write_source_list(source_ids) print("Updated source list:", source_ids) # Read existing destination config saved_destination_chat_id, saved_channel_link, saved_amazon_affiliate_id = read_destination_config() use_bot = True # Check if channel_link is empty or invalid need_new_channel_link = not saved_channel_link or saved_channel_link == "" or not is_valid_url(saved_channel_link) if saved_destination_chat_id and saved_channel_link and not need_new_channel_link: print(f"\nSaved destination chat ID: {saved_destination_chat_id}") print(f"Saved channel/group link: {saved_channel_link}") use_saved = input("Use saved destination? (y/n): ").lower() if use_saved == 'y': destination_chat_id = saved_destination_chat_id channel_link = saved_channel_link amazon_affiliate_id = saved_amazon_affiliate_id # Check if bot ID is configured if not destination_chat_id or destination_chat_id == "": use_bot = False else: use_bot = True else: # Ask for bot ID has_bot = input("Do you have an Enkaro bot ID? (y/n): ").lower() if has_bot == 'y': destination_chat_id = input("Enter the destination chat ID or bot username: ") if not destination_chat_id.startswith('@'): try: destination_chat_id = int(destination_chat_id) except ValueError: print("Invalid input. Please enter a valid integer ID or bot username starting with @.") return use_bot = True else: destination_chat_id = "" use_bot = False logger.info("Bot forwarding disabled") # Get valid channel link with validation channel_link = get_valid_channel_link() amazon_affiliate_id = saved_amazon_affiliate_id else: # Ask for bot ID has_bot = input("Do you have an Enkaro bot ID? (y/n): ").lower() if has_bot == 'y': destination_chat_id = input("Enter the destination chat ID or bot username: ") if not destination_chat_id.startswith('@'): try: destination_chat_id = int(destination_chat_id) except ValueError: print("Invalid input. Please enter a valid integer ID or bot username starting with @.") return use_bot = True else: destination_chat_id = "" use_bot = False logger.info("Bot forwarding disabled") # Get valid channel link with validation channel_link = get_valid_channel_link() amazon_affiliate_id = saved_amazon_affiliate_id # Handle Amazon Affiliate ID if not amazon_affiliate_id or amazon_affiliate_id == "": has_affiliate = input("\nDo you have an Amazon Affiliate ID? (y/n): ").lower() if has_affiliate == 'y': amazon_affiliate_id = input("Enter your Amazon Affiliate ID: ").strip() if amazon_affiliate_id: # Replace only the first affiliate tag in memory forwarder.affiliate_tags[0] = amazon_affiliate_id logger.info(f"Using custom Amazon Affiliate ID: {amazon_affiliate_id}") else: # Use default if empty input amazon_affiliate_id = random.choice(forwarder.affiliate_tags) logger.info(f"Using default Amazon Affiliate ID: {amazon_affiliate_id}") else: # User doesn't have affiliate ID, use default amazon_affiliate_id = random.choice(forwarder.affiliate_tags) logger.info(f"Using default Amazon Affiliate ID: {amazon_affiliate_id}") # Save the configuration write_destination_config(destination_chat_id, channel_link, amazon_affiliate_id) else: # Use saved affiliate ID - replace only the first tag forwarder.affiliate_tags[0] = amazon_affiliate_id logger.info(f"Using saved Amazon Affiliate ID: {amazon_affiliate_id}") keywords = input("Enter keywords (comma-separated) or leave blank: ").split(",") if input("Use keywords? (y/n): ").lower() == 'y' else None await forwarder.forward_messages(source_ids, destination_chat_id, channel_link, use_bot, keywords) finally: # Clean up background WhatsApp forwarder process when Telegram exits if wa_proc and wa_proc.poll() is None: print("\n🛑 Stopping WhatsApp forwarder...") wa_proc.terminate() wa_proc.wait() if __name__ == "__main__": try: asyncio.run(main()) except KeyboardInterrupt: print("\n🛑 Stopped by user.") except Exception as e: import traceback print("\n" + "="*60) print("❌ FATAL ERROR — Auto.py crashed:") print("="*60) traceback.print_exc() print("="*60 + "\n")