├── requirements.txt ├── LICENSE ├── README.md ├── server.py └── client.py /requirements.txt: -------------------------------------------------------------------------------- 1 | colorama 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 kaifcodec 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Term-chat 2 | 3 | Terminal Chat repository which lets you chat over the internet from anywhere with your friends or family members using Termux and Ngrok. Why use WhatsApp when you don't have control? Why let meta eye on your datas? Remember the PEGASUS case... 4 | 5 | --- 6 | 7 | ## Key Features 8 | 9 | 1. **Lightweight** 10 | 2. **No connection issues** 11 | 3. **Localhost chat over Wi-Fi or hotspot** 12 | 4. **Supports all file formats** 13 | 5. **Send directories (zip them first)** 14 | 6. **Requires Ngrok account and authtoken** 15 | 7. **Easy Ngrok authtoken acquisition** 16 | 8. **Uses sockets for stable connections** 17 | 9. **Built with Python** 18 | 10. **Compatible with Termux** 19 | 11. **Runs on other environments** 20 | 12. **Regular updates and bug fixes** 21 | 13. **24×7 issue resolution service** 22 | 23 | --- 24 | ## Screenshots 25 | 26 | - **Starting the server:** 27 | ![Screenshot_20250118-112328_1](https://github.com/user-attachments/assets/a77e5a1f-9d70-4d8f-9ae5-793b629b4378) 28 | 29 | - **Starting Ngrok:** 30 | ![Screenshot_20250118-111227_1~2](https://github.com/user-attachments/assets/38e632f4-b2df-4932-8900-35110be8e984) 31 | 32 | 33 | - **Joining the server (you):** 34 | ![Screenshot_20250118-112311_1](https://github.com/user-attachments/assets/c0f4ed14-3e22-42eb-b2aa-46140b227fad) 35 | 36 | - **Joining the server (your friend):** 37 | ![Screenshot_20250118-112319_1](https://github.com/user-attachments/assets/cc215786-0e88-4252-ab9e-a7b4360e8df6) 38 | 39 | --- 40 | ## Usage Commands for Termux 41 | 42 | 1. `cd $HOME` 43 | 2. `apt update -y && apt upgrade -y` 44 | 3. `pkg install git` 45 | 4. `pkg install openssl` 46 | 5. `pkg install python` 47 | 6. `pkg install wget unzip` 48 | 7. `git clone https://github.com/kaifcodec/Term-chat.git` 49 | 8. `wget https://bin.equinox.io/c/4VmDzA7iaHb/ngrok-stable-linux-arm.zip` 50 | 9. `unzip ngrok-stable-linux-arm.zip` 51 | 10. `mv ngrok /data/data/com.termux/files/usr/bin/` 52 | 11. `cd Term-chat` 53 | 12. `pip install -r requirements.txt` 54 | 13. `python server.py` 55 | 56 | --- 57 | 58 | ## Next Steps 59 | 60 | 1. **Server Start** 61 | - After completing the above commands, your server will start successfully. 62 | - It will display "Server started at 0.0.0.0:port". 63 | - Note the port number. 64 | 65 | 2. **Ngrok Setup** 66 | - Start a new session and enable the hotspot on your phone. 67 | - Get your Ngrok authtoken from the official site. 68 | - Run `ngrok config add-authtoken 'your_authtoken'` 69 | - Run `ngrok tcp 'port'` (use the server's listening port) 70 | - Ngrok will provide a link like `tcp://0.tcp.in.ngrok.io:port2`. Copy it. 71 | 72 | 3. **Client Connection** 73 | - In the Term-chat directory, run `python client.py` 74 | - Enter the copied Ngrok link IP (i.e. 0.tcp.in.ngrok.io) and port. 75 | - Choose your nickname and join the server. 76 | 77 | --- 78 | 79 | ## Steps for Others to Join the Chat 80 | 81 | 1. Clone the repository in their Termux. 82 | 2. Install the requirements. 83 | 3. Start `client.py` using `python client.py`. 84 | 4. Enter the same IP and port from the Ngrok link. 85 | 5. Choose a nickname and start chatting. 86 | 87 | --- 88 | 89 | ## Additional Features 90 | 91 | - **File Transfers** 92 | 1. Copy the path of the file you want to send. 93 | 2. Type `#file` in the chat. 94 | 3. Paste the copied path when prompted. 95 | 4. Press enter to send the file. 96 | 97 | --- 98 | 99 | ## Troubleshooting 100 | 101 | 1. **Server IP Issues** 102 | - If starting the server one after another, change the server IP on which it is listening (use the nano tool for editing). 103 | 2. **Upcoming Automation** 104 | - Automation functions will be updated soon. 105 | 3. **Issue Reporting** 106 | - Open an issue if you encounter any errors and provide screenshots for quick resolution. 107 | 108 | --- 109 | 110 | Feel free to reach out for any support or suggestions at **kaifcodec@gmail.com**! 111 | 112 | --- 113 | -------------------------------------------------------------------------------- /server.py: -------------------------------------------------------------------------------- 1 | import socket 2 | import threading 3 | 4 | class ChatServer: 5 | def __init__(self, host, port): 6 | self.host = host 7 | self.port = port 8 | self.clients = {} 9 | self.lock = threading.Lock() 10 | 11 | def broadcast(self, message, exclude_client=None): 12 | """Broadcast a message to all connected clients except the excluded one.""" 13 | with self.lock: # Prevent race conditions 14 | for client in list(self.clients.keys()): 15 | if client != exclude_client: 16 | try: 17 | client.send(message.encode("utf-8")) # Ensure message is encoded to UTF-8 18 | except UnicodeEncodeError: 19 | print(f"Error encoding message for {client}") 20 | self.remove_client(client) 21 | except Exception as e: 22 | print(f"Error sending message to {client}: {e}") 23 | self.remove_client(client) 24 | 25 | def handle_client(self, client_socket, client_address): 26 | """Handle client communication.""" 27 | try: 28 | while True: 29 | # Receive the message 30 | message = client_socket.recv(1024) 31 | if not message: 32 | break 33 | 34 | # Attempt to decode the message 35 | try: 36 | decoded_message = message.decode("utf-8") 37 | if decoded_message.startswith("#file"): # Check if it's a file header 38 | self.handle_file_transfer(client_socket, decoded_message) 39 | else: 40 | self.broadcast(decoded_message, exclude_client=client_socket) 41 | except UnicodeDecodeError: 42 | print(f"Error decoding message from {client_address}") 43 | continue # Skip this message if it can't be decoded 44 | except Exception as e: 45 | print(f"Error with client {client_address}: {e}") 46 | finally: 47 | self.remove_client(client_socket) 48 | 49 | def handle_file_transfer(self, client_socket, header): 50 | """Handle file transfer from client.""" 51 | try: 52 | # Parse file metadata from the header 53 | _, file_name, file_size = header.split("|") 54 | file_size = int(file_size) 55 | 56 | # Receive file data 57 | file_data = b"" 58 | while len(file_data) < file_size: 59 | chunk = client_socket.recv(min(file_size - len(file_data), 1024)) 60 | if not chunk: 61 | raise ConnectionError("File transfer interrupted.") 62 | file_data += chunk 63 | 64 | # Save the received file 65 | with open(f"/storage/emulated/0/Download/{file_name}", "wb") as file: 66 | file.write(file_data) 67 | 68 | print(f"Received file: {file_name} ({len(file_data)} bytes)") 69 | self.broadcast(f"{file_name} has been received.", exclude_client=client_socket) 70 | 71 | except Exception as e: 72 | print(f"Error during file transfer: {e}") 73 | self.broadcast(f"Error receiving file.", exclude_client=client_socket) 74 | 75 | def remove_client(self, client_socket): 76 | """Remove a client from the server.""" 77 | with self.lock: 78 | client_socket.close() 79 | self.clients.pop(client_socket, None) 80 | 81 | def accept_connections(self): 82 | """Accept new client connections.""" 83 | server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 84 | server_socket.bind((self.host, self.port)) 85 | server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 86 | server_socket.listen(5) 87 | print(f"Server started on {self.host}:{self.port}") 88 | while True: 89 | client_socket, client_address = server_socket.accept() 90 | print(f"Connection from {client_address}") 91 | self.clients[client_socket] = client_address 92 | threading.Thread(target=self.handle_client, args=(client_socket, client_address)).start() 93 | 94 | def run(self): 95 | """Start the server.""" 96 | self.accept_connections() 97 | 98 | if __name__ == "__main__": 99 | host = "0.0.0.0" # Listen on all available interfaces 100 | port = 5032 101 | chat_server = ChatServer(host, port) 102 | chat_server.run() 103 | -------------------------------------------------------------------------------- /client.py: -------------------------------------------------------------------------------- 1 | import socket 2 | import threading 3 | import textwrap # To wrap long messages into multiple lines 4 | from colorama import Fore, init 5 | import os # For file operations 6 | 7 | # Initialize colorama 8 | init(autoreset=True) 9 | 10 | class ChatClient: 11 | def __init__(self, host, port): 12 | self.host = host 13 | self.port = port 14 | self.client = None 15 | self.nickname = None 16 | self.first_prompt = True # Flag to show the prompt only once 17 | self.header_size = 64 # Fixed header size in bytes 18 | 19 | def connect(self): 20 | """Connect to the chat server.""" 21 | if self.client: # Close the existing socket if it's already open 22 | self.client.close() 23 | 24 | self.client = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 25 | self.client.connect((self.host, self.port)) 26 | self.nickname = input(Fore.YELLOW + "Choose a nickname: ") 27 | self.client.send(self.nickname.encode("utf-8")) 28 | 29 | def receive_messages(self): 30 | """Receive and display messages from the server.""" 31 | while True: 32 | try: 33 | message = self.client.recv(1024) # Receive the message 34 | if not message: 35 | raise ConnectionError("Disconnected from server.") 36 | decoded_message = message.decode("utf-8") # Decode message 37 | if decoded_message.startswith("#file"): # Check if it's a file header 38 | self.receive_file(decoded_message) 39 | else: 40 | self.display_message_in_box(decoded_message, sender=True) 41 | except UnicodeDecodeError: 42 | print(Fore.RED + "Error decoding message. Skipping message.") 43 | continue 44 | except ConnectionError as e: 45 | print(Fore.RED + f"Connection error: {e}") 46 | break 47 | except Exception as e: 48 | print(Fore.RED + f"An error occurred: {e}") 49 | break 50 | 51 | def receive_file(self, header): 52 | """Handle receiving a file.""" 53 | try: 54 | _, file_name, file_size = header.split("|") 55 | file_size = int(file_size) 56 | 57 | # Receive file data 58 | file_data = b"" 59 | while len(file_data) < file_size: 60 | chunk = self.client.recv(min(file_size - len(file_data), 1024)) 61 | if not chunk: 62 | raise ConnectionError("File transfer interrupted.") 63 | file_data += chunk 64 | 65 | # Save the file temporarily in the current directory 66 | with open(file_name, "wb") as file: 67 | file.write(file_data) 68 | 69 | # Move the file to /storage/emulated/0/Download 70 | os.system(f"mv {file_name} /storage/emulated/0/Download/") 71 | 72 | print(Fore.GREEN + f"Received file saved to: /storage/emulated/0/Download/{file_name}") 73 | except Exception as e: 74 | print(Fore.RED + f"Error receiving file: {e}") 75 | 76 | def display_message_in_box(self, message, sender): 77 | """Display a message inside a box with multi-line support.""" 78 | terminal_width = 80 # Adjust to your terminal width 79 | max_msg_width = terminal_width - 20 80 | 81 | # Wrap long messages into multiple lines 82 | wrapped_lines = textwrap.wrap(message, width=max_msg_width) 83 | 84 | # Calculate box width (same for sender and user) 85 | box_width = max(len(line) for line in wrapped_lines) + 2 86 | 87 | if sender: 88 | # Sender's message on the left 89 | border = Fore.GREEN + f"+{'-' * box_width}+" 90 | print("\n" + border) 91 | for line in wrapped_lines: 92 | print(Fore.GREEN + f"| {line.ljust(box_width - 2)} |") # Left-align lines 93 | print(border) 94 | else: 95 | # Your message on the right 96 | spaces = ' ' * (terminal_width - box_width - 4) 97 | border = Fore.YELLOW + f"{spaces}+{'-' * box_width}+" 98 | print("\n" + border) 99 | for line in wrapped_lines: 100 | print(Fore.YELLOW + f"{spaces}| {line.ljust(box_width - 2)} |") # Right-align lines 101 | print(border) 102 | 103 | def send_messages(self): 104 | """Send messages or files to the server.""" 105 | while True: 106 | if self.first_prompt: 107 | print(Fore.CYAN + "You can type a message or use #file to send a file.") 108 | self.first_prompt = False 109 | 110 | try: 111 | message = input("").strip() 112 | 113 | if self.client.fileno() == -1: # Check if the socket is still open 114 | print(Fore.RED + "Connection lost. Reconnecting...") 115 | self.connect() # Reconnect if socket is closed 116 | 117 | if message == "#file": 118 | file_path = input(Fore.BLUE + "Enter the file path: ").strip() 119 | try: 120 | with open(file_path, "rb") as file: 121 | file_data = file.read() 122 | file_name = file_path.split("/")[-1] 123 | file_size = len(file_data) 124 | header = f"#file|{file_name}|{file_size}".ljust(self.header_size) 125 | self.client.send(header.encode("utf-8")) # Send fixed-size header 126 | self.client.sendall(file_data) # Send the file 127 | print(Fore.GREEN + f"File '{file_name}' sent successfully.") 128 | except FileNotFoundError: 129 | print(Fore.RED + "File not found. Please try again.") 130 | else: 131 | formatted_message = f"{self.nickname}: {message}".ljust(self.header_size) 132 | self.client.send(formatted_message.encode("utf-8")) 133 | 134 | # Clear the input line from the terminal after sending 135 | print("\033[A \033[A", end="\r") 136 | self.display_message_in_box(f"You: {message}", sender=False) 137 | 138 | except Exception as e: 139 | print(Fore.RED + f"Error sending message: {e}") 140 | continue # Continue to next iteration if there's an error 141 | 142 | def run(self): 143 | """Run the client.""" 144 | try: 145 | self.connect() 146 | receive_thread = threading.Thread(target=self.receive_messages) 147 | receive_thread.start() 148 | self.send_messages() 149 | except Exception as e: 150 | print(Fore.RED + f"An error occurred: {e}") 151 | finally: 152 | if self.client: 153 | self.client.close() 154 | 155 | if __name__ == "__main__": 156 | HOST = input(Fore.YELLOW + "Enter server IP: ").strip() 157 | PORT = int(input(Fore.YELLOW + "Enter server port: ").strip()) 158 | 159 | chat_client = ChatClient(HOST, PORT) 160 | chat_client.run() 161 | --------------------------------------------------------------------------------