Advertisement
Guest User

Untitled

a guest
Feb 17th, 2019
92
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 23.81 KB | None | 0 0
  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("Lancement du système de 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. master = ""
  77. for x in range(0, int(len(binary) / 7)):
  78. master += chr(int(binary[x * 7: (x + 1) * 7], 2) + 0)
  79. return master
  80.  
  81.  
  82. def formatNumber(number):
  83. """Ensures that number is at least length 4 by
  84. adding extra 0s to the front.
  85. """
  86. temp = str(number)
  87. while len(temp) < 4:
  88. temp = '0' + temp
  89. return temp
  90.  
  91. def netThrow(conn, secret, message):
  92. """Sends message through the open socket conn with the encryption key
  93. secret. Sends the length of the incoming message, then sends the actual
  94. message.
  95. """
  96. try:
  97. conn.send(formatNumber(len(x_encode(message, secret))).encode())
  98. conn.send(x_encode(message, secret).encode())
  99. except socket.error:
  100. if len(conn_array) != 0:
  101. writeToScreen(
  102. "Connection issue. Sending message failed.", "System")
  103. processFlag("-001")
  104.  
  105. def netCatch(conn, secret):
  106. """Receive and return the message through open socket conn, decrypting
  107. using key secret. If the message length begins with - instead of a number,
  108. process as a flag and return 1.
  109. """
  110. try:
  111. data = conn.recv(4)
  112. if data.decode()[0] == '-':
  113. processFlag(data.decode(), conn)
  114. return 1
  115. data = conn.recv(int(data.decode()))
  116. return refract(xcrypt(data.decode(), bin(secret)[2:]))
  117. except socket.error:
  118. if len(conn_array) != 0:
  119. writeToScreen(
  120. "Connection issue. Receiving message failed.", "System")
  121. processFlag("-001")
  122.  
  123. def isPrime(number):
  124. """Checks to see if a number is prime."""
  125. x = 1
  126. if number == 2 or number == 3:
  127. return True
  128. while x < math.sqrt(number):
  129. x += 1
  130. if number % x == 0:
  131. return False
  132. return True
  133.  
  134. def processFlag(number, conn=None):
  135. """Process the flag corresponding to number, using open socket conn
  136. if necessary.
  137. """
  138. global statusConnect
  139. global conn_array
  140. global secret_array
  141. global username_array
  142. global contact_array
  143. global isCLI
  144. t = int(number[1:])
  145. if t == 1: # disconnect
  146. # in the event of single connection being left or if we're just a
  147. # client
  148. if len(conn_array) == 1:
  149. writeToScreen("Connection fermée", "System")
  150. dump = secret_array.pop(conn_array[0])
  151. dump = conn_array.pop()
  152. try:
  153. dump.close()
  154. except socket.error:
  155. print("Issue with someone being bad about disconnecting")
  156. if not isCLI:
  157. statusConnect.set("Connect")
  158. connecter.config(state=NORMAL)
  159. return
  160.  
  161. if conn != None:
  162. writeToScreen("Connect to " + conn.getsockname()
  163. [0] + " closed.", "System")
  164. dump = secret_array.pop(conn)
  165. conn_array.remove(conn)
  166. conn.close()
  167.  
  168. if t == 2: # username change
  169. name = netCatch(conn, secret_array[conn])
  170. if(isUsernameFree(name)):
  171. writeToScreen(
  172. "User " + username_array[conn] + " has changed their username to " + name, "System")
  173. username_array[conn] = name
  174. contact_array[
  175. conn.getpeername()[0]] = [conn.getpeername()[1], name]
  176.  
  177. # passing a friend who this should connect to (I am assuming it will be
  178. # running on the same port as the other session)
  179. if t == 4:
  180. data = conn.recv(4)
  181. data = conn.recv(int(data.decode()))
  182. Client(data.decode(),
  183. int(contact_array[conn.getpeername()[0]][0])).start()
  184.  
  185. def processUserCommands(command, param):
  186. """Processes commands passed in via the / text input."""
  187. global conn_array
  188. global secret_array
  189. global username
  190.  
  191. if command == "nick": # change nickname
  192. for letter in param[0]:
  193. if letter == " " or letter == "\n":
  194. if isCLI:
  195. error_window(0, "Invalid username. No spaces allowed.")
  196. else:
  197. error_window(root, "Invalid username. No spaces allowed.")
  198. return
  199. if isUsernameFree(param[0]):
  200. writeToScreen("Username is being changed to " + param[0], "System")
  201. for conn in conn_array:
  202. conn.send("-002".encode())
  203. netThrow(conn, secret_array[conn], param[0])
  204. username = param[0]
  205. else:
  206. writeToScreen(param[0] +
  207. " is already taken as a username", "System")
  208. if command == "disconnect": # disconnects from current connection
  209. for conn in conn_array:
  210. conn.send("-001".encode())
  211. processFlag("-001")
  212. if command == "connect": # connects to passed in host port
  213. if(options_sanitation(param[1], param[0])):
  214. Client(param[0], int(param[1])).start()
  215. if command == "host": # starts server on passed in port
  216. if(options_sanitation(param[0])):
  217. Server(int(param[0])).start()
  218.  
  219. def isUsernameFree(name):
  220. """Checks to see if the username name is free for use."""
  221. global username_array
  222. global username
  223. for conn in username_array:
  224. if name == username_array[conn] or name == username:
  225. return False
  226. return True
  227.  
  228. def passFriends(conn):
  229. """Sends conn all of the people currently in conn_array so they can connect
  230. to them.
  231. """
  232. global conn_array
  233. for connection in conn_array:
  234. if conn != connection:
  235. conn.send("-004".encode())
  236. conn.send(
  237. formatNumber(len(connection.getpeername()[0])).encode()) # pass the ip address
  238. conn.send(connection.getpeername()[0].encode())
  239. # conn.send(formatNumber(len(connection.getpeername()[1])).encode()) #pass the port number
  240. # conn.send(connection.getpeername()[1].encode())
  241.  
  242. #--------------------------------------------------------------------------
  243.  
  244. def client_options_window(master):
  245. """Launches client options window for getting destination hostname
  246. and port.
  247. """
  248. top = Toplevel(master)
  249. top.title("Connection options")
  250. top.protocol("WM_DELETE_WINDOW", lambda: optionDelete(top))
  251. top.grab_set()
  252. Label(top, text="Server IP:").grid(row=0)
  253. location = Entry(top)
  254. location.grid(row=0, column=1)
  255. location.focus_set()
  256. Label(top, text="Port:").grid(row=1)
  257. port = Entry(top)
  258. port.grid(row=1, column=1)
  259. go = Button(top, text="Connect", command=lambda:
  260. client_options_go(location.get(), port.get(), top))
  261. go.grid(row=2, column=1)
  262.  
  263. def client_options_go(dest, port, window):
  264. "Processes the options entered by the user in the client options window."""
  265. if options_sanitation(port, dest):
  266. if not isCLI:
  267. window.destroy()
  268. Client(dest, int(port)).start()
  269. elif isCLI:
  270. sys.exit(1)
  271.  
  272. def options_sanitation(por, loc=""):
  273. """Checks to make sure the port and destination ip are both valid.
  274. Launches error windows if there are any issues.
  275. """
  276. global root
  277. if version == 2:
  278. por = unicode(por)
  279. if isCLI:
  280. root = 0
  281. if not por.isdigit():
  282. error_window(root, "Veuillez rentrer un port valide.")
  283. return False
  284. if int(por) < 0 or 65555 < int(por):
  285. error_window(root, "Please input a port number between 0 and 65555")
  286. return False
  287. if loc != "":
  288. if not ip_process(loc.split(".")):
  289. error_window(root, "Please input a valid ip address.")
  290. return False
  291. return True
  292.  
  293. def ip_process(ipArray):
  294. """Checks to make sure every section of the ip is a valid number."""
  295. if len(ipArray) != 4:
  296. return False
  297. for ip in ipArray:
  298. if version == 2:
  299. ip = unicode(ip)
  300. if not ip.isdigit():
  301. return False
  302. t = int(ip)
  303. if t < 0 or 255 < t:
  304. return False
  305. return True
  306.  
  307. #------------------------------------------------------------------------------
  308.  
  309. def server_options_window(master):
  310. """Launches server options window for getting port."""
  311. top = Toplevel(master)
  312. top.title("Connection options")
  313. top.grab_set()
  314. top.protocol("WM_DELETE_WINDOW", lambda: optionDelete(top))
  315. Label(top, text="Port:").grid(row=0)
  316. port = Entry(top)
  317. port.grid(row=0, column=1)
  318. port.focus_set()
  319. go = Button(top, text="Launch", command=lambda:
  320. server_options_go(port.get(), top))
  321. go.grid(row=1, column=1)
  322.  
  323. def server_options_go(port, window):
  324. """Processes the options entered by the user in the
  325. server options window.
  326. """
  327. if options_sanitation(port):
  328. if not isCLI:
  329. window.destroy()
  330. Server(int(port)).start()
  331. elif isCLI:
  332. sys.exit(1)
  333.  
  334. #-------------------------------------------------------------------------
  335.  
  336. def username_options_window(master):
  337. """Launches username options window for setting username."""
  338. top = Toplevel(master)
  339. top.title("Username options")
  340. top.grab_set()
  341. Label(top, text="Username:").grid(row=0)
  342. name = Entry(top)
  343. name.focus_set()
  344. name.grid(row=0, column=1)
  345. go = Button(top, text="Change", command=lambda:
  346. username_options_go(name.get(), top))
  347. go.grid(row=1, column=1)
  348.  
  349.  
  350. def username_options_go(name, window):
  351. """Processes the options entered by the user in the
  352. server options window.
  353. """
  354. processUserCommands("nick", [name])
  355. window.destroy()
  356.  
  357. #-------------------------------------------------------------------------
  358.  
  359. def error_window(master, texty):
  360. """Launches a new window to display the message texty."""
  361. global isCLI
  362. if isCLI:
  363. writeToScreen(texty, "System")
  364. else:
  365. window = Toplevel(master)
  366. window.title("ERROR")
  367. window.grab_set()
  368. Label(window, text=texty).pack()
  369. go = Button(window, text="OK", command=window.destroy)
  370. go.pack()
  371. go.focus_set()
  372.  
  373. def optionDelete(window):
  374. connecter.config(state=NORMAL)
  375. window.destroy()
  376.  
  377.  
  378. # places the text from the text bar on to the screen and sends it to
  379. # everyone this program is connected to
  380. def placeText(text):
  381. """Places the text from the text bar on to the screen and sends it to
  382. everyone this program is connected to.
  383. """
  384. global conn_array
  385. global secret_array
  386. global username
  387. writeToScreen(text, username)
  388. for person in conn_array:
  389. netThrow(person, secret_array[person], text)
  390.  
  391. def writeToScreen(text, username=""):
  392. """Places text to main text body in format "username: text"."""
  393. global main_body_text
  394. global isCLI
  395. if isCLI:
  396. if username:
  397. print(username + ": " + text)
  398. else:
  399. print(text)
  400. else:
  401. main_body_text.config(state=NORMAL)
  402. main_body_text.insert(END, '\n')
  403. if username:
  404. main_body_text.insert(END, username + ": ")
  405. main_body_text.insert(END, text)
  406. main_body_text.yview(END)
  407. main_body_text.config(state=DISABLED)
  408.  
  409. def processUserText(event):
  410. """Takes text from text bar input and calls processUserCommands if it
  411. begins with '/'.
  412. """
  413. data = text_input.get()
  414. if data[0] != "/": # is not a command
  415. placeText(data)
  416. else:
  417. if data.find(" ") == -1:
  418. command = data[1:]
  419. else:
  420. command = data[1:data.find(" ")]
  421. params = data[data.find(" ") + 1:].split(" ")
  422. processUserCommands(command, params)
  423. text_input.delete(0, END)
  424.  
  425.  
  426. def processUserInput(text):
  427. """ClI version of processUserText."""
  428. if text[0] != "/":
  429. placeText(text)
  430. else:
  431. if text.find(" ") == -1:
  432. command = text[1:]
  433. else:
  434. command = text[1:text.find(" ")]
  435. params = text[text.find(" ") + 1:].split(" ")
  436. processUserCommands(command, params)
  437.  
  438.  
  439. #-------------------------------------------------------------------------
  440.  
  441. class Server (threading.Thread):
  442. "A class for a Server instance."""
  443. def __init__(self, port):
  444. threading.Thread.__init__(self)
  445. self.port = port
  446.  
  447. def run(self):
  448. global conn_array
  449. s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  450. s.bind(('', self.port))
  451.  
  452. if len(conn_array) == 0:
  453. writeToScreen(
  454. "Socket is good, waiting for connections on port: " +
  455. str(self.port), "System")
  456. s.listen(1)
  457. global conn_init
  458. conn_init, addr_init = s.accept()
  459. serv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  460. serv.bind(('', 0)) # get a random empty port
  461. serv.listen(1)
  462.  
  463. portVal = str(serv.getsockname()[1])
  464. if len(portVal) == 5:
  465. conn_init.send(portVal.encode())
  466. else:
  467. conn_init.send(("0" + portVal).encode())
  468.  
  469. conn_init.close()
  470. conn, addr = serv.accept()
  471. conn_array.append(conn) # add an array entry for this connection
  472. writeToScreen("Connected by " + str(addr[0]), "System")
  473.  
  474. global statusConnect
  475. statusConnect.set("Disconnect")
  476. connecter.config(state=NORMAL)
  477.  
  478. # create the numbers for my encryption
  479. prime = random.randint(1000, 9000)
  480. while not isPrime(prime):
  481. prime = random.randint(1000, 9000)
  482. base = random.randint(20, 100)
  483. a = random.randint(20, 100)
  484.  
  485. # send the numbers (base, prime, A)
  486. conn.send(formatNumber(len(str(base))).encode())
  487. conn.send(str(base).encode())
  488.  
  489. conn.send(formatNumber(len(str(prime))).encode())
  490. conn.send(str(prime).encode())
  491.  
  492. conn.send(formatNumber(len(str(pow(base, a) % prime))).encode())
  493. conn.send(str(pow(base, a) % prime).encode())
  494.  
  495. # get B
  496. data = conn.recv(4)
  497. data = conn.recv(int(data.decode()))
  498. b = int(data.decode())
  499.  
  500. # calculate the encryption key
  501. global secret_array
  502. secret = pow(b, a) % prime
  503. # store the encryption key by the connection
  504. secret_array[conn] = secret
  505.  
  506. conn.send(formatNumber(len(username)).encode())
  507. conn.send(username.encode())
  508.  
  509. data = conn.recv(4)
  510. data = conn.recv(int(data.decode()))
  511. if data.decode() != "Self":
  512. username_array[conn] = data.decode()
  513. contact_array[str(addr[0])] = [str(self.port), data.decode()]
  514. else:
  515. username_array[conn] = addr[0]
  516. contact_array[str(addr[0])] = [str(self.port), "No_nick"]
  517.  
  518. passFriends(conn)
  519. threading.Thread(target=Runner, args=(conn, secret)).start()
  520. Server(self.port).start()
  521.  
  522.  
  523. class Client (threading.Thread):
  524. """A class for a Client instance."""
  525. def __init__(self, host, port):
  526. threading.Thread.__init__(self)
  527. self.port = port
  528. self.host = host
  529.  
  530. def run(self):
  531. global conn_array
  532. global secret_array
  533. conn_init = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  534. conn_init.settimeout(5.0)
  535. try:
  536. conn_init.connect((self.host, self.port))
  537. except socket.timeout:
  538. writeToScreen("Timeout issue. Host possible not there.", "System")
  539. connecter.config(state=NORMAL)
  540. raise SystemExit(0)
  541. except socket.error:
  542. writeToScreen(
  543. "Connection issue. Host actively refused connection.", "System")
  544. connecter.config(state=NORMAL)
  545. raise SystemExit(0)
  546. porta = conn_init.recv(5)
  547. porte = int(porta.decode())
  548. conn_init.close()
  549. conn = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  550. conn.connect((self.host, porte))
  551.  
  552. writeToScreen("Connected to: " + self.host +
  553. " on port: " + str(porte), "System")
  554.  
  555. global statusConnect
  556. statusConnect.set("Disconnect")
  557. connecter.config(state=NORMAL)
  558.  
  559. conn_array.append(conn)
  560. # get my base, prime, and A values
  561. data = conn.recv(4)
  562. data = conn.recv(int(data.decode()))
  563. base = int(data.decode())
  564. data = conn.recv(4)
  565. data = conn.recv(int(data.decode()))
  566. prime = int(data.decode())
  567. data = conn.recv(4)
  568. data = conn.recv(int(data.decode()))
  569. a = int(data.decode())
  570. b = random.randint(20, 100)
  571. # send the B value
  572. conn.send(formatNumber(len(str(pow(base, b) % prime))).encode())
  573. conn.send(str(pow(base, b) % prime).encode())
  574. secret = pow(a, b) % prime
  575. secret_array[conn] = secret
  576.  
  577. conn.send(formatNumber(len(username)).encode())
  578. conn.send(username.encode())
  579.  
  580. data = conn.recv(4)
  581. data = conn.recv(int(data.decode()))
  582. if data.decode() != "Self":
  583. username_array[conn] = data.decode()
  584. contact_array[
  585. conn.getpeername()[0]] = [str(self.port), data.decode()]
  586. else:
  587. username_array[conn] = self.host
  588. contact_array[conn.getpeername()[0]] = [str(self.port), "No_nick"]
  589. threading.Thread(target=Runner, args=(conn, secret)).start()
  590. # Server(self.port).start()
  591. # ##########################################################################THIS
  592. # IS GOOD, BUT I CAN'T TEST ON ONE MACHINE
  593.  
  594. def Runner(conn, secret):
  595. global username_array
  596. while 1:
  597. data = netCatch(conn, secret)
  598. if data != 1:
  599. writeToScreen(data, username_array[conn])
  600.  
  601. #-------------------------------------------------------------------------
  602. # Menu helpers
  603.  
  604. def QuickClient():
  605. """Menu window for connection options."""
  606. window = Toplevel(root)
  607. window.title("Connection options")
  608. window.grab_set()
  609. Label(window, text="Server IP:").grid(row=0)
  610. destination = Entry(window)
  611. destination.grid(row=0, column=1)
  612. go = Button(window, text="Connect", command=lambda:
  613. client_options_go(destination.get(), "9999", window))
  614. go.grid(row=1, column=1)
  615.  
  616.  
  617. def QuickServer():
  618. """Quickstarts a server."""
  619. Server(9999).start()
  620.  
  621. def saveHistory():
  622. """Saves history with Tkinter's asksaveasfilename dialog."""
  623. global main_body_text
  624. file_name = asksaveasfilename(
  625. title="Choose save location",
  626. filetypes=[('Plain text', '*.txt'), ('Any File', '*.*')])
  627. try:
  628. filehandle = open(file_name + ".txt", "w")
  629. except IOError:
  630. print("Can't save history.")
  631. return
  632. contents = main_body_text.get(1.0, END)
  633. for line in contents:
  634. filehandle.write(line)
  635. filehandle.close()
  636.  
  637.  
  638. def connects(clientType):
  639. global conn_array
  640. connecter.config(state=DISABLED)
  641. if len(conn_array) == 0:
  642. if clientType == 0:
  643. client_options_window(root)
  644. if clientType == 1:
  645. server_options_window(root)
  646. else:
  647. # connecter.config(state=NORMAL)
  648. for connection in conn_array:
  649. connection.send("-001".encode())
  650. processFlag("-001")
  651.  
  652.  
  653. def toOne():
  654. global clientType
  655. clientType = 0
  656.  
  657.  
  658. def toTwo():
  659. global clientType
  660. clientType = 1
  661.  
  662.  
  663. #-------------------------------------------------------------------------
  664.  
  665.  
  666. if len(sys.argv) > 1 and sys.argv[1] == "-cli":
  667. print("Starting command line chat")
  668.  
  669. else:
  670. root = Tk()
  671. root.title("Chat")
  672.  
  673. menubar = Menu(root)
  674.  
  675. file_menu = Menu(menubar, tearoff=0)
  676. file_menu.add_command(label="Save chat", command=lambda: saveHistory())
  677. file_menu.add_command(label="Change username",
  678. command=lambda: username_options_window(root))
  679. file_menu.add_command(label="Exit", command=lambda: root.destroy())
  680. menubar.add_cascade(label="File", menu=file_menu)
  681.  
  682. connection_menu = Menu(menubar, tearoff=0)
  683. connection_menu.add_command(label="Quick Connect", command=QuickClient)
  684. connection_menu.add_command(
  685. label="Connect on port", command=lambda: client_options_window(root))
  686. connection_menu.add_command(
  687. label="Disconnect", command=lambda: processFlag("-001"))
  688. menubar.add_cascade(label="Connect", menu=connection_menu)
  689.  
  690. server_menu = Menu(menubar, tearoff=0)
  691. server_menu.add_command(label="Launch server", command=QuickServer)
  692. server_menu.add_command(label="Listen on port",
  693. command=lambda: server_options_window(root))
  694. menubar.add_cascade(label="Server", menu=server_menu)
  695.  
  696.  
  697. root.config(menu=menubar)
  698.  
  699. main_body = Frame(root, height=20, width=50)
  700.  
  701. main_body_text = Text(main_body)
  702. body_text_scroll = Scrollbar(main_body)
  703. main_body_text.focus_set()
  704. body_text_scroll.pack(side=RIGHT, fill=Y)
  705. main_body_text.pack(side=LEFT, fill=Y)
  706. body_text_scroll.config(command=main_body_text.yview)
  707. main_body_text.config(yscrollcommand=body_text_scroll.set)
  708. main_body.pack()
  709.  
  710. main_body_text.insert(END, "Hello World ! | Bienvenue dans le système de chat textuel en peer2peer.")
  711. main_body_text.config(state=DISABLED)
  712.  
  713. text_input = Entry(root, width=60)
  714. text_input.bind("<Return>", processUserText)
  715. text_input.pack()
  716.  
  717. statusConnect = StringVar()
  718. statusConnect.set("Connect")
  719. clientType = 1
  720. Radiobutton(root, text="Client", variable=clientType,
  721. value=0, command=toOne).pack(anchor=E)
  722. Radiobutton(root, text="Server", variable=clientType,
  723. value=1, command=toTwo).pack(anchor=E)
  724. connecter = Button(root, textvariable=statusConnect,
  725. command=lambda: connects(clientType))
  726. connecter.pack()
  727.  
  728. load_contacts()
  729.  
  730. #------------------------------------------------------------#
  731.  
  732. root.mainloop()
  733.  
  734. dump_contacts()
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement