I am taking a cyber security class and our assignment is to find the part in this script that is broken. When it tries to log into api id/hash with a new number, it gives the error like this

2024-10-07 22:51:02 - ERROR - Invalid API ID 28941988 and API Hash 69a4e47dbcd4bcc01fddc92ab1ffd74f combination.

I think it may not be saving the session files incorrectly, because it should try to log in and create a new session if the phone number isn't associated with the id hash. All the ID/Hash combos its being fed are correct. I've double checked that multiple times. What is the cause of the problem? Here is the script:

import asyncio
import json
import logging
import os
from telethon import TelegramClient, errors, Button
from collections import defaultdict

# ----------------------------- Configuration -----------------------------

# Configure logging
logging.basicConfig(
    filename='bot_log.log',  # Log file name
    level=logging.INFO,       # Log level
    format='%(asctime)s - %(levelname)s - %(message)s',  # Log format
    datefmt='%Y-%m-%d %H:%M:%S'  # Date format
)

# Configuration Constants
GROUP_LINK = 'https://t.me/rocketproxyupdates'  # Telegram group link
BUTTON_TEXT = 'Join Our Telegram Group'         # Text displayed on the button

USER_LIST_FILE = 'targets.json'         # JSON file containing target users
SENT_FILE = 'sent.json'                 # JSON file to track sent users
API_CONFIG_FILE = 'clean_accounts.json' # JSON file containing API IDs and Hashes
PHONE_NUMBERS_FILE = 'phone_numbers.json' # JSON file containing phone numbers
BLOCKED_FILE = 'blocked_numbers.json'   # JSON file to track blocked phone numbers
BLOCKED_API_FILE = 'blocked_api_ids.json'  # JSON file to track blocked API IDs/hashes

# Directory to store session files
SESSION_DIR = 'sessions'

# Create the sessions directory if it doesn't exist
if not os.path.exists(SESSION_DIR):
    os.makedirs(SESSION_DIR)

# Rate Limiting Constants
WAIT_TIME_BASE = 60    # Base wait time in seconds between messages
WAIT_TIME_FACTOR = 60  # Additional wait time factor based on errors
MAX_WAIT_TIME = 3600    # Maximum wait time in seconds (1 hour)

# Source chat information
FORWARD_SOURCE_CHAT_ID = 'rocketproxyupdates'  # Telegram chat to fetch messages from

# ----------------------------- Functions -----------------------------

def read_api_config(filename, blocked_api_ids):
    """
    Reads API IDs and Hashes from the specified JSON file, excluding blocked ones.

    Args:
        filename (str): Path to the JSON configuration file.
        blocked_api_ids (set): Set of blocked API IDs/hashes.

    Returns:
        list: List of dictionaries containing 'api_id', 'api_hash', and 'api_key'.

    Raises:
        ValueError: If the number of API IDs and Hashes do not match.
    """
    with open(filename, 'r') as f:
        data = json.load(f)
    api_ids = data.get('API_ID_LIST', [])
    api_hashes = data.get('API_HASH_LIST', [])
    if len(api_ids) != len(api_hashes):
        logging.error("API_ID_LIST and API_HASH_LIST must be of the same length.")
        raise ValueError("API_ID_LIST and API_HASH_LIST must be of the same length.")
    api_credentials = []
    for api_id, api_hash in zip(api_ids, api_hashes):
        api_key = f"{api_id}_{api_hash}"
        if api_key not in blocked_api_ids:
            api_credentials.append({'api_id': api_id, 'api_hash': api_hash, 'api_key': api_key})
    return api_credentials

def read_phone_numbers(filename, blocked_numbers):
    """
    Reads phone numbers from the specified JSON file, excluding blocked numbers.

    Args:
        filename (str): Path to the JSON file containing phone numbers.
        blocked_numbers (set): Set of blocked phone numbers.

    Returns:
        list: A list of phone numbers in international format.
    """
    with open(filename, 'r') as f:
        phone_numbers = json.load(f)
    return [phone for phone in phone_numbers if phone not in blocked_numbers]

def sanitize_phone_number(phone_number):
    """
    Sanitizes the phone number to create a valid filename.

    Args:
        phone_number (str): Phone number in international format.

    Returns:
        str: Sanitized phone number suitable for filenames.
    """
    return phone_number.replace('+', '').replace(' ', '').replace('-', '').replace('(', '').replace(')', '')

def get_session_file(phone_number, api_id):
    """
    Generates the session file path for a given phone number and API ID.

    Args:
        phone_number (str): Phone number in international format.
        api_id (str or int): API ID.

    Returns:
        str: Path to the session file.
    """
    sanitized_number = sanitize_phone_number(phone_number)
    return os.path.join(SESSION_DIR, f'session_{sanitized_number}_{api_id}.session')

