From bae244803c18f394737fdb0fbe42dbd7563fef24 Mon Sep 17 00:00:00 2001 From: sloumdrone Date: Tue, 23 Oct 2018 22:32:26 -0700 Subject: [PATCH] Started reconfiguring of classes, buggy but semifunctional output is go --- burrow.py | 13 ++ conn.py | 69 +++++----- connect.py | 25 ++++ go.config.json | 2 +- gui.py | 349 +++++++++++++++++++++++++++++++++++++++++++++++++ parser.py | 54 ++++++++ 6 files changed, 475 insertions(+), 37 deletions(-) create mode 100644 burrow.py create mode 100644 connect.py create mode 100644 gui.py create mode 100644 parser.py diff --git a/burrow.py b/burrow.py new file mode 100644 index 0000000..8770d7d --- /dev/null +++ b/burrow.py @@ -0,0 +1,13 @@ +#!/usr/bin/env python3 +################################################ +## Burrow, a gopher client/browser +## - - - - - - - - - - - - - - - - - - - - - - - +## Version 0.2.1 +################################################ + +from gui import GUI + + +if __name__ == '__main__': + app = GUI() + app.root.mainloop() diff --git a/conn.py b/conn.py index 23d19f7..c701b56 100644 --- a/conn.py +++ b/conn.py @@ -28,18 +28,15 @@ class Tunnel: def make_connection(self, resource, host, itemtype, port=70): - endline = '\r\n' s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.settimeout(30.0) - try: - port = int(port) - except: - port = 70 s.connect((host, port)) s.sendall((resource + '\r\n').encode('utf-8')) - r = s.makefile(mode = 'r', errors='ignore') + + req = s.makefile(mode = 'r', errors='ignore') + try: - raw_data = r.read() + raw_data = req.read() except UnicodeDecodeError: raw_data = 'iError decoding server response :(\tfalse\tnull.host\t1' @@ -62,39 +59,39 @@ class Tunnel: - def gopher_to_text(self, message): - message = message.split('\n') - message = [x.split('\t') for x in message] - message = [{'type': x[0][0], 'description': x[0][1:], 'resource': x[1], 'host': x[2], 'port': x[3]} for x in message if len(x) >= 4] - return message + # def gopher_to_text(self, message): + # message = message.split('\n') + # message = [x.split('\t') for x in message] + # message = [{'type': x[0][0], 'description': x[0][1:], 'resource': x[1], 'host': x[2], 'port': x[3]} for x in message if len(x) >= 4] + # return message - def parse_url(self, url, execute=True): - regex = r'^(?P(?:gopher:\/\/)?)?(?P[\w\.\d]+)(?P(?::\d+)?)?(?P(?:\/\d)?)?(?P(?:\/[\w\/\d\-?.]*)?)?$' - match = re.match(regex, url) - protocol = match.group('protocol') - itemtype = match.group('type') - host = match.group('host') - port = match.group('port') - resource = match.group('resource') + # def parse_url(self, url, execute=True): + # regex = r'^(?P(?:gopher:\/\/)?)?(?P[\w\.\d]+)(?P(?::\d+)?)?(?P(?:\/\d)?)?(?P(?:\/[\w\/\d\-?.]*)?)?$' + # match = re.match(regex, url) + # protocol = match.group('protocol') + # itemtype = match.group('type') + # host = match.group('host') + # port = match.group('port') + # resource = match.group('resource') - if protocol != 'gopher://' and protocol: - return False - if itemtype and not self.types[itemtype[1]]: - return False - elif not itemtype: - itemtype = '/1' - if not host: - return False - if not resource: - resource = '/' - if port: - port = port[1:] + # if protocol != 'gopher://' and protocol: + # return False + # if itemtype and not self.types[itemtype[1]]: + # return False + # elif not itemtype: + # itemtype = '/1' + # if not host: + # return False + # if not resource: + # resource = '/' + # if port: + # port = port[1:] - if execute: - self.make_connection(resource, host, itemtype, port) - else: - return {'host': host, 'resource': resource, 'type': itemtype} + # if execute: + # self.make_connection(resource, host, itemtype, port) + # else: + # return {'host': host, 'resource': resource, 'type': itemtype} if __name__ == '__main__': diff --git a/connect.py b/connect.py new file mode 100644 index 0000000..bafe28a --- /dev/null +++ b/connect.py @@ -0,0 +1,25 @@ +import socket + +class connect: + def __init__(self): + self.raw_response = None + self.filetype = None + + def request(self, resource, host, itemtype, port=70): + #connects to server and returns list with response type and body + socket_conn = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + socket_conn.settimeout(20.0) + socket_conn.connect((host, port)) + socket_conn.sendall((resource + '\r\n').encode('utf-8')) + + response = socket_conn.makefile(mode = 'r', errors = 'ignore') + try: + self.raw_response = response.read() + self.filetype = itemtype + except UnicodeDecodeError: + self.raw_response = '3Error decoding server response\tfalse\tnull.host\t1' + self.filetype = '3' + + socket_conn.close() + + return {'type': self.filetype, 'body': self.raw_response} diff --git a/go.config.json b/go.config.json index 879e5ab..89f1b31 100644 --- a/go.config.json +++ b/go.config.json @@ -1 +1 @@ -{"favorites": [], "last_viewed": "gopher://gopher.club:70/1/phlogs/"} \ No newline at end of file +{"favorites": [], "last_viewed": "gopher://gopher.floodgap.com:70/1/"} \ No newline at end of file diff --git a/gui.py b/gui.py new file mode 100644 index 0000000..5648cde --- /dev/null +++ b/gui.py @@ -0,0 +1,349 @@ +#!/usr/bin/env python3 + +import tkinter as tk +from connect import connect as conn +from parser import parser +import time +import sys +import json +import os.path + +class GUI: + def __init__(self): + self.history = [] + self.history_location = -1 + self.message_bar_content = '' + self.config = None + self.read_config() + self.conn = conn() + self.parser = parser() + + #colors + self.FG = 'black' + self.BG = 'white' + self.LINK = 'blue' + self.ACTIVELINK = 'red' + self.HLB = '#e37800' + self.HLF = 'white' + self.STATUS = 'silver' + self.ERROR = 'red' + + #create and configure root window + self.root = tk.Tk(className='Digger') + self.root.title('Digger') + self.root.geometry("1200x800") + self.add_assets() + + #main frame objects + self.top_bar = tk.Frame(self.root, padx=10, height=70, relief=tk.FLAT, bd=2) + self.body = tk.Frame(self.root, relief=tk.RIDGE, bd=2) + self.status_bar = tk.Frame(self.root, height="20", bg=self.STATUS, takefocus=0) + + #top bar objects + self.btn_back = tk.Button(self.top_bar, image=self.img_back, bd=0, highlightthickness=0, takefocus=1) + self.btn_forward = tk.Button(self.top_bar,image=self.img_forward, bd=0, highlightthickness=0) + self.btn_favorite = tk.Button(self.top_bar,image=self.img_favorite, bd=0, highlightthickness=0) + self.btn_home = tk.Button(self.top_bar, image=self.img_home, bd=0, highlightthickness=0) + self.entry_url = tk.Entry(self.top_bar, selectbackground=self.HLB, selectforeground=self.HLF, highlightcolor=self.HLB) + self.btn_menu = tk.Button(self.top_bar, image=self.img_menu, bd=0, highlightthickness=0) + + #body objects + self.scroll_bar = tk.Scrollbar(self.body) + self.site_display = tk.Text(self.body, bg=self.BG, foreground=self.FG, padx=20, pady=20, wrap=tk.WORD, state=tk.DISABLED, spacing2=5, spacing1=5, yscrollcommand=self.scroll_bar.set) + self.scroll_bar.config(command=self.site_display.yview, width=20, relief=tk.RIDGE) + self.site_display.tag_configure('linkcolor', foreground=self.LINK, spacing1=5) + self.site_display.tag_configure('type_tag', background=self.FG, foreground=self.BG, spacing2=0, spacing1=0) + self.site_display.tag_configure('error_text', foreground=self.ERROR, spacing1=5, spacing2=5, spacing3=5) + + #status bar objects + self.status_info = tk.Label(self.status_bar, textvariable=self.message_bar_content, bg=self.STATUS, takefocus=0) + + self.pack_geometry() + self.add_status_titles() + self.add_event_listeners() + + #load the home screen + self.load_home_screen() + + #-----------Start GUI configuration----------------------- + + def add_event_listeners(self): + buttons = [ + self.btn_back, + self.btn_forward, + self.btn_favorite, + self.btn_home, + self.btn_menu + ] + + for x in buttons: + x.bind('', self.update_status) + x.bind('', self.clear_status) + self.entry_url.bind('', self.execute_address) + self.btn_back.bind('', self.go_back) + self.btn_forward.bind('', self.go_forward) + self.btn_home.bind('', self.load_home_screen) + self.site_display.bind("", lambda event: self.site_display.yview_scroll(-1, 'units')) + self.site_display.bind("", lambda event: self.site_display.yview_scroll(1, 'units')) + self.site_display.bind("", lambda event: self.site_display.focus_set()) + self.entry_url.bind("", lambda event: self.entry_url.focus_set()) + self.root.protocol('WM_DELETE_WINDOW', self.close_window) + + + def pack_geometry(self): + self.top_bar.pack(expand=False,fill=tk.BOTH,side=tk.TOP,anchor=tk.NW) + self.top_bar.pack_propagate(False) + self.body.pack(expand=True,fill=tk.BOTH,side=tk.TOP) + self.status_bar.pack(expand=False,fill=tk.X,side=tk.TOP,anchor=tk.SW) + self.btn_back.pack(side=tk.LEFT, padx=(0,20)) + self.btn_forward.pack(side=tk.LEFT, padx=(0,20)) + self.btn_home.pack(side=tk.LEFT, padx=(0,20)) + self.entry_url.pack(side=tk.LEFT, fill=tk.X, expand=True, ipady=10, ipadx=10) + self.btn_favorite.pack(side=tk.LEFT, padx=(10,10)) + self.btn_menu.pack(side=tk.LEFT) + self.scroll_bar.pack(side=tk.RIGHT,fill=tk.Y) + self.site_display.pack(expand=True, side=tk.TOP, fill=tk.BOTH) + self.status_info.pack(side=tk.LEFT) + + + def add_status_titles(self): + self.btn_back.pop_title = 'Back' + self.btn_forward.pop_title = 'Forward' + self.btn_favorite.pop_title = 'Favorite' + self.btn_home.pop_title = 'Home' + self.btn_menu.pop_title = 'Menu' + + + def add_assets(self): + self.img_back = tk.PhotoImage(file='./btn_back.png') + self.img_forward = tk.PhotoImage(file='./btn_forward.png') + self.img_favorite = tk.PhotoImage(file='./btn_refresh.png') + self.img_menu = tk.PhotoImage(file='./btn_menu.png') + self.img_home = tk.PhotoImage(file='./btn_home.png') + self.img_menu = tk.PhotoImage(file='./btn_menu.png') + self.message_bar_content = tk.StringVar() + self.message_bar_content.set('Ready.') + + + # ------------Start navigation methods---------------------------- + + + def execute_address(self, event=False, btn_url=False, history=True): + url = btn_url if btn_url else self.entry_url.get() + if url == 'home': + self.load_home_screen() + return True + + parsed_url = self.parser.parse_url(url) + + if not parsed_url: + # To do: build errors class to handle displaying errors + # return errors.url_error + return False + + response = self.conn.request(self.parser.resource, self.parser.host, self.parser.filetype, self.parser.port) + + if not response: + # To do: build errors class to handle displaying errors + # return errors.connection_error_NUMBER + return False + + if history: + self.history = self.history[:self.history_location+1] + self.history.append(url) + self.history_location = len(self.history) - 1 + + # Get the data to the screen + self.site_display.focus_set() + self.config["last_viewed"] = url + + self.send_to_screen(self.conn.raw_response, self.conn.filetype) + return True + + + def gotolink(self, event, href, tag_name): + element = event.widget + element.tag_config(tag_name, background=self.ACTIVELINK) + element.update_idletasks() # make sure change is visible + time.sleep(.5) # optional delay to show changed text + self.entry_url.delete(0,tk.END) + self.entry_url.insert(tk.END,href) + success = self.execute_address() + element.tag_config(tag_name, background=self.BG) # restore tag text style + element.update_idletasks() + + + def load_home_screen(self,event=None): + print('Loading home') + with open('./home.gopher','r') as f: + data = f.read() + self.entry_url.delete(0, tk.END) + self.entry_url.insert(tk.END, 'home') + self.send_to_screen(data, '1') + + + def go_back(self, event): + if len(self.history) <= 1 and self.history_location <= 0: + return False + + self.history_location -= 1 + href = self.history[self.history_location] + self.populate_url_bar(href) + self.execute_address(False, href, False) + + + def go_forward(self, event): + if len(self.history) <= 1 and self.history_location >= len(self.history) - 1: + return False + + self.history_location += 1 + href = self.history[self.history_location] + self.populate_url_bar(href) + self.execute_address(False, href, False) + + + #-------------Start view methods---------------- + + + def load_favorites(self): + header = 'i#############\tfalse\tnull.host\t1\r\ni manually edit in go.config.json\tfalse\tnull.host\t1\r\n or add using the favorites button\tfalse\tnull.host\t1\r\ni\tfalse\tnull.host\t1\r\n' + #soon add code to load in favorites here + self.send_to_screen(data=header, clear=False) + + def show_menu(self, data, clear=True): + if not data: + #error handling will go here + return False + + types = { + '0': '( TEXT )', + '1': '( MENU )', + '3': '( ERROR)', + '7': '( INTR )', + '9': '( BIN )', + 'g': '( GIF )', + 'I': '( IMG )', + 'h': '( HTML )', + 'i': '( INFO )', + 's': '( SOUND)' + } + + self.site_display.config(state=tk.NORMAL) + + if clear: + self.site_display.delete(1.0, tk.END) + print('---------------') + print(data) + for x in data[1:]: + print(x) + if x['type'] == 'i': + self.site_display.insert(tk.END,' \t\t{}\n'.format(x['description'])) + elif x['type'] == '3': + self.site_display.insert(tk.END,' \t\t{}\n'.format(x['description'])) + elif x['type'] in types: + link_count = 0 + + # adapted from: + # https://stackoverflow.com/questions/27760561/tkinter-and-hyperlinks + if x['port'] and x['port'][0] != ':': + x['port'] = ':{}'.format(x['port']) + + link = 'gopher://{}{}/{}{}'.format(x['host'], x['port'], x['type'], x['resource']) + tag_name = 'link{}'.format(link_count) + callback = (lambda event, href=link, tag_name=tag_name: self.gotolink(event, href, tag_name)) + hover = (lambda event, href=link, tag_name=tag_name: self.hoverlink(event, href, tag_name)) + clear = (lambda event, tag_name=tag_name: self.clear_status(event, tag_name)) + self.site_display.tag_bind(tag_name, "", callback) + self.site_display.tag_bind(tag_name, "", hover) + self.site_display.tag_bind(tag_name, '', clear) + self.site_display.insert(tk.END, types[x['type']], ('type_tag',)) + self.site_display.insert(tk.END,'\t\t') + self.site_display.insert(tk.END, x['description'], (tag_name,'linkcolor')) + self.site_display.insert(tk.END, '\n') + link_count += 1 + + self.site_display.config(state=tk.DISABLED) + + return True + + + def show_text(self, data): + print('Showing text') + self.site_display.config(state=tk.NORMAL) + self.site_display.delete(1.0, tk.END) + self.site_display.insert(tk.END, data) + self.site_display.config(state=tk.DISABLED) + + + def send_to_screen(self, data, itemtype='1', clear=True): + if itemtype == '0': + self.show_text(data) + elif itemtype in ['1','3']: + print('got to menu call') + data = self.parser.parse_menu(data) + self.show_menu(data, clear) + + + def update_status(self, event, href=False): + if href: + self.message_bar_content.set(href) + else: + self.message_bar_content.set(event.widget.pop_title) + + + def clear_status(self, event, tag_name=False): + if tag_name: + e = event.widget + e.tag_config(tag_name, underline=0) + self.site_display.config(cursor='xterm') + e.update_idletasks() + self.message_bar_content.set('') + + + def populate_url_bar(self, url): + self.entry_url.delete(0, tk.END) + self.entry_url.insert(tk.END, url) + + + def hoverlink(self, event, href, tag_name): + self.update_status(event, href) + e = event.widget + e.tag_config(tag_name, underline=1) + self.site_display.config(cursor="arrow") + e.update_idletasks() + + + #--------Start file handling methods------------ + + + def read_config(self, url='./go.config.json'): + if not os.path.isfile(url): + self.create_config() + with open('./go.config.json', 'r') as f: + config = f.read() + config = json.loads(config) + self.config = config + + + def write_config(self, config, url='./go.config.json'): + with open(url, 'w') as f: + data = json.dumps(config) + f.write(data) + + + def create_config(self): + config = {"favorites": [],"last_viewed": None} + self.write_config(config) + + + def close_window(self): + self.write_config(self.config) + self.root.destroy() + + + +if __name__ == '__main__': + app = GUI() + app.root.mainloop() + + diff --git a/parser.py b/parser.py new file mode 100644 index 0000000..5251883 --- /dev/null +++ b/parser.py @@ -0,0 +1,54 @@ +import re + +# Handles parsing gopher data: +# URLs, Menus + +class parser: + + def __init__(self): + self.host = None + self.resource = None + self.filetype = None + self.protocol = None + self.port = None + self.menu = None + + def parse_url(self, url): + # Take in a URL and output a dict of the url parts + + regex = r'^(?P(?:gopher:\/\/)?)?(?P[\w\.\d]+)(?P(?::\d+)?)?(?P(?:\/\d)?)?(?P(?:\/[\w\/\d\-?.]*)?)?$' + + match = re.match(regex, url) + protocol = match.group('protocol') + itemtype = match.group('type') + host = match.group('host') + port = match.group('port') + resource = match.group('resource') + + if not host: + return False + + if not resource: + resource = '/' + + + self.filetype = itemtype[len(itemtype) - 1] if itemtype else '1' + self.protocol = protocol if protocol else 'gopher://' + self.port = int(port[1:]) if port else 70 + self.host = host + self.resource = resource + + return {'host': self.host, 'resource': self.resource, 'type': self.filetype, 'protocol': self.protocol, 'port': self.port} + + + def parse_menu(self, text): + # Take in text from connection and output a list + # w/ objects representing each menu item + + message_list = text.split('\n') + message_list = [x.split('\t') for x in message_list] + message_list = [{'type': x[0][0], 'description': x[0][1:], 'resource': x[1], 'host': x[2], 'port': x[3]} for x in message_list if len(x) >= 4] + self.menu = message_list + + return message_list +