├── .gitignore ├── README.md ├── data\contacts.dat └── pyChat.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Swp files 2 | *.swp 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | #Python Chat program# 2 | 3 | A GUI chat program written in python 3. It is designed to not need a central server and so work very well on LANs that are not connected to the internet. One user starts the program in server mode, listening on a port of their choosing, and then other users connect to their machine. Each new user then acts as a host, allowing other people to connect to them. Encryption is done using Diffie-Hellman xor encrypting. Eventually some sort of real encryption will happen, but it is atleast semi-secured. It allows users to set nicknames, save chat history, and stores recent connections in addition to regular chatting. 4 | 5 | -------------------------------------------------------------------------------- /data\contacts.dat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chprice/Python-Chat-Program/da5fb76493747abeeb32d8c86c19a1f572f061b3/data\contacts.dat -------------------------------------------------------------------------------- /pyChat.py: -------------------------------------------------------------------------------- 1 | import sys 2 | if not sys.hexversion > 0x03000000: 3 | version = 2 4 | else: 5 | version = 3 6 | if len(sys.argv) > 1 and sys.argv[1] == "-cli": 7 | print("Starting command line chat") 8 | isCLI = True 9 | else: 10 | isCLI = False 11 | 12 | 13 | if version == 2: 14 | from Tkinter import * 15 | from tkFileDialog import asksaveasfilename 16 | if version == 3: 17 | from tkinter import * 18 | from tkinter.filedialog import asksaveasfilename 19 | import threading 20 | import socket 21 | import random 22 | import math 23 | 24 | 25 | # GLOBALS 26 | conn_array = [] # stores open sockets 27 | secret_array = dict() # key: the open sockets in conn_array, 28 | # value: integers for encryption 29 | username_array = dict() # key: the open sockets in conn_array, 30 | # value: usernames for the connection 31 | contact_array = dict() # key: ip address as a string, value: [port, username] 32 | 33 | username = "Self" 34 | 35 | location = 0 36 | port = 0 37 | top = "" 38 | 39 | main_body_text = 0 40 | #-GLOBALS- 41 | 42 | # So, 43 | # x_encode your message with the key, then pass that to 44 | # refract to get a string out of it. 45 | # To decrypt, pass the message back to x_encode, and then back to refract 46 | 47 | def binWord(word): 48 | """Converts the string into binary.""" 49 | master = "" 50 | for letter in word: 51 | temp = bin(ord(letter))[2:] 52 | while len(temp) < 7: 53 | temp = '0' + temp 54 | master = master + temp 55 | return master 56 | 57 | def xcrypt(message, key): 58 | """Encrypts the binary message by the binary key.""" 59 | count = 0 60 | master = "" 61 | for letter in message: 62 | if count == len(key): 63 | count = 0 64 | master += str(int(letter) ^ int(key[count])) 65 | count += 1 66 | return master 67 | 68 | def x_encode(string, number): 69 | """Encrypts the string by the number.""" 70 | return xcrypt(binWord(string), bin(number)[2:]) 71 | 72 | def refract(binary): 73 | """Returns the string representation of the binary. 74 | Has trouble with spaces. 75 | 76 | """ 77 | master = "" 78 | for x in range(0, int(len(binary) / 7)): 79 | master += chr(int(binary[x * 7: (x + 1) * 7], 2) + 0) 80 | return master 81 | 82 | 83 | def formatNumber(number): 84 | """Ensures that number is at least length 4 by 85 | adding extra 0s to the front. 86 | 87 | """ 88 | temp = str(number) 89 | while len(temp) < 4: 90 | temp = '0' + temp 91 | return temp 92 | 93 | def netThrow(conn, secret, message): 94 | """Sends message through the open socket conn with the encryption key 95 | secret. Sends the length of the incoming message, then sends the actual 96 | message. 97 | 98 | """ 99 | try: 100 | conn.send(formatNumber(len(x_encode(message, secret))).encode()) 101 | conn.send(x_encode(message, secret).encode()) 102 | except socket.error: 103 | if len(conn_array) != 0: 104 | writeToScreen( 105 | "Connection issue. Sending message failed.", "System") 106 | processFlag("-001") 107 | 108 | def netCatch(conn, secret): 109 | """Receive and return the message through open socket conn, decrypting 110 | using key secret. If the message length begins with - instead of a number, 111 | process as a flag and return 1. 112 | 113 | """ 114 | try: 115 | data = conn.recv(4) 116 | if data.decode()[0] == '-': 117 | processFlag(data.decode(), conn) 118 | return 1 119 | data = conn.recv(int(data.decode())) 120 | return refract(xcrypt(data.decode(), bin(secret)[2:])) 121 | except socket.error: 122 | if len(conn_array) != 0: 123 | writeToScreen( 124 | "Connection issue. Receiving message failed.", "System") 125 | processFlag("-001") 126 | 127 | def isPrime(number): 128 | """Checks to see if a number is prime.""" 129 | x = 1 130 | if number == 2 or number == 3: 131 | return True 132 | while x < math.sqrt(number): 133 | x += 1 134 | if number % x == 0: 135 | return False 136 | return True 137 | 138 | def processFlag(number, conn=None): 139 | """Process the flag corresponding to number, using open socket conn 140 | if necessary. 141 | 142 | """ 143 | global statusConnect 144 | global conn_array 145 | global secret_array 146 | global username_array 147 | global contact_array 148 | global isCLI 149 | t = int(number[1:]) 150 | if t == 1: # disconnect 151 | # in the event of single connection being left or if we're just a 152 | # client 153 | if len(conn_array) == 1: 154 | writeToScreen("Connection closed.", "System") 155 | dump = secret_array.pop(conn_array[0]) 156 | dump = conn_array.pop() 157 | try: 158 | dump.close() 159 | except socket.error: 160 | print("Issue with someone being bad about disconnecting") 161 | if not isCLI: 162 | statusConnect.set("Connect") 163 | connecter.config(state=NORMAL) 164 | return 165 | 166 | if conn != None: 167 | writeToScreen("Connect to " + conn.getsockname() 168 | [0] + " closed.", "System") 169 | dump = secret_array.pop(conn) 170 | conn_array.remove(conn) 171 | conn.close() 172 | 173 | if t == 2: # username change 174 | name = netCatch(conn, secret_array[conn]) 175 | if(isUsernameFree(name)): 176 | writeToScreen( 177 | "User " + username_array[conn] + " has changed their username to " + name, "System") 178 | username_array[conn] = name 179 | contact_array[ 180 | conn.getpeername()[0]] = [conn.getpeername()[1], name] 181 | 182 | # passing a friend who this should connect to (I am assuming it will be 183 | # running on the same port as the other session) 184 | if t == 4: 185 | data = conn.recv(4) 186 | data = conn.recv(int(data.decode())) 187 | Client(data.decode(), 188 | int(contact_array[conn.getpeername()[0]][0])).start() 189 | 190 | def processUserCommands(command, param): 191 | """Processes commands passed in via the / text input.""" 192 | global conn_array 193 | global secret_array 194 | global username 195 | 196 | if command == "nick": # change nickname 197 | for letter in param[0]: 198 | if letter == " " or letter == "\n": 199 | if isCLI: 200 | error_window(0, "Invalid username. No spaces allowed.") 201 | else: 202 | error_window(root, "Invalid username. No spaces allowed.") 203 | return 204 | if isUsernameFree(param[0]): 205 | writeToScreen("Username is being changed to " + param[0], "System") 206 | for conn in conn_array: 207 | conn.send("-002".encode()) 208 | netThrow(conn, secret_array[conn], param[0]) 209 | username = param[0] 210 | else: 211 | writeToScreen(param[0] + 212 | " is already taken as a username", "System") 213 | if command == "disconnect": # disconnects from current connection 214 | for conn in conn_array: 215 | conn.send("-001".encode()) 216 | processFlag("-001") 217 | if command == "connect": # connects to passed in host port 218 | if(options_sanitation(param[1], param[0])): 219 | Client(param[0], int(param[1])).start() 220 | if command == "host": # starts server on passed in port 221 | if(options_sanitation(param[0])): 222 | Server(int(param[0])).start() 223 | 224 | def isUsernameFree(name): 225 | """Checks to see if the username name is free for use.""" 226 | global username_array 227 | global username 228 | for conn in username_array: 229 | if name == username_array[conn] or name == username: 230 | return False 231 | return True 232 | 233 | def passFriends(conn): 234 | """Sends conn all of the people currently in conn_array so they can connect 235 | to them. 236 | 237 | """ 238 | global conn_array 239 | for connection in conn_array: 240 | if conn != connection: 241 | conn.send("-004".encode()) 242 | conn.send( 243 | formatNumber(len(connection.getpeername()[0])).encode()) # pass the ip address 244 | conn.send(connection.getpeername()[0].encode()) 245 | # conn.send(formatNumber(len(connection.getpeername()[1])).encode()) #pass the port number 246 | # conn.send(connection.getpeername()[1].encode()) 247 | 248 | #-------------------------------------------------------------------------- 249 | 250 | def client_options_window(master): 251 | """Launches client options window for getting destination hostname 252 | and port. 253 | 254 | """ 255 | top = Toplevel(master) 256 | top.title("Connection options") 257 | top.protocol("WM_DELETE_WINDOW", lambda: optionDelete(top)) 258 | top.grab_set() 259 | Label(top, text="Server IP:").grid(row=0) 260 | location = Entry(top) 261 | location.grid(row=0, column=1) 262 | location.focus_set() 263 | Label(top, text="Port:").grid(row=1) 264 | port = Entry(top) 265 | port.grid(row=1, column=1) 266 | go = Button(top, text="Connect", command=lambda: 267 | client_options_go(location.get(), port.get(), top)) 268 | go.grid(row=2, column=1) 269 | 270 | def client_options_go(dest, port, window): 271 | "Processes the options entered by the user in the client options window.""" 272 | if options_sanitation(port, dest): 273 | if not isCLI: 274 | window.destroy() 275 | Client(dest, int(port)).start() 276 | elif isCLI: 277 | sys.exit(1) 278 | 279 | def options_sanitation(por, loc=""): 280 | """Checks to make sure the port and destination ip are both valid. 281 | Launches error windows if there are any issues. 282 | 283 | """ 284 | global root 285 | if version == 2: 286 | por = unicode(por) 287 | if isCLI: 288 | root = 0 289 | if not por.isdigit(): 290 | error_window(root, "Please input a port number.") 291 | return False 292 | if int(por) < 0 or 65555 < int(por): 293 | error_window(root, "Please input a port number between 0 and 65555") 294 | return False 295 | if loc != "": 296 | if not ip_process(loc.split(".")): 297 | error_window(root, "Please input a valid ip address.") 298 | return False 299 | return True 300 | 301 | def ip_process(ipArray): 302 | """Checks to make sure every section of the ip is a valid number.""" 303 | if len(ipArray) != 4: 304 | return False 305 | for ip in ipArray: 306 | if version == 2: 307 | ip = unicode(ip) 308 | if not ip.isdigit(): 309 | return False 310 | t = int(ip) 311 | if t < 0 or 255 < t: 312 | return False 313 | return True 314 | 315 | #------------------------------------------------------------------------------ 316 | 317 | def server_options_window(master): 318 | """Launches server options window for getting port.""" 319 | top = Toplevel(master) 320 | top.title("Connection options") 321 | top.grab_set() 322 | top.protocol("WM_DELETE_WINDOW", lambda: optionDelete(top)) 323 | Label(top, text="Port:").grid(row=0) 324 | port = Entry(top) 325 | port.grid(row=0, column=1) 326 | port.focus_set() 327 | go = Button(top, text="Launch", command=lambda: 328 | server_options_go(port.get(), top)) 329 | go.grid(row=1, column=1) 330 | 331 | def server_options_go(port, window): 332 | """Processes the options entered by the user in the 333 | server options window. 334 | 335 | """ 336 | if options_sanitation(port): 337 | if not isCLI: 338 | window.destroy() 339 | Server(int(port)).start() 340 | elif isCLI: 341 | sys.exit(1) 342 | 343 | #------------------------------------------------------------------------- 344 | 345 | def username_options_window(master): 346 | """Launches username options window for setting username.""" 347 | top = Toplevel(master) 348 | top.title("Username options") 349 | top.grab_set() 350 | Label(top, text="Username:").grid(row=0) 351 | name = Entry(top) 352 | name.focus_set() 353 | name.grid(row=0, column=1) 354 | go = Button(top, text="Change", command=lambda: 355 | username_options_go(name.get(), top)) 356 | go.grid(row=1, column=1) 357 | 358 | 359 | def username_options_go(name, window): 360 | """Processes the options entered by the user in the 361 | server options window. 362 | 363 | """ 364 | processUserCommands("nick", [name]) 365 | window.destroy() 366 | 367 | #------------------------------------------------------------------------- 368 | 369 | def error_window(master, texty): 370 | """Launches a new window to display the message texty.""" 371 | global isCLI 372 | if isCLI: 373 | writeToScreen(texty, "System") 374 | else: 375 | window = Toplevel(master) 376 | window.title("ERROR") 377 | window.grab_set() 378 | Label(window, text=texty).pack() 379 | go = Button(window, text="OK", command=window.destroy) 380 | go.pack() 381 | go.focus_set() 382 | 383 | def optionDelete(window): 384 | connecter.config(state=NORMAL) 385 | window.destroy() 386 | 387 | #----------------------------------------------------------------------------- 388 | # Contacts window 389 | 390 | def contacts_window(master): 391 | """Displays the contacts window, allowing the user to select a recent 392 | connection to reuse. 393 | 394 | """ 395 | global contact_array 396 | cWindow = Toplevel(master) 397 | cWindow.title("Contacts") 398 | cWindow.grab_set() 399 | scrollbar = Scrollbar(cWindow, orient=VERTICAL) 400 | listbox = Listbox(cWindow, yscrollcommand=scrollbar.set) 401 | scrollbar.config(command=listbox.yview) 402 | scrollbar.pack(side=RIGHT, fill=Y) 403 | buttons = Frame(cWindow) 404 | cBut = Button(buttons, text="Connect", 405 | command=lambda: contacts_connect( 406 | listbox.get(ACTIVE).split(" "))) 407 | cBut.pack(side=LEFT) 408 | dBut = Button(buttons, text="Remove", 409 | command=lambda: contacts_remove( 410 | listbox.get(ACTIVE).split(" "), listbox)) 411 | dBut.pack(side=LEFT) 412 | aBut = Button(buttons, text="Add", 413 | command=lambda: contacts_add(listbox, cWindow)) 414 | aBut.pack(side=LEFT) 415 | buttons.pack(side=BOTTOM) 416 | 417 | for person in contact_array: 418 | listbox.insert(END, contact_array[person][1] + " " + 419 | person + " " + contact_array[person][0]) 420 | listbox.pack(side=LEFT, fill=BOTH, expand=1) 421 | 422 | def contacts_connect(item): 423 | """Establish a connection between two contacts.""" 424 | Client(item[1], int(item[2])).start() 425 | 426 | def contacts_remove(item, listbox): 427 | """Remove a contact.""" 428 | if listbox.size() != 0: 429 | listbox.delete(ACTIVE) 430 | global contact_array 431 | h = contact_array.pop(item[1]) 432 | 433 | 434 | def contacts_add(listbox, master): 435 | """Add a contact.""" 436 | aWindow = Toplevel(master) 437 | aWindow.title("Contact add") 438 | Label(aWindow, text="Username:").grid(row=0) 439 | name = Entry(aWindow) 440 | name.focus_set() 441 | name.grid(row=0, column=1) 442 | Label(aWindow, text="IP:").grid(row=1) 443 | ip = Entry(aWindow) 444 | ip.grid(row=1, column=1) 445 | Label(aWindow, text="Port:").grid(row=2) 446 | port = Entry(aWindow) 447 | port.grid(row=2, column=1) 448 | go = Button(aWindow, text="Add", command=lambda: 449 | contacts_add_helper(name.get(), ip.get(), port.get(), 450 | aWindow, listbox)) 451 | go.grid(row=3, column=1) 452 | 453 | 454 | def contacts_add_helper(username, ip, port, window, listbox): 455 | """Contact adding helper function. Recognizes invalid usernames and 456 | adds contact to listbox and contact_array. 457 | 458 | """ 459 | for letter in username: 460 | if letter == " " or letter == "\n": 461 | error_window(root, "Invalid username. No spaces allowed.") 462 | return 463 | if options_sanitation(port, ip): 464 | listbox.insert(END, username + " " + ip + " " + port) 465 | contact_array[ip] = [port, username] 466 | window.destroy() 467 | return 468 | 469 | def load_contacts(): 470 | """Loads the recent chats out of the persistent file contacts.dat.""" 471 | global contact_array 472 | try: 473 | filehandle = open("data\\contacts.dat", "r") 474 | except IOError: 475 | return 476 | line = filehandle.readline() 477 | while len(line) != 0: 478 | temp = (line.rstrip('\n')).split(" ") # format: ip, port, name 479 | contact_array[temp[0]] = temp[1:] 480 | line = filehandle.readline() 481 | filehandle.close() 482 | 483 | def dump_contacts(): 484 | """Saves the recent chats to the persistent file contacts.dat.""" 485 | global contact_array 486 | try: 487 | filehandle = open("data\\contacts.dat", "w") 488 | except IOError: 489 | print("Can't dump contacts.") 490 | return 491 | for contact in contact_array: 492 | filehandle.write( 493 | contact + " " + str(contact_array[contact][0]) + " " + 494 | contact_array[contact][1] + "\n") 495 | filehandle.close() 496 | 497 | #----------------------------------------------------------------------------- 498 | 499 | # places the text from the text bar on to the screen and sends it to 500 | # everyone this program is connected to 501 | def placeText(text): 502 | """Places the text from the text bar on to the screen and sends it to 503 | everyone this program is connected to. 504 | 505 | """ 506 | global conn_array 507 | global secret_array 508 | global username 509 | writeToScreen(text, username) 510 | for person in conn_array: 511 | netThrow(person, secret_array[person], text) 512 | 513 | def writeToScreen(text, username=""): 514 | """Places text to main text body in format "username: text".""" 515 | global main_body_text 516 | global isCLI 517 | if isCLI: 518 | if username: 519 | print(username + ": " + text) 520 | else: 521 | print(text) 522 | else: 523 | main_body_text.config(state=NORMAL) 524 | main_body_text.insert(END, '\n') 525 | if username: 526 | main_body_text.insert(END, username + ": ") 527 | main_body_text.insert(END, text) 528 | main_body_text.yview(END) 529 | main_body_text.config(state=DISABLED) 530 | 531 | def processUserText(event): 532 | """Takes text from text bar input and calls processUserCommands if it 533 | begins with '/'. 534 | 535 | """ 536 | data = text_input.get() 537 | if data[0] != "/": # is not a command 538 | placeText(data) 539 | else: 540 | if data.find(" ") == -1: 541 | command = data[1:] 542 | else: 543 | command = data[1:data.find(" ")] 544 | params = data[data.find(" ") + 1:].split(" ") 545 | processUserCommands(command, params) 546 | text_input.delete(0, END) 547 | 548 | 549 | def processUserInput(text): 550 | """ClI version of processUserText.""" 551 | if text[0] != "/": 552 | placeText(text) 553 | else: 554 | if text.find(" ") == -1: 555 | command = text[1:] 556 | else: 557 | command = text[1:text.find(" ")] 558 | params = text[text.find(" ") + 1:].split(" ") 559 | processUserCommands(command, params) 560 | 561 | 562 | #------------------------------------------------------------------------- 563 | 564 | class Server (threading.Thread): 565 | "A class for a Server instance.""" 566 | def __init__(self, port): 567 | threading.Thread.__init__(self) 568 | self.port = port 569 | 570 | def run(self): 571 | global conn_array 572 | s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 573 | s.bind(('', self.port)) 574 | 575 | if len(conn_array) == 0: 576 | writeToScreen( 577 | "Socket is good, waiting for connections on port: " + 578 | str(self.port), "System") 579 | s.listen(1) 580 | global conn_init 581 | conn_init, addr_init = s.accept() 582 | serv = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 583 | serv.bind(('', 0)) # get a random empty port 584 | serv.listen(1) 585 | 586 | portVal = str(serv.getsockname()[1]) 587 | if len(portVal) == 5: 588 | conn_init.send(portVal.encode()) 589 | else: 590 | conn_init.send(("0" + portVal).encode()) 591 | 592 | conn_init.close() 593 | conn, addr = serv.accept() 594 | conn_array.append(conn) # add an array entry for this connection 595 | writeToScreen("Connected by " + str(addr[0]), "System") 596 | 597 | global statusConnect 598 | statusConnect.set("Disconnect") 599 | connecter.config(state=NORMAL) 600 | 601 | # create the numbers for my encryption 602 | prime = random.randint(1000, 9000) 603 | while not isPrime(prime): 604 | prime = random.randint(1000, 9000) 605 | base = random.randint(20, 100) 606 | a = random.randint(20, 100) 607 | 608 | # send the numbers (base, prime, A) 609 | conn.send(formatNumber(len(str(base))).encode()) 610 | conn.send(str(base).encode()) 611 | 612 | conn.send(formatNumber(len(str(prime))).encode()) 613 | conn.send(str(prime).encode()) 614 | 615 | conn.send(formatNumber(len(str(pow(base, a) % prime))).encode()) 616 | conn.send(str(pow(base, a) % prime).encode()) 617 | 618 | # get B 619 | data = conn.recv(4) 620 | data = conn.recv(int(data.decode())) 621 | b = int(data.decode()) 622 | 623 | # calculate the encryption key 624 | global secret_array 625 | secret = pow(b, a) % prime 626 | # store the encryption key by the connection 627 | secret_array[conn] = secret 628 | 629 | conn.send(formatNumber(len(username)).encode()) 630 | conn.send(username.encode()) 631 | 632 | data = conn.recv(4) 633 | data = conn.recv(int(data.decode())) 634 | if data.decode() != "Self": 635 | username_array[conn] = data.decode() 636 | contact_array[str(addr[0])] = [str(self.port), data.decode()] 637 | else: 638 | username_array[conn] = addr[0] 639 | contact_array[str(addr[0])] = [str(self.port), "No_nick"] 640 | 641 | passFriends(conn) 642 | threading.Thread(target=Runner, args=(conn, secret)).start() 643 | Server(self.port).start() 644 | 645 | 646 | class Client (threading.Thread): 647 | """A class for a Client instance.""" 648 | def __init__(self, host, port): 649 | threading.Thread.__init__(self) 650 | self.port = port 651 | self.host = host 652 | 653 | def run(self): 654 | global conn_array 655 | global secret_array 656 | conn_init = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 657 | conn_init.settimeout(5.0) 658 | try: 659 | conn_init.connect((self.host, self.port)) 660 | except socket.timeout: 661 | writeToScreen("Timeout issue. Host possible not there.", "System") 662 | connecter.config(state=NORMAL) 663 | raise SystemExit(0) 664 | except socket.error: 665 | writeToScreen( 666 | "Connection issue. Host actively refused connection.", "System") 667 | connecter.config(state=NORMAL) 668 | raise SystemExit(0) 669 | porta = conn_init.recv(5) 670 | porte = int(porta.decode()) 671 | conn_init.close() 672 | conn = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 673 | conn.connect((self.host, porte)) 674 | 675 | writeToScreen("Connected to: " + self.host + 676 | " on port: " + str(porte), "System") 677 | 678 | global statusConnect 679 | statusConnect.set("Disconnect") 680 | connecter.config(state=NORMAL) 681 | 682 | conn_array.append(conn) 683 | # get my base, prime, and A values 684 | data = conn.recv(4) 685 | data = conn.recv(int(data.decode())) 686 | base = int(data.decode()) 687 | data = conn.recv(4) 688 | data = conn.recv(int(data.decode())) 689 | prime = int(data.decode()) 690 | data = conn.recv(4) 691 | data = conn.recv(int(data.decode())) 692 | a = int(data.decode()) 693 | b = random.randint(20, 100) 694 | # send the B value 695 | conn.send(formatNumber(len(str(pow(base, b) % prime))).encode()) 696 | conn.send(str(pow(base, b) % prime).encode()) 697 | secret = pow(a, b) % prime 698 | secret_array[conn] = secret 699 | 700 | conn.send(formatNumber(len(username)).encode()) 701 | conn.send(username.encode()) 702 | 703 | data = conn.recv(4) 704 | data = conn.recv(int(data.decode())) 705 | if data.decode() != "Self": 706 | username_array[conn] = data.decode() 707 | contact_array[ 708 | conn.getpeername()[0]] = [str(self.port), data.decode()] 709 | else: 710 | username_array[conn] = self.host 711 | contact_array[conn.getpeername()[0]] = [str(self.port), "No_nick"] 712 | threading.Thread(target=Runner, args=(conn, secret)).start() 713 | # Server(self.port).start() 714 | # ##########################################################################THIS 715 | # IS GOOD, BUT I CAN'T TEST ON ONE MACHINE 716 | 717 | def Runner(conn, secret): 718 | global username_array 719 | while 1: 720 | data = netCatch(conn, secret) 721 | if data != 1: 722 | writeToScreen(data, username_array[conn]) 723 | 724 | #------------------------------------------------------------------------- 725 | # Menu helpers 726 | 727 | def QuickClient(): 728 | """Menu window for connection options.""" 729 | window = Toplevel(root) 730 | window.title("Connection options") 731 | window.grab_set() 732 | Label(window, text="Server IP:").grid(row=0) 733 | destination = Entry(window) 734 | destination.grid(row=0, column=1) 735 | go = Button(window, text="Connect", command=lambda: 736 | client_options_go(destination.get(), "9999", window)) 737 | go.grid(row=1, column=1) 738 | 739 | 740 | def QuickServer(): 741 | """Quickstarts a server.""" 742 | Server(9999).start() 743 | 744 | def saveHistory(): 745 | """Saves history with Tkinter's asksaveasfilename dialog.""" 746 | global main_body_text 747 | file_name = asksaveasfilename( 748 | title="Choose save location", 749 | filetypes=[('Plain text', '*.txt'), ('Any File', '*.*')]) 750 | try: 751 | filehandle = open(file_name + ".txt", "w") 752 | except IOError: 753 | print("Can't save history.") 754 | return 755 | contents = main_body_text.get(1.0, END) 756 | for line in contents: 757 | filehandle.write(line) 758 | filehandle.close() 759 | 760 | 761 | def connects(clientType): 762 | global conn_array 763 | connecter.config(state=DISABLED) 764 | if len(conn_array) == 0: 765 | if clientType == 0: 766 | client_options_window(root) 767 | if clientType == 1: 768 | server_options_window(root) 769 | else: 770 | # connecter.config(state=NORMAL) 771 | for connection in conn_array: 772 | connection.send("-001".encode()) 773 | processFlag("-001") 774 | 775 | 776 | def toOne(): 777 | global clientType 778 | clientType = 0 779 | 780 | 781 | def toTwo(): 782 | global clientType 783 | clientType = 1 784 | 785 | 786 | #------------------------------------------------------------------------- 787 | 788 | 789 | if len(sys.argv) > 1 and sys.argv[1] == "-cli": 790 | print("Starting command line chat") 791 | 792 | else: 793 | root = Tk() 794 | root.title("Chat") 795 | 796 | menubar = Menu(root) 797 | 798 | file_menu = Menu(menubar, tearoff=0) 799 | file_menu.add_command(label="Save chat", command=lambda: saveHistory()) 800 | file_menu.add_command(label="Change username", 801 | command=lambda: username_options_window(root)) 802 | file_menu.add_command(label="Exit", command=lambda: root.destroy()) 803 | menubar.add_cascade(label="File", menu=file_menu) 804 | 805 | connection_menu = Menu(menubar, tearoff=0) 806 | connection_menu.add_command(label="Quick Connect", command=QuickClient) 807 | connection_menu.add_command( 808 | label="Connect on port", command=lambda: client_options_window(root)) 809 | connection_menu.add_command( 810 | label="Disconnect", command=lambda: processFlag("-001")) 811 | menubar.add_cascade(label="Connect", menu=connection_menu) 812 | 813 | server_menu = Menu(menubar, tearoff=0) 814 | server_menu.add_command(label="Launch server", command=QuickServer) 815 | server_menu.add_command(label="Listen on port", 816 | command=lambda: server_options_window(root)) 817 | menubar.add_cascade(label="Server", menu=server_menu) 818 | 819 | menubar.add_command(label="Contacts", command=lambda: 820 | contacts_window(root)) 821 | 822 | root.config(menu=menubar) 823 | 824 | main_body = Frame(root, height=20, width=50) 825 | 826 | main_body_text = Text(main_body) 827 | body_text_scroll = Scrollbar(main_body) 828 | main_body_text.focus_set() 829 | body_text_scroll.pack(side=RIGHT, fill=Y) 830 | main_body_text.pack(side=LEFT, fill=Y) 831 | body_text_scroll.config(command=main_body_text.yview) 832 | main_body_text.config(yscrollcommand=body_text_scroll.set) 833 | main_body.pack() 834 | 835 | main_body_text.insert(END, "Welcome to the chat program!") 836 | main_body_text.config(state=DISABLED) 837 | 838 | text_input = Entry(root, width=60) 839 | text_input.bind("", processUserText) 840 | text_input.pack() 841 | 842 | statusConnect = StringVar() 843 | statusConnect.set("Connect") 844 | clientType = 1 845 | Radiobutton(root, text="Client", variable=clientType, 846 | value=0, command=toOne).pack(anchor=E) 847 | Radiobutton(root, text="Server", variable=clientType, 848 | value=1, command=toTwo).pack(anchor=E) 849 | connecter = Button(root, textvariable=statusConnect, 850 | command=lambda: connects(clientType)) 851 | connecter.pack() 852 | 853 | load_contacts() 854 | 855 | #------------------------------------------------------------# 856 | 857 | root.mainloop() 858 | 859 | dump_contacts() 860 | --------------------------------------------------------------------------------