it seems you lost in context, robo.


filterings should be normally be, you know?


"""

eg. 192.168.0.1
eg. [fe80::]
eg. 192.168.0.1:443
eg. [fe80::]:443
eg. 192.168.0.1-192.168.0.255
eg. 192.168.0.1-192.168.0.255:443
eg. 192.168.0.0/16
eg. fe80::/10
eg. 80
eg. 443
eg. 20-21
eg. 49152-65534

""".


since we do not doings with ipv6, we can omit it, but you can preserve if you want to.


you for example can:

111.*.*.*
*.111.*.*
*.*.111.*
*.*.*.111

and in either case you inputed it, it will filter these bridges strings through IPs. comprehend?





to this full script:


"""

import sys
import json
import re
import socket
import time
import netifaces
from PyQt5.QtWidgets import (
    QApplication, QWidget, QVBoxLayout, QHBoxLayout, QLabel,
    QPushButton, QPlainTextEdit, QListWidget, QRadioButton, QScrollArea
)
from PyQt5.QtCore import Qt, QThread, pyqtSignal
from PyQt5.QtGui import QColor


class BridgeChecker(QWidget):
    CONFIG_FILE = "bridges.cfg"
    GEOIP_FILE = "geoip"

    def __init__(self):
        super().__init__()
        self.geoip_data = self.load_geoip_data()
        self.country_buttons = {}  # To keep track of country code buttons
        self.selected_countries = set()  # To keep track of selected countries
        self.initUI()
        self.load_settings()

    def load_geoip_data(self):
        geoip_ranges = {}
        try:
            with open(self.GEOIP_FILE, 'r') as f:
                for line in f:
                    line = line.strip()
                    if line and not line.startswith('#'):
                        parts = line.split(',')
                        country_code = parts[2][:2]  # Get just the first two letters
                        if country_code not in geoip_ranges:
                            geoip_ranges[country_code] = []
                        geoip_ranges[country_code].append((int(parts[0]), int(parts[1])))
        except IOError:
            print("Error: GeoIP file not found.")
        return geoip_ranges

    def initUI(self):
        self.setWindowTitle('Tor Bridge Connectivity Checker')

        main_layout = QVBoxLayout(self)

        iface_layout = QHBoxLayout()
        iface_label = QLabel("Network Interface:")
        self.iface_selector = QPlainTextEdit(self)
        self.iface_selector.setReadOnly(True)
        self.iface_selector.setMaximumHeight(30)
        interfaces, _ = self.get_network_interfaces()
        self.iface_selector.setPlainText(', '.join(interfaces))
        iface_layout.addWidget(iface_label)
        iface_layout.addWidget(self.iface_selector)
        main_layout.addLayout(iface_layout)

        bridge_input_label = QLabel("Bridges to Check:")
        self.bridge_input = QPlainTextEdit(self)
        main_layout.addWidget(bridge_input_label)
        main_layout.addWidget(self.bridge_input)

        check_button = QPushButton('Check Bridges', self)
        check_button.clicked.connect(self.start_bridge_check)
        main_layout.addWidget(check_button)

        self.extract_clones_checkbox = QRadioButton('Extract Clones', self)
        self.extract_clones_checkbox.setChecked(True)
        main_layout.addWidget(self.extract_clones_checkbox)

        filter_country_label = QLabel("Select Countries to Exclude:")
        main_layout.addWidget(filter_country_label)

        # Create a horizontal layout for buttons
        country_button_layout = QHBoxLayout()
        for country_code in sorted(self.geoip_data.keys()):
            button = QPushButton(country_code)  # Show two-letter country codes
            button.setFixedSize(30, 30)  # Set a fixed size for compact alignment
            button.setCheckable(True)  # Button can be checked/unchecked
            button.setStyleSheet("background-color: lightgray;")  # Default background
            button.clicked.connect(lambda checked, code=country_code: self.toggle_country_selection(code, checked))
            self.country_buttons[country_code] = button  # Store the button for later access
            country_button_layout.addWidget(button)  # Add button to the horizontal layout

        # Add a button to deselect all
        deselect_all_button = QPushButton('Deselect All', self)
        deselect_all_button.clicked.connect(self.deselect_all_countries)
        country_button_layout.addWidget(deselect_all_button)

        # Add the country button layout to a scroll area
        scroll_area = QScrollArea()
        scroll_area.setWidgetResizable(True)
        country_widget = QWidget()
        country_widget.setLayout(country_button_layout)
        scroll_area.setWidget(country_widget)
        scroll_area.setMaximumHeight(50)  # Set a maximum height for the scroll area
        main_layout.addWidget(scroll_area)

        # Adjust the height of the octet input frame
        self.first_octet_input = QPlainTextEdit(self)
        self.first_octet_input.setPlaceholderText("Enter subnet octet (e.g., 185.177)...")
        self.first_octet_input.setMaximumHeight(30)  # Set a smaller height
        main_layout.addWidget(QLabel("Blacklist by Octet:"))
        main_layout.addWidget(self.first_octet_input)

        self.active_filters_list = QListWidget(self)
        main_layout.addWidget(QLabel("Active Blacklisted Filters:"))
        main_layout.addWidget(self.active_filters_list)

        add_filter_button = QPushButton('Add Filter', self)
        add_filter_button.clicked.connect(self.add_filter)
        main_layout.addWidget(add_filter_button)

        remove_filter_button = QPushButton('Remove Selected Filter', self)
        remove_filter_button.clicked.connect(self.remove_filter)
        main_layout.addWidget(remove_filter_button)

        # Use QHBoxLayout to hold both the bridge list and RTT list together
        list_layout = QHBoxLayout()
        self.bridges_list = QListWidget(self)
        self.rtt_labels = QListWidget(self)
        self.rtt_labels.setFocusPolicy(Qt.NoFocus)

        # Set a fixed width for the RTT label list to fit approximately 10 characters
        self.rtt_labels.setFixedWidth(100)  # Adjust this width for "XX ms"
        
        # Link scroll bars
        self.bridges_list.verticalScrollBar().valueChanged.connect(self.rtt_labels.verticalScrollBar().setValue)
        self.rtt_labels.verticalScrollBar().valueChanged.connect(self.bridges_list.verticalScrollBar().setValue)

        list_layout.addWidget(self.bridges_list)
        list_layout.addWidget(self.rtt_labels)
        main_layout.addLayout(list_layout)

        copy_all_button = QPushButton('Copy All', self)
        copy_all_button.clicked.connect(self.copy_all_to_clipboard)
        main_layout.addWidget(copy_all_button)

        console_label = QLabel("Console Log:")
        self.console_display = QPlainTextEdit(self)
        self.console_display.setReadOnly(True)
        self.console_display.setStyleSheet("background-color: black; color: white;")  # Black background, white text
        main_layout.addWidget(console_label)
        main_layout.addWidget(self.console_display)

    def deselect_all_countries(self):
        for button in self.country_buttons.values():
            button.setChecked(False)  # Uncheck all buttons
            button.setStyleSheet("background-color: lightgray; color: black;")  # Reset button color
        self.selected_countries.clear()  # Clear selected countries set

    def toggle_country_selection(self, country_code, checked):
        button = self.country_buttons[country_code]
        if checked:
            button.setStyleSheet("background-color: red; color: white;")  # Highlight color when selected
            self.selected_countries.add(country_code)  # Add to selected countries
        else:
            button.setStyleSheet("background-color: lightgray; color: black;")  # Default color when not selected
            self.selected_countries.remove(country_code)  # Remove from selected countries

    def get_network_interfaces(self):
        interfaces = netifaces.interfaces()
        try:
            default_gateway = netifaces.gateways()['default'][netifaces.AF_INET][1]
        except (KeyError, TypeError):
            default_gateway = interfaces[0] if interfaces else ''
        return interfaces, default_gateway

    def update_console(self, message):
        self.console_display.appendPlainText(message)

    def start_bridge_check(self):
        raw_bridge_data = self.bridge_input.toPlainText()
        cleaned_bridge_data = self.clean_input_bridge_data(raw_bridge_data)
        self.worker = BridgeCheckWorker(parent=self)
        self.worker.setBridgeData(cleaned_bridge_data)
        self.worker.resultReady.connect(self.display_results)
        self.worker.consoleUpdate.connect(self.update_console)
        self.worker.start()

    def clean_input_bridge_data(self, bridge_data):
        clean_data = re.sub(r'\s+\d+(\.\d+)?\s*ms', '', bridge_data)
        return clean_data

    def is_valid_ip(self, ip):
        pattern = re.compile(r'^(?:\d{1,3}\.){3}\d{1,3}$')
        if pattern.match(ip):
            return all(0 <= int(octet) <= 255 for octet in ip.split('.'))
        return False

    def add_filter(self):
        octet_pattern = self.first_octet_input.toPlainText().strip()
        if self.is_valid_octet_pattern(octet_pattern):
            if not self.active_filters_list.findItems(octet_pattern, Qt.MatchExactly):
                self.active_filters_list.addItem(octet_pattern)
                self.first_octet_input.clear()
                self.save_settings()  # Save to config after adding a filter
            else:
                self.update_console("This pattern is already in the list.")
        else:
            self.update_console("Invalid octet pattern. Please use the format 'X.Y' or 'X.*.*.*'.")

    def is_valid_octet_pattern(self, pattern):
        regex = r'^(?:\d{1,3}\.\d{1,3}|\d{1,3}\.\*\.\*\.\*|\*\.\*\.\*\.\*|\*\.\*\.\d{1,3})$'
        return re.match(regex, pattern) is not None

    def remove_filter(self):
        selected_items = self.active_filters_list.selectedItems()
        if not selected_items:
            self.update_console("No filter selected to remove.")
            return
        for item in selected_items:
            self.active_filters_list.takeItem(self.active_filters_list.row(item))
        self.save_settings()  # Save to config after removing a filter

    def display_results(self, sorted_bridges):
        self.bridges_list.clear()
        self.rtt_labels.clear()

        blacklist = [item.text() for item in self.active_filters_list.findItems("*", Qt.MatchWildcard)]
        extract_clones = self.extract_clones_checkbox.isChecked()
        unique_bridges = {}
        
        # Use selected countries for exclusions
        excluded_countries = self.selected_countries

        for bridge, latency in sorted_bridges:
            ip_port_match = re.search(r'(\d{1,3})\.(\d+)\.(\d+)\.(\d+)', bridge)
            if ip_port_match:
                ip_str = ip_port_match.group(0)
                ip_int = self.ip_to_int(ip_str)
                country_code_found = self.check_ip_in_geoip(ip_int)

                if country_code_found in excluded_countries:
                    continue  # Skip this bridge as it's in an excluded country

                filter_match = any(filter_entry.startswith(f"{ip_port_match.group(1)}.") or filter_entry.startswith(f"{ip_port_match.group(1)}.{ip_port_match.group(2)}.") for filter_entry in blacklist)

                if not filter_match:
                    if extract_clones and ip_port_match.group(1) not in unique_bridges:
                        unique_bridges[ip_port_match.group(1)] = (bridge, latency)

        for bridge, latency in unique_bridges.values():
            self.bridges_list.addItem(bridge)
            self.rtt_labels.addItem(f"{latency:.2f} ms")

    def ip_to_int(self, ip):
        return sum(int(octet) << (8 * (3 - i)) for i, octet in enumerate(ip.split('.')))

    def check_ip_in_geoip(self, ip_int):
        for country_code, ranges in self.geoip_data.items():
            for start, end in ranges:
                if start <= ip_int <= end:
                    return country_code
        return None

    def load_settings(self):
        try:
            with open(self.CONFIG_FILE, 'r') as f:
                config = json.load(f)
                self.bridge_input.setPlainText(config.get('bridge_input', ''))
                self.extract_clones_checkbox.setChecked(config.get('extract_clones', True))
                filters = config.get('filters', [])
                self.active_filters_list.clear()
                self.active_filters_list.addItems(filters)

                # Restore selected countries from settings
                self.selected_countries = set(config.get('selected_countries', []))
                for country_code, button in self.country_buttons.items():
                    if country_code in self.selected_countries:
                        button.setChecked(True)
                        button.setStyleSheet("background-color: red; color: white;")  # Highlight selected
        except FileNotFoundError:
            self.update_console("No configuration file found. Starting with default settings.")

    def save_settings(self):
        config = {
            'bridge_input': self.bridge_input.toPlainText(),
            'extract_clones': self.extract_clones_checkbox.isChecked(),
            'filters': [self.active_filters_list.item(i).text() for i in range(self.active_filters_list.count())],
            'selected_countries': list(self.selected_countries)  # Save selected countries
        }
        with open(self.CONFIG_FILE, 'w') as f:
            json.dump(config, f)

    def copy_all_to_clipboard(self):
        clipboard_text = []
        for i in range(self.bridges_list.count()):
            clipboard_text.append(self.bridges_list.item(i).text())
        clipboard = QApplication.clipboard()
        clipboard.setText("\n".join(clipboard_text))


class BridgeCheckWorker(QThread):
    resultReady = pyqtSignal(list)
    consoleUpdate = pyqtSignal(str)

    def __init__(self, parent=None):
        super().__init__(parent)
        self.bridge_data = ""

    def setBridgeData(self, bridge_data):
        self.bridge_data = bridge_data

    def run(self):
        responsive_bridges = self.process_bridges(self.bridge_data)
        sorted_bridges = sorted(responsive_bridges.items(), key=lambda x: x[1])
        self.resultReady.emit(sorted_bridges)

    def process_bridges(self, bridge_data):
        bridge_strings = self.extract_bridges(bridge_data)
        responsive_bridges = {}
        for bridge_string in bridge_strings:
            ip_port_match = re.search(r'(\d{1,3}(?:\.\d{1,3}){3}):(\d+)', bridge_string)
            if ip_port_match:
                ip = ip_port_match.group(1)
                port = int(ip_port_match.group(2))
                success, latency = self.check_ip_port(ip, port, 3.0)
                if success:
                    responsive_bridges[bridge_string] = latency
        return responsive_bridges

    def extract_bridges(self, bridge_data):
        pattern = re.compile(
            r'(?:(\d{1,3}(?:\.\d{1,3}){3}:\d+\s+[A-F0-9]{40})|'  # Type One
            r'(obfs4\s+\d{1,3}(?:\.\d{1,3}){3}:\d+\s+[A-F0-9]{40}\s+cert=[A-Za-z0-9+/=]+\s+iat-mode=\d+))',  # Type Two
            re.MULTILINE
        )
        matches = pattern.findall(bridge_data)
        return [''.join(filter(None, match)) for match in matches]

    def check_ip_port(self, ip, port, timeout):
        start_time = time.perf_counter()
        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
        sock.settimeout(timeout)
        try:
            sock.connect((ip, port))
            end_time = time.perf_counter()
            latency = (end_time - start_time) * 1000
            self.consoleUpdate.emit(f"Checking {ip}:{port}… Success")
            return True, latency
        except socket.error:
            self.consoleUpdate.emit(f"Checking {ip}:{port}… Failed")
            return False, None
        finally:
            sock.close()


if __name__ == '__main__':
    app = QApplication(sys.argv)
    ex = BridgeChecker()
    ex.show()
    sys.exit(app.exec_())

""".










something functioning, but code reduced to 343 still, not sure.


also, need prepared filtering proceeding as:

1-we firstly checking if any filtration present
2-then before initiating any checkings on "check bridges" button press, we aplying a filtration presenting "IF"
3-then we can omit these bridges strings we copy-pasted into that "bridges to check" list so it will not even care to check them through filters and do no waste time in line of checking.


also, could be nice to do Log more informative as for example displaying how much overally bridges there anywhere in total or left to check and how much or many got filtered out.

need do proper PRE-filtering system which basically initially PRE-filter everything to be checked in from "bridges to check" list, and then if we wanting to adjust filters after checking, then POST-filtering should be performed live while adjusting filters and updated in sorted list in bridges strings filtered.

also, need a "select all" button just right before "deselect" one.
we are also storing all settings in "bridges"cfg" and all possible/impossible states of that config file.