2019-11-15 19:45:01 +00:00
|
|
|
#!/usr/bin/env python3
|
|
|
|
|
|
|
|
## If this script contains bugs, blame cmccabe.
|
|
|
|
|
|
|
|
import glob
|
|
|
|
import getpass
|
|
|
|
import os
|
2019-11-19 03:42:00 +00:00
|
|
|
from pathlib import Path
|
2019-11-20 02:49:34 +00:00
|
|
|
from shutil import which
|
2019-11-18 17:49:54 +00:00
|
|
|
import signal
|
2019-11-15 19:45:01 +00:00
|
|
|
import sys
|
2019-11-18 12:22:31 +00:00
|
|
|
import subprocess
|
2019-11-20 02:49:34 +00:00
|
|
|
import time
|
2019-11-15 19:45:01 +00:00
|
|
|
|
|
|
|
username = getpass.getuser()
|
|
|
|
|
2019-11-20 02:49:34 +00:00
|
|
|
browser = "lynx"
|
|
|
|
|
2019-11-18 20:24:35 +00:00
|
|
|
help_text = """
|
|
|
|
options: -h or --help; -p or --post; or no option to browse links.
|
|
|
|
|
|
|
|
Linkulator is a minimalist, commandline link aggregator for small, trusting shell communities.
|
|
|
|
|
|
|
|
A few important points about Linkulator:
|
|
|
|
* Your username is associated with everything you post. No real anonymity.
|
|
|
|
* You may ignore other users by adding their username to your ~/.linkulator/ignore file,
|
|
|
|
followed by an optional description of why you're ignoring them.
|
|
|
|
* Link post categories are created dynamically. Think before you create a new category and
|
|
|
|
don't litter the board.
|
|
|
|
* No files are stored centrally. Each users' contributions are stored in ~/.linkulator/,
|
|
|
|
meaning that you may always edit or delete your own files. Please don't use this ability
|
|
|
|
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.
|
2019-11-18 22:12:46 +00:00
|
|
|
* Your ~/.linkulator/linkulator_data file must be readable by others, or nobody else will
|
|
|
|
see your contributions.
|
2019-11-20 02:49:34 +00:00
|
|
|
* Linkulator may not work outside of Linux systems.
|
2019-11-18 20:24:35 +00:00
|
|
|
"""
|
2019-11-15 19:45:01 +00:00
|
|
|
|
2019-11-18 17:49:54 +00:00
|
|
|
pipe_count = 4 ## A PROPERLY FORMATED LINE IN linkulator.data HAS EXACTLY FOUR PIPES.
|
|
|
|
|
2019-11-16 12:21:31 +00:00
|
|
|
link_data = []
|
2019-11-17 12:36:50 +00:00
|
|
|
## username, datestamp, parent-id, category, link-url, link-title
|
2019-11-16 12:21:31 +00:00
|
|
|
categories = []
|
2019-11-20 16:01:45 +00:00
|
|
|
category_counts = {}
|
2019-11-18 20:24:35 +00:00
|
|
|
ignore_names = []
|
|
|
|
|
2019-11-19 03:42:00 +00:00
|
|
|
|
|
|
|
# READS THE CURRENT USER'S IGNORE FILE AND ADDS ANY ENTRIES TO THE GLOBAL VARIABLE
|
|
|
|
# IGNORE NAMES.
|
2019-11-18 20:24:35 +00:00
|
|
|
def parse_ignore_file():
|
|
|
|
global ignore_names
|
2019-11-19 03:42:00 +00:00
|
|
|
p = Path(Path.home(), '.linkulator/ignore')
|
|
|
|
if p.exists():
|
|
|
|
s = p.read_text()
|
|
|
|
l = s.splitlines()
|
|
|
|
for line in l:
|
|
|
|
name = line.split(" ")[0]
|
|
|
|
ignore_names.append(name)
|
2019-11-18 20:24:35 +00:00
|
|
|
|
2019-11-16 12:21:31 +00:00
|
|
|
|
2019-11-17 12:36:50 +00:00
|
|
|
def build_menu():
|
2019-11-16 12:21:31 +00:00
|
|
|
## 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.
|
2019-11-15 19:45:01 +00:00
|
|
|
|
|
|
|
linkulator_files = glob.glob('/home/*/.linkulator/linkulator.data')
|
|
|
|
|
|
|
|
if len(linkulator_files) == 0:
|
2019-11-17 12:36:50 +00:00
|
|
|
print("It looks link there are no links yet. Run 'linkulator -p' to add one.")
|
2019-11-18 03:04:57 +00:00
|
|
|
exit()
|
2019-11-15 19:45:01 +00:00
|
|
|
|
|
|
|
else:
|
|
|
|
|
2019-11-16 12:21:31 +00:00
|
|
|
global link_data
|
2019-11-15 19:45:01 +00:00
|
|
|
linkulator_lines = []
|
2019-11-16 12:21:31 +00:00
|
|
|
for filename in linkulator_files:
|
|
|
|
with open(filename) as f:
|
|
|
|
file_owner = filename.split('/')[2]
|
2019-11-18 20:24:35 +00:00
|
|
|
if file_owner in ignore_names:
|
|
|
|
continue ## IGNORE NAMES IN ignore_file
|
2019-11-15 19:45:01 +00:00
|
|
|
for line in f:
|
2019-11-18 17:49:54 +00:00
|
|
|
if line.count("|") != pipe_count:
|
|
|
|
continue ## IGNORE LINES THAT AREN'T FORMATTED PROPERLY.
|
2019-11-16 12:21:31 +00:00
|
|
|
line = line.rstrip('\n')
|
|
|
|
split_line = line.split('|')
|
|
|
|
split_line.insert(0,file_owner)
|
|
|
|
linkulator_lines.append(split_line) ## creating a list of lists
|
2019-11-18 03:04:57 +00:00
|
|
|
|
2019-11-17 01:49:06 +00:00
|
|
|
i=1
|
|
|
|
for idx, line in enumerate(linkulator_lines):
|
|
|
|
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,"")
|
2019-11-16 12:21:31 +00:00
|
|
|
link_data = linkulator_lines
|
|
|
|
|
2019-11-18 22:12:46 +00:00
|
|
|
## THIS IS SUPPOSED TO SORT ALL LINKS BY CREATION DATE. NEED TO CONFIRM THAT IT WORKS.
|
|
|
|
link_data.sort(key=lambda x: x[2])
|
|
|
|
|
2019-11-16 12:21:31 +00:00
|
|
|
global categories
|
2019-11-20 16:01:45 +00:00
|
|
|
global category_counts
|
|
|
|
categories = []
|
|
|
|
category_counts.clear() ## CLEAR SO WE DON'T DOUBLE-COUNT IF FNC RUN MORE THAN ONCE.
|
2019-11-16 12:21:31 +00:00
|
|
|
for line in link_data:
|
2019-11-17 12:36:50 +00:00
|
|
|
if line[4] not in categories and line[4] != "":
|
2019-11-17 01:49:06 +00:00
|
|
|
categories.append(line[4])
|
2019-11-20 16:01:45 +00:00
|
|
|
category_counts[line[4]] = 1
|
|
|
|
elif line[4] in categories:
|
|
|
|
category_counts[line[4]] = category_counts[line[4]] + 1
|
|
|
|
|
2019-11-18 03:04:57 +00:00
|
|
|
|
2019-11-17 12:36:50 +00:00
|
|
|
def print_categories():
|
2019-11-16 12:21:31 +00:00
|
|
|
print("Current link post categories include: ")
|
2019-11-20 16:01:45 +00:00
|
|
|
for i in categories:
|
|
|
|
print(" * " + i + "(" + str(category_counts[i]) + ")")
|
|
|
|
# print(categories)
|
2019-11-16 12:21:31 +00:00
|
|
|
view_category_contents()
|
|
|
|
|
2019-11-17 01:49:06 +00:00
|
|
|
|
2019-11-16 12:21:31 +00:00
|
|
|
def view_category_contents():
|
|
|
|
view_cat = ""
|
|
|
|
while view_cat not in categories:
|
2019-11-18 20:24:35 +00:00
|
|
|
view_cat = input("View category (or hit [Enter] to quit): ")
|
2019-11-17 20:13:01 +00:00
|
|
|
if view_cat == "":
|
2019-11-18 03:04:57 +00:00
|
|
|
graceful_exit()
|
2019-11-16 12:21:31 +00:00
|
|
|
if view_cat not in categories:
|
2019-11-18 20:24:35 +00:00
|
|
|
print("Sorry, that category does not exist. Try again.")
|
2019-11-18 03:04:57 +00:00
|
|
|
|
2019-11-16 12:21:31 +00:00
|
|
|
for line in link_data:
|
2019-11-17 01:49:06 +00:00
|
|
|
if line[4] == view_cat:
|
2019-11-18 20:24:35 +00:00
|
|
|
print("ID:", line[0], "|", line[6], "|", line[5], "|", line[1])
|
2019-11-18 03:04:57 +00:00
|
|
|
|
2019-11-18 12:22:31 +00:00
|
|
|
pid = input("Enter a post ID to see its thread, \"m\" to return to the main menu, or hit [Enter] to quit: ")
|
2019-11-18 03:04:57 +00:00
|
|
|
if pid == "": ## HARMLESS BUT UNINTENDED
|
|
|
|
graceful_exit() ## ABILITY HERE IS THAT USERS
|
2019-11-18 12:22:31 +00:00
|
|
|
elif pid =="m" or pid == "M": ## CAN PUT ANY PID IN, NOT JUST
|
|
|
|
print_categories() ## FROM WITHIN THIS CATEGORY.
|
|
|
|
return()
|
|
|
|
else:
|
|
|
|
view_thread(pid)
|
2019-11-17 01:49:06 +00:00
|
|
|
|
|
|
|
|
|
|
|
def view_thread(post_id):
|
2019-11-17 12:36:50 +00:00
|
|
|
|
|
|
|
parent_id = ""
|
2019-11-18 12:22:31 +00:00
|
|
|
url = ""
|
2019-11-17 12:36:50 +00:00
|
|
|
for line in link_data:
|
|
|
|
if str(line[0]) == post_id:
|
|
|
|
parent_id = line[1] + "+" + str(line[2])
|
|
|
|
parent_user = line[1]
|
|
|
|
parent_timestamp = line[2]
|
2019-11-18 12:22:31 +00:00
|
|
|
url = line[5]
|
2019-11-18 03:04:57 +00:00
|
|
|
|
2019-11-17 12:36:50 +00:00
|
|
|
if parent_id == "":
|
2019-11-18 22:12:46 +00:00
|
|
|
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.
|
2019-11-17 12:36:50 +00:00
|
|
|
for line in link_data:
|
|
|
|
if line[1] == parent_user and line[2] == parent_timestamp:
|
2019-11-20 16:01:45 +00:00
|
|
|
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)
|
2019-11-17 12:36:50 +00:00
|
|
|
|
2019-11-20 16:01:45 +00:00
|
|
|
print("\nReplies:")
|
2019-11-17 12:36:50 +00:00
|
|
|
i = 0
|
|
|
|
for line in link_data:
|
|
|
|
if line[3] == parent_id:
|
2019-11-20 16:01:45 +00:00
|
|
|
print(line[1] + ": " + line[6])
|
2019-11-17 12:36:50 +00:00
|
|
|
i = i+1
|
|
|
|
if i == 0:
|
2019-11-20 16:01:45 +00:00
|
|
|
print("(No replies yet. Be the first!)\n")
|
2019-11-17 12:36:50 +00:00
|
|
|
|
2019-11-20 02:49:34 +00:00
|
|
|
next_step = input("Type 'R' to reply, 'B' to view in " + browser + ", 'M' for main menu, or anything else to quit: ")
|
2019-11-17 12:36:50 +00:00
|
|
|
if next_step == "M" or next_step == "m":
|
|
|
|
print_categories()
|
2019-11-18 12:22:31 +00:00
|
|
|
elif next_step =="B" or next_step == "b":
|
|
|
|
view_link_in_browser(url, post_id)
|
2019-11-17 12:36:50 +00:00
|
|
|
elif next_step == "R" or next_step == "r":
|
|
|
|
reply(parent_user, parent_timestamp, post_id)
|
|
|
|
else:
|
2019-11-18 03:04:57 +00:00
|
|
|
graceful_exit()
|
2019-11-15 19:45:01 +00:00
|
|
|
|
|
|
|
|
2019-11-18 12:22:31 +00:00
|
|
|
def view_link_in_browser(url, post_id):
|
2019-11-20 02:49:34 +00:00
|
|
|
if which(browser) is None:
|
|
|
|
print("Sorry, " + browser + " is not installed on your system. Ask your sysadmin to install it.")
|
|
|
|
view_thread(post_id)
|
|
|
|
|
2019-11-18 12:22:31 +00:00
|
|
|
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://")
|
2019-11-20 02:49:34 +00:00
|
|
|
tryAnyway = input("Do you want to try it in", browser, "anyway? Y/[N] ")
|
2019-11-18 17:49:54 +00:00
|
|
|
if tryAnyway == "Y" or tryAnyway == "y":
|
|
|
|
subprocess.call(['lynx', url])
|
2019-11-18 12:22:31 +00:00
|
|
|
view_thread(post_id)
|
|
|
|
|
|
|
|
|
2019-11-15 19:45:01 +00:00
|
|
|
def post_link():
|
2019-11-18 17:49:54 +00:00
|
|
|
print("Enter link information here. Leaving any field blank aborts post.")
|
|
|
|
|
|
|
|
link_url = ""
|
|
|
|
while link_url == "":
|
|
|
|
link_url = input("URL: ")
|
|
|
|
if "|" in link_url:
|
|
|
|
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.")
|
|
|
|
link_category = ""
|
|
|
|
elif link_category == "":
|
|
|
|
graceful_exit()
|
|
|
|
|
|
|
|
link_title = ""
|
|
|
|
while link_title == "":
|
|
|
|
link_title = input("Title: ")
|
|
|
|
if "|" in link_title:
|
|
|
|
print("Pipes, \"|\", are illegal characters in Linkulator. Please try again.")
|
|
|
|
link_title = ""
|
|
|
|
elif link_title == "":
|
|
|
|
graceful_exit()
|
|
|
|
|
2019-11-15 19:45:01 +00:00
|
|
|
timestamp = str(time.time())
|
|
|
|
filename = '/home/' + username + '/.linkulator/linkulator.data'
|
|
|
|
if os.path.exists(filename):
|
|
|
|
append_write = 'a' # append if already exists
|
|
|
|
else:
|
2019-11-19 22:57:08 +00:00
|
|
|
append_write = 'w+' # make a new file if not
|
2019-11-15 19:45:01 +00:00
|
|
|
with open(filename, append_write) as file:
|
2019-11-19 14:39:30 +00:00
|
|
|
file.write(timestamp + '||' + link_category + '|' + link_url + '|' + link_title + "\n")
|
2019-11-18 03:04:57 +00:00
|
|
|
print("Link added!")
|
|
|
|
graceful_exit()
|
2019-11-18 17:49:54 +00:00
|
|
|
|
2019-11-15 19:45:01 +00:00
|
|
|
|
2019-11-17 12:36:50 +00:00
|
|
|
def reply(owner, tstamp, post_id):
|
|
|
|
global username
|
|
|
|
|
|
|
|
comment = input("Enter your comment: ")
|
|
|
|
|
|
|
|
filename = '/home/' + username + '/.linkulator/linkulator.data'
|
|
|
|
if os.path.exists(filename):
|
|
|
|
append_write = 'a' # append if already exists
|
|
|
|
else:
|
2019-11-21 12:00:21 +00:00
|
|
|
append_write = 'w+' # make a new file if not
|
2019-11-17 12:36:50 +00:00
|
|
|
with open(filename, append_write) as file:
|
2019-11-18 22:12:46 +00:00
|
|
|
timestamp = str(time.time())
|
|
|
|
file.write(timestamp + '|' + owner + "+" + tstamp + '|||' + comment + "\r")
|
2019-11-17 12:36:50 +00:00
|
|
|
|
|
|
|
x = input('Reply added. Hit [Enter] to return to thread.')
|
|
|
|
build_menu()
|
|
|
|
view_thread(post_id)
|
|
|
|
|
2019-11-16 12:21:31 +00:00
|
|
|
|
2019-11-19 14:52:35 +00:00
|
|
|
def search(keyword):
|
|
|
|
print("Doesn't work yet. Would be searching title, category, comment for ", keyword)
|
|
|
|
|
2019-11-19 20:19:28 +00:00
|
|
|
## PSEUDOCODE:
|
|
|
|
## results_found = ""
|
|
|
|
## for line in link_data:
|
|
|
|
## if keyword in link_data[title] or keyword in link_data[category] or keyword in link_data[comment]:
|
|
|
|
## results_found = "yes"
|
|
|
|
## if line is parent post:
|
|
|
|
## print line
|
|
|
|
## elif line is reply:
|
|
|
|
## get parentID
|
|
|
|
## print(line[parentID])
|
|
|
|
## if results_found == "":
|
|
|
|
## print("No results found")
|
|
|
|
## else:
|
|
|
|
## next_step = input("Enter ID to view thread, "M" for main menu, or [Enter] to quit: ")
|
|
|
|
## if next_step...
|
|
|
|
|
2019-11-18 03:04:57 +00:00
|
|
|
def graceful_exit():
|
2019-11-20 02:49:34 +00:00
|
|
|
print("\n\nThank you for linkulating. Goodbye.\n")
|
2019-11-18 17:49:54 +00:00
|
|
|
exit(0)
|
|
|
|
|
|
|
|
|
|
|
|
def signal_handler(sig, frame):
|
|
|
|
graceful_exit()
|
|
|
|
signal.signal(signal.SIGINT, signal_handler)
|
2019-11-18 03:04:57 +00:00
|
|
|
|
2019-11-16 12:21:31 +00:00
|
|
|
|
2019-11-15 19:45:01 +00:00
|
|
|
def parse_command():
|
|
|
|
args = sys.argv[1:]
|
|
|
|
if not len(args):
|
2019-11-17 12:36:50 +00:00
|
|
|
print("----------")
|
2019-11-17 01:49:06 +00:00
|
|
|
print("LINKULATOR")
|
2019-11-17 12:36:50 +00:00
|
|
|
print("----------")
|
2019-11-18 20:24:35 +00:00
|
|
|
parse_ignore_file()
|
2019-11-17 12:36:50 +00:00
|
|
|
build_menu()
|
|
|
|
print_categories()
|
2019-11-15 19:45:01 +00:00
|
|
|
elif args[0] in ["-h", "--help", "help"]:
|
|
|
|
print(help_text)
|
|
|
|
elif args[0] in ["-p", "--post", "-p"]:
|
|
|
|
post_link()
|
|
|
|
else:
|
|
|
|
print("Unknown command: {}".format(args[0]))
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == '__main__':
|
|
|
|
parse_command()
|