async def get_or_create_client(phone_number, credentials):
    """
    Creates a new Telegram client or loads an existing session for a given phone number.

    Args:
        phone_number (str): Phone number in international format.
        credentials (dict): Dictionary containing 'api_id' and 'api_hash'.

    Returns:
        TelegramClient or None: The Telegram client instance or None if failed.
    """
    api_id = credentials['api_id']
    api_hash = credentials['api_hash']
    session_file = get_session_file(phone_number, api_id)
    
    client = TelegramClient(session_file, api_id, api_hash)

    try:
        await client.connect()
    except Exception as e:
        logging.error(f"Failed to connect client for {phone_number} using API ID {api_id}: {e}")
        # Attempt to delete the session file and retry
        if os.path.exists(session_file):
            os.remove(session_file)
            logging.info(f"Deleted corrupted session file {session_file}. Retrying connection.")
            client = TelegramClient(session_file, api_id, api_hash)
            await client.connect()
        else:
            return None

    if await client.is_user_authorized():
        logging.info(f"Loaded existing session for {phone_number} using API ID {api_id}")
        return client
    else:
        try:
            await client.send_code_request(phone_number)
            code = input(f"Enter the authentication code for {phone_number}: ")
            await client.sign_in(phone_number, code)
            logging.info(f"Created new session for {phone_number} using API ID {api_id}")
            return client
        except errors.PhoneCodeInvalidError:
            logging.error(f"Invalid authentication code for {phone_number}.")
        except errors.PhoneCodeExpiredError:
            logging.error(f"Authentication code expired for {phone_number}.")
        except errors.PhoneNumberBannedError:
            logging.error(f"Phone number {phone_number} is banned.")
        except errors.ApiIdInvalidError:
            logging.error(f"Invalid API ID {api_id} and API Hash {api_hash} combination.")
            raise  # Re-raise to be caught in main()
        except Exception as e:
            error_message = str(e)
            if 'The api_id/api_hash combination is invalid' in error_message:
                logging.error(f"Invalid API ID {api_id} and API Hash {api_hash} combination.")
                raise errors.ApiIdInvalidError('Invalid API credentials')
            logging.error(f"Failed to create session for {phone_number} using API ID {api_id}: {e}")
        return None

async def forward_to_user(client, user, source_chat_entity, message_id, phone_number):
    """
    Forwards a message from the source chat to the target user.

    Args:
        client (TelegramClient): The Telegram client instance.
        user (dict): Dictionary containing user information (e.g., {'username': 'example_user'}).
        source_chat_entity: The source chat entity from which to forward messages.
        message_id (int): The ID of the message to forward.
        phone_number (str): The phone number associated with the client.

    Returns:
        Message or None: The forwarded message or None if failed.
    """
    try:
        forwarded_message = await client.forward_messages(
            entity=user['username'],
            messages=message_id,
            from_peer=source_chat_entity
        )
        logging.info(f"Forwarded message to {user['username']} using API ID {client.api_id} and phone {phone_number}")
        return forwarded_message
    except errors.FloodWaitError as e:
        wait_time = max(WAIT_TIME_BASE, e.seconds + WAIT_TIME_FACTOR)
        logging.warning(f"Flood wait error for phone {phone_number} on API ID {client.api_id}: Waiting for {wait_time} seconds before retrying.")
        await asyncio.sleep(wait_time)
        return None
    except errors.UsernameNotOccupiedError:
        logging.warning(f"No user has the username {user['username']} for phone {phone_number} using API ID {client.api_id}.")
        return None
    except errors.PeerFloodError as e:
        logging.error(f"Peer flood error when forwarding message to {user['username']} using API ID {client.api_id} and phone {phone_number}.")
        logging.error(f"Details: {e}")
        raise e  # Re-raise the exception to be caught in main()
    except Exception as e:
        logging.error(f"Failed to forward message to {user['username']} using API ID {client.api_id} and phone {phone_number}. Error: {e}")
        return None

async def send_button_message(client, user, phone_number):
    """
    Sends a message with a button to the target user.

    Args:
        client (TelegramClient): The Telegram client instance.
        user (dict): Dictionary containing user information (e.g., {'username': 'example_user'}).
        phone_number (str): The phone number associated with the client.

    Returns:
        bool: True if the message was sent successfully, False otherwise.
    """
    try:
        message_text = "hey bro check this out"
        
        await client.send_message(
            entity=user['username'],
            message=message_text,
            buttons=[Button.url(BUTTON_TEXT, GROUP_LINK)]
        )
        
        logging.info(f"Sent message to {user['username']} using API ID {client.api_id} and phone {phone_number}")
        await asyncio.sleep(1)
        return True

    except errors.FloodWaitError as e:
        wait_time = max(WAIT_TIME_BASE, e.seconds + WAIT_TIME_FACTOR)
        logging.warning(f"Flood wait error for phone {phone_number} on API ID {client.api_id}: Waiting for {wait_time} seconds before retrying.")
        await asyncio.sleep(wait_time)
        return False

    except errors.UsernameNotOccupiedError:
        logging.warning(f"No user has the username {user['username']} for phone {phone_number} using API ID {client.api_id}.")
        return False

    except errors.PeerFloodError as e:
        logging.error(f"Peer flood error when sending message to {user['username']} using API ID {client.api_id} and phone {phone_number}.")
        logging.error(f"Details: {e}")
        raise e  # Re-raise the exception to be caught in main()

    except Exception as e:
        logging.error(f"Failed to send message to {user['username']} using API ID {client.api_id} and phone {phone_number}. Error: {e}")
        return False

async def get_source_chat_and_message(client, phone_number):
    """
    Retrieves the source chat entity and the latest message ID.

    Args:
        client (TelegramClient): The Telegram client instance.
        phone_number (str): The phone number associated with the client.

    Returns:
        tuple: Source chat entity and the latest message ID.
    """
    try:
        source_chat = await client.get_entity(FORWARD_SOURCE_CHAT_ID)
    except Exception as e:
        logging.error(f"Error resolving source chat '{FORWARD_SOURCE_CHAT_ID}' for phone {phone_number} using API ID {client.api_id}: {e}")
        return None, None

    try:
        message = await client.get_messages(source_chat, limit=1)
        if not message:
            logging.warning(f"No messages found in the source chat '{FORWARD_SOURCE_CHAT_ID}' for phone {phone_number} using API ID {client.api_id}.")
            return source_chat, None
        return source_chat, message[0].id
    except Exception as e:
        logging.error(f"Error fetching messages from '{FORWARD_SOURCE_CHAT_ID}' for phone {phone_number} using API ID {client.api_id}: {e}")
        return source_chat, None

# ----------------------------- Main Function -----------------------------

async def main():
    """
    The main function that orchestrates reading configurations, assigning API credentials,
    creating/loading clients, sending messages, forwarding messages, and handling rate limits.
    """
    # Load blocked numbers
    blocked_numbers = set()
    if os.path.exists(BLOCKED_FILE):
        try:
            with open(BLOCKED_FILE, 'r') as f:
                blocked_numbers = set(json.load(f))
            logging.info(f"Loaded {len(blocked_numbers)} blocked phone numbers.")
        except Exception as e:
            logging.error(f"Failed to read blocked numbers from '{BLOCKED_FILE}': {e}")
            return
    else:
        logging.info("No blocked phone numbers found. Starting fresh.")

    # Load blocked API IDs/hashes
    blocked_api_ids = set()
    if os.path.exists(BLOCKED_API_FILE):
        try:
            with open(BLOCKED_API_FILE, 'r') as f:
                blocked_api_ids = set(json.load(f))
            logging.info(f"Loaded {len(blocked_api_ids)} blocked API IDs/hashes.")
        except Exception as e:
            logging.error(f"Failed to read blocked API IDs/hashes from '{BLOCKED_API_FILE}': {e}")
            return
    else:
        logging.info("No blocked API IDs/hashes found. Starting fresh.")

    # Load API credentials, excluding blocked ones
    try:
        api_credentials = read_api_config(API_CONFIG_FILE, blocked_api_ids)
        logging.info(f"Loaded {len(api_credentials)} API credentials after excluding blocked ones.")
    except Exception as e:
        logging.error(f"Failed to read API configuration: {e}")
        return

    # Load phone numbers, excluding blocked ones
    try:
        phone_numbers = read_phone_numbers(PHONE_NUMBERS_FILE, blocked_numbers)
        logging.info(f"Loaded {len(phone_numbers)} phone numbers after excluding blocked numbers.")
    except Exception as e:
        logging.error(f"Failed to read phone numbers: {e}")
        return

    # Load target users
    try:
        with open(USER_LIST_FILE, 'r') as f:
            users = json.load(f)
        logging.info(f"Loaded {len(users)} target users.")
    except Exception as e:
        logging.error(f"Failed to read target users from '{USER_LIST_FILE}': {e}")
        return

    # Load already sent users to prevent duplicates
    sent_users = set()
    if os.path.exists(SENT_FILE):
        try:
            with open(SENT_FILE, 'r') as f:
                sent_users = set(json.load(f))
            logging.info(f"Loaded {len(sent_users)} already sent users.")
        except Exception as e:
            logging.error(f"Failed to read sent users from '{SENT_FILE}': {e}")
            return
    else:
        logging.info("No sent users found. Starting fresh.")

    # Initialize error tracking
    error_counts_phone_number = defaultdict(int)
    error_counts_api = defaultdict(int)

    # While there are API credentials to try
    available_api_credentials = api_credentials.copy()

    while available_api_credentials:
        creds = available_api_credentials.pop(0)
        api_id = creds['api_id']
        api_hash = creds['api_hash']
        api_key = creds['api_key']

        logging.info(f"Processing with API ID {api_id} and API Hash {api_hash}")

        # Start from the beginning of the phone number list for each new API credential
        phone_numbers_to_try = phone_numbers.copy()

        phone_number_index = 0
        api_blocked = False

        while phone_number_index < len(phone_numbers_to_try):
            phone_number = phone_numbers_to_try[phone_number_index]
            phone_number_index += 1

            # Skip blocked phone numbers
            if phone_number in blocked_numbers:
                continue

            try:
                client = await get_or_create_client(phone_number, {'api_id': api_id, 'api_hash': api_hash})
            except errors.ApiIdInvalidError:
                # Invalid API ID/Hash
                blocked_api_ids.add(api_key)
                with open(BLOCKED_API_FILE, 'w') as f:
                    json.dump(list(blocked_api_ids), f)
                logging.info(f"Added API ID {api_id} and API Hash to blocked API IDs.")
                api_blocked = True
                break  # Exit the phone number loop to move to the next API credential

            if client is None:
                # Phone number is likely blocked or failed to connect
                blocked_numbers.add(phone_number)
                with open(BLOCKED_FILE, 'w') as f:
                    json.dump(list(blocked_numbers), f)
                logging.info(f"Added {phone_number} to blocked numbers.")
                continue

            try:
                # Retrieve the latest message from the source chat
                source_chat_entity, message_id = await get_source_chat_and_message(client, phone_number)
                if not source_chat_entity or not message_id:
                    logging.warning(f"Skipping phone number '{phone_number}' due to source chat or message retrieval failure.")
                    continue

                # Iterate through target users
                for user in users:
                    if user['username'] in sent_users:
                        continue  # Skip already sent users

                    # Send button message
                    await send_button_message(client, user, phone_number)

                    # Forward the original message
                    await forward_to_user(client, user, source_chat_entity, message_id, phone_number)

                    sent_users.add(user['username'])
                    # Update sent.json
                    try:
                        with open(SENT_FILE, 'w') as f:
                            json.dump(list(sent_users), f)
                        logging.info(f"Added {user['username']} to sent users.")
                    except Exception as e:
                        logging.error(f"Failed to update '{SENT_FILE}': {e}")

                    # Wait to respect rate limits
                    logging.info(f"Waiting {WAIT_TIME_BASE} seconds before sending the next message.")
                    await asyncio.sleep(WAIT_TIME_BASE)

            except errors.PeerFloodError as e:
                logging.error(f"Peer flood error occurred with phone {phone_number} using API ID {api_id}: {e}")

                # Update error counts
                error_counts_phone_number[phone_number] += 1
                error_counts_api[api_key] += 1

                # Check if we should block the phone number
                if error_counts_phone_number[phone_number] >= 2:
                    logging.info(f"Phone number {phone_number} failed with PeerFloodError multiple times. Blocking phone number.")
                    blocked_numbers.add(phone_number)
                    with open(BLOCKED_FILE, 'w') as f:
                        json.dump(list(blocked_numbers), f)
                    logging.info(f"Added {phone_number} to blocked numbers.")

                # Check if we should block the API credential
                if error_counts_api[api_key] >= 2:
                    logging.info(f"API ID/Hash {api_key} failed with PeerFloodError multiple times. Blocking API ID/Hash.")
                    blocked_api_ids.add(api_key)
                    with open(BLOCKED_API_FILE, 'w') as f:
                        json.dump(list(blocked_api_ids), f)
                    api_blocked = True
                    break  # Exit the phone number loop to move to the next API credential

                continue  # Move to the next phone number

            except errors.FloodWaitError as e:
                wait_time = e.seconds + WAIT_TIME_FACTOR
                logging.warning(f"Flood wait error for phone {phone_number} on API ID {api_id}: Waiting for {wait_time} seconds.")
                await asyncio.sleep(wait_time)
                continue  # After waiting, try the next action

            except Exception as e:
                logging.error(f"An unexpected error occurred with phone number {phone_number}: {e}")
                continue

            finally:
                await client.disconnect()

        # If API credential is blocked, move to the next one
        if api_blocked:
            # Remove blocked API credential from the available list
            api_credentials = [cred for cred in api_credentials if cred['api_key'] not in blocked_api_ids]
            available_api_credentials = [cred for cred in available_api_credentials if cred['api_key'] not in blocked_api_ids]
            continue  # Proceed to next API credential

    logging.info("All API credentials have been processed.")

# ----------------------------- Entry Point -----------------------------

if __name__ == '__main__':
    asyncio.run(main())