diff --git a/linkulator b/linkulator index 1b93009..fd69cb2 100755 --- a/linkulator +++ b/linkulator @@ -2,15 +2,16 @@ ## If this script contains bugs, blame cmccabe. -import glob import getpass +import glob import os +import signal +import stat +import subprocess +import sys +import time from pathlib import Path from shutil import which -import signal -import sys -import subprocess -import time username = getpass.getuser() @@ -32,12 +33,12 @@ A few important points about Linkulator: to deceive others. * Link/reply threads disappear if the original link poster deletes their post; or if you put their username in your ignore file. -* Your ~/.linkulator/linkulator_data file must be readable by others, or nobody else will +* Your ~/.linkulator/linkulator.data file must be readable by others, or nobody else will see your contributions. * Linkulator may not work outside of Linux systems. """ -pipe_count = 4 ## A PROPERLY FORMATED LINE IN linkulator.data HAS EXACTLY FOUR PIPES. +pipe_count = 4 ## A PROPERLY FORMATED LINE IN linkulator.data HAS EXACTLY FOUR PIPES. link_data = [] ## username, datestamp, parent-id, category, link-url, link-title @@ -50,7 +51,7 @@ ignore_names = [] # IGNORE NAMES. def parse_ignore_file(): global ignore_names - p = Path(Path.home(), '.linkulator/ignore') + p = Path(Path.home(), ".linkulator/ignore") if p.exists(): s = p.read_text() l = s.splitlines() @@ -63,7 +64,7 @@ def build_menu(): ## WHENEVER THIS FUNCTION IS CALLED, THE DATA IS REFRESHED FROM FILES. SINCE ## DISK IO IS PROBABLY THE HEAVIEST PART OF THIS SCRIPT, DON'T DO THIS OFTEN. - linkulator_files = glob.glob('/home/*/.linkulator/linkulator.data') + linkulator_files = glob.glob("/home/*/.linkulator/linkulator.data") if len(linkulator_files) == 0: print("It looks link there are no links yet. Run 'linkulator -p' to add one.") @@ -75,24 +76,24 @@ def build_menu(): linkulator_lines = [] for filename in linkulator_files: with open(filename) as f: - file_owner = filename.split('/')[2] + file_owner = filename.split("/")[2] if file_owner in ignore_names: - continue ## IGNORE NAMES IN ignore_file + continue ## IGNORE NAMES IN ignore_file for line in f: if line.count("|") != pipe_count: - continue ## IGNORE LINES THAT AREN'T FORMATTED PROPERLY. - line = line.rstrip('\n') - split_line = line.split('|') - split_line.insert(0,file_owner) - linkulator_lines.append(split_line) ## creating a list of lists + continue ## IGNORE LINES THAT AREN'T FORMATTED PROPERLY. + line = line.rstrip("\n") + split_line = line.split("|") + split_line.insert(0, file_owner) + linkulator_lines.append(split_line) ## creating a list of lists - i=1 + i = 1 for idx, line in enumerate(linkulator_lines): - if line[2] == "": # CREATE/INSERT PARENT ID: + if line[2] == "": # CREATE/INSERT PARENT ID: linkulator_lines[idx].insert(0, i) - i=i+1 - else: ## NOT PARENT, SO NO PARENT ID - linkulator_lines[idx].insert(0,"") + i = i + 1 + else: ## NOT PARENT, SO NO PARENT ID + linkulator_lines[idx].insert(0, "") link_data = linkulator_lines ## THIS IS SUPPOSED TO SORT ALL LINKS BY CREATION DATE. NEED TO CONFIRM THAT IT WORKS. @@ -101,7 +102,7 @@ def build_menu(): global categories global category_counts categories = [] - category_counts.clear() ## CLEAR SO WE DON'T DOUBLE-COUNT IF FNC RUN MORE THAN ONCE. + category_counts.clear() ## CLEAR SO WE DON'T DOUBLE-COUNT IF FNC RUN MORE THAN ONCE. for line in link_data: if line[4] not in categories and line[4] != "": categories.append(line[4]) @@ -114,7 +115,7 @@ def print_categories(): print("Current link post categories include: ") for i in categories: print(" * " + i + "(" + str(category_counts[i]) + ")") -# print(categories) + # print(categories) view_category_contents() @@ -131,12 +132,14 @@ def view_category_contents(): if line[4] == view_cat: print("ID:", line[0], "|", line[6], "|", line[5], "|", line[1]) - pid = input("Enter a post ID to see its thread, \"m\" to return to the main menu, or hit [Enter] to quit: ") - if pid == "": ## HARMLESS BUT UNINTENDED - graceful_exit() ## ABILITY HERE IS THAT USERS - elif pid =="m" or pid == "M": ## CAN PUT ANY PID IN, NOT JUST - print_categories() ## FROM WITHIN THIS CATEGORY. - return() + pid = input( + 'Enter a post ID to see its thread, "m" to return to the main menu, or hit [Enter] to quit: ' + ) + if pid == "": ## HARMLESS BUT UNINTENDED + graceful_exit() ## ABILITY HERE IS THAT USERS + elif pid == "m" or pid == "M": ## CAN PUT ANY PID IN, NOT JUST + print_categories() ## FROM WITHIN THIS CATEGORY. + return () else: view_thread(pid) @@ -154,25 +157,34 @@ def view_thread(post_id): if parent_id == "": print("Sorry, no thread found with that ID.") - print_categories() ## THIS IS NOT A GOOD END POINT. SHOULD ASK USER TO RE-ENTER THEIR CHOICE. + print_categories() ## THIS IS NOT A GOOD END POINT. SHOULD ASK USER TO RE-ENTER THEIR CHOICE. for line in link_data: if line[1] == parent_user and line[2] == parent_timestamp: - ftime = time.strftime("%b %d %Y", time.gmtime(float(parent_timestamp))) ## UGGHH... - print("\nLink: ", line[6] + " (" + line[5] + "),\n posted by " + line[1] + " on " + ftime) + ftime = time.strftime( + "%b %d %Y", time.gmtime(float(parent_timestamp)) + ) ## UGGHH... + print( + "\nLink: ", + line[6] + " (" + line[5] + "),\n posted by " + line[1] + " on " + ftime, + ) print("\nReplies:") i = 0 for line in link_data: if line[3] == parent_id: print(line[1] + ": " + line[6]) - i = i+1 + i = i + 1 if i == 0: print("(No replies yet. Be the first!)\n") - next_step = input("Type 'R' to reply, 'B' to view in " + browser + ", 'M' for main menu, or anything else to quit: ") + next_step = input( + "Type 'R' to reply, 'B' to view in " + + browser + + ", 'M' for main menu, or anything else to quit: " + ) if next_step == "M" or next_step == "m": print_categories() - elif next_step =="B" or next_step == "b": + elif next_step == "B" or next_step == "b": view_link_in_browser(url, post_id) elif next_step == "R" or next_step == "r": reply(parent_user, parent_timestamp, post_id) @@ -182,16 +194,24 @@ def view_thread(post_id): def view_link_in_browser(url, post_id): if which(browser) is None: - print("Sorry, " + browser + " is not installed on your system. Ask your sysadmin to install it.") + print( + "Sorry, " + + browser + + " is not installed on your system. Ask your sysadmin to install it." + ) view_thread(post_id) - if url.startswith("gopher://") or url.startswith("https://") or url.startswith("http://"): - subprocess.call(['lynx', url]) + if ( + url.startswith("gopher://") + or url.startswith("https://") + or url.startswith("http://") + ): + subprocess.call(["lynx", url]) else: print("Sorry, that url doesn't start with gopher://, http:// or https://") tryAnyway = input("Do you want to try it in", browser, "anyway? Y/[N] ") if tryAnyway == "Y" or tryAnyway == "y": - subprocess.call(['lynx', url]) + subprocess.call(["lynx", url]) view_thread(post_id) @@ -202,16 +222,20 @@ def post_link(): while link_url == "": link_url = input("URL: ") if "|" in link_url: - print("Pipes, \"|\", are illegal characters in Linkulator. Please try again.") + print( + 'Pipes, "|", are illegal characters in Linkulator. Please try again.' + ) link_url = "" elif link_url == "": graceful_exit() - + link_category = "" while link_category == "": link_category = input("Category: ") if "|" in link_category: - print("Pipes, \"|\", are illegal characters in Linkulator. Please try again.") + print( + 'Pipes, "|", are illegal characters in Linkulator. Please try again.' + ) link_category = "" elif link_category == "": graceful_exit() @@ -220,45 +244,50 @@ def post_link(): while link_title == "": link_title = input("Title: ") if "|" in link_title: - print("Pipes, \"|\", are illegal characters in Linkulator. Please try again.") + print( + 'Pipes, "|", are illegal characters in Linkulator. Please try again.' + ) link_title = "" elif link_title == "": graceful_exit() timestamp = str(time.time()) - filename = '/home/' + username + '/.linkulator/linkulator.data' + filename = "/home/" + username + "/.linkulator/linkulator.data" if os.path.exists(filename): - append_write = 'a' # append if already exists + append_write = "a" # append if already exists else: - append_write = 'w+' # make a new file if not + append_write = "w+" # make a new file if not with open(filename, append_write) as file: - file.write(timestamp + '||' + link_category + '|' + link_url + '|' + link_title + "\n") + file.write( + timestamp + "||" + link_category + "|" + link_url + "|" + link_title + "\n" + ) print("Link added!") graceful_exit() - + def reply(owner, tstamp, post_id): global username comment = input("Enter your comment: ") - filename = '/home/' + username + '/.linkulator/linkulator.data' + filename = "/home/" + username + "/.linkulator/linkulator.data" if os.path.exists(filename): - append_write = 'a' # append if already exists + append_write = "a" # append if already exists else: - append_write = 'w+' # make a new file if not + append_write = "w+" # make a new file if not with open(filename, append_write) as file: - timestamp = str(time.time()) - file.write(timestamp + '|' + owner + "+" + tstamp + '|||' + comment + "\r") + timestamp = str(time.time()) + file.write(timestamp + "|" + owner + "+" + tstamp + "|||" + comment + "\r") - x = input('Reply added. Hit [Enter] to return to thread.') + x = input("Reply added. Hit [Enter] to return to thread.") build_menu() view_thread(post_id) - + def search(keyword): print("Doesn't work yet. Would be searching title, category, comment for ", keyword) + ## PSEUDOCODE: ## results_found = "" ## for line in link_data: @@ -275,6 +304,52 @@ def search(keyword): ## next_step = input("Enter ID to view thread, "M" for main menu, or [Enter] to quit: ") ## if next_step... + +def is_readable(st_mode: int) -> bool: + """Checks the provided mode is group and other readable, returns true if this is the case + + Check if 700 is readable: + >>> is_readable(16832) + False + + Check if 755 is readable: + >>> is_readable(16877) + True + """ + if bool(st_mode & stat.S_IRGRP) & bool(st_mode & stat.S_IROTH): + return True + return False + + +def init(): + """Performs startup checks to ensure environment is set up for use + + Creates necessary data directory and data file. If they exist, no error + occurs. + Checks that the data directory and data file are group and other readable. + Sets some correct permissions if they are not. + Other errors may raise an exception. + """ + dir_p = Path(Path.home(), ".linkulator") + file_p = Path(dir_p, "linkulator.data") + + dir_p.mkdir(mode=0o755, exist_ok=True) + file_p.touch(mode=0o644, exist_ok=True) + + if not is_readable(dir_p.stat().st_mode): + print( + "Warning: %s is not group or other readable - changing permissions" + % str(dir_p) + ) + dir_p.chmod(0o755) + if not is_readable(file_p.stat().st_mode): + print( + "Warning: %s is not group or other readable - changing permissions" + % str(file_p) + ) + file_p.chmod(0o644) + + def graceful_exit(): print("\n\nThank you for linkulating. Goodbye.\n") exit(0) @@ -282,11 +357,14 @@ def graceful_exit(): def signal_handler(sig, frame): graceful_exit() + + signal.signal(signal.SIGINT, signal_handler) def parse_command(): args = sys.argv[1:] + init() if not len(args): print("----------") print("LINKULATOR") @@ -302,5 +380,5 @@ def parse_command(): print("Unknown command: {}".format(args[0])) -if __name__ == '__main__': +if __name__ == "__main__": parse_command()