2019-11-15 19:45:01 +00:00
|
|
|
#!/usr/bin/env python3
|
|
|
|
|
|
|
|
## If this script contains bugs, blame cmccabe.
|
|
|
|
|
|
|
|
import getpass
|
|
|
|
import os
|
2019-11-18 17:49:54 +00:00
|
|
|
import signal
|
2019-11-18 12:22:31 +00:00
|
|
|
import subprocess
|
2019-11-22 06:59:10 +00:00
|
|
|
import sys
|
2019-11-29 02:05:10 +00:00
|
|
|
import time
|
|
|
|
from datetime import datetime
|
|
|
|
from glob import glob
|
|
|
|
from pathlib import Path, PurePath
|
2019-11-22 06:59:10 +00:00
|
|
|
from shutil import which
|
2019-11-29 02:05:10 +00:00
|
|
|
|
2019-11-30 12:00:37 +00:00
|
|
|
import config
|
2019-11-26 10:36:07 +00:00
|
|
|
import posts
|
2019-12-05 01:14:10 +00:00
|
|
|
import data
|
2019-11-15 19:45:01 +00:00
|
|
|
|
|
|
|
username = getpass.getuser()
|
|
|
|
|
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.
|
2019-11-26 01:31:13 +00:00
|
|
|
* Link post categories are created dynamically when you submit a link. Think before you
|
|
|
|
create a new category and don't litter the board.
|
2019-11-18 20:24:35 +00:00
|
|
|
* 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-22 06:59:10 +00:00
|
|
|
* Your ~/.linkulator/linkulator.data file must be readable by others, or nobody else will
|
2019-11-18 22:12:46 +00:00
|
|
|
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
|
|
|
|
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-30 12:00:37 +00:00
|
|
|
if config.USER.ignorefile.exists():
|
|
|
|
s = config.USER.ignorefile.read_text()
|
2019-11-19 03:42:00 +00:00
|
|
|
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-30 11:49:45 +00:00
|
|
|
global link_data
|
2019-11-16 12:21:31 +00:00
|
|
|
global categories
|
2019-11-20 16:01:45 +00:00
|
|
|
global category_counts
|
2019-12-05 01:14:10 +00:00
|
|
|
|
|
|
|
link_data, categories, category_counts = data.get(config, ignore_names)
|
2019-12-05 02:20:23 +00:00
|
|
|
if len(link_data) == 0:
|
|
|
|
print("It looks link there are no links yet. Run 'linkulator -p' to add one.")
|
|
|
|
graceful_exit()
|
|
|
|
|
2019-11-20 16:01:45 +00:00
|
|
|
|
2019-11-18 03:04:57 +00:00
|
|
|
|
2019-11-17 12:36:50 +00:00
|
|
|
def print_categories():
|
2019-11-24 23:58:44 +00:00
|
|
|
print("\n{:>4s} {:<25s}".format("ID#", "Category"))
|
|
|
|
for i, cat in enumerate(categories):
|
2019-11-28 12:03:35 +00:00
|
|
|
print("{:4d} {} ({})".format(i + 1, cat, category_counts[cat]))
|
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 = ""
|
2019-11-24 23:58:44 +00:00
|
|
|
while True:
|
2019-11-28 12:03:35 +00:00
|
|
|
view_cat = input(
|
|
|
|
"\nEnter category ID or {} to quit: ".format(style_text("q", "underline"))
|
|
|
|
).lower()
|
2019-11-24 23:58:44 +00:00
|
|
|
if view_cat == "q":
|
2019-11-18 03:04:57 +00:00
|
|
|
graceful_exit()
|
2019-11-24 23:58:44 +00:00
|
|
|
else:
|
|
|
|
try:
|
|
|
|
view_cat = categories[int(view_cat) - 1]
|
|
|
|
break
|
|
|
|
except (IndexError, ValueError):
|
|
|
|
print("Sorry, that category does not exist. Try again.")
|
2019-11-18 03:04:57 +00:00
|
|
|
|
2019-11-28 12:03:35 +00:00
|
|
|
header = "\n\n{:>4s} {:<15s}{:<12s} #RESP {:<13s}".format(
|
|
|
|
"ID#", "DATE", "AUTHOR", "DESC"
|
|
|
|
)
|
2019-11-22 04:15:23 +00:00
|
|
|
out = ""
|
2019-11-22 17:15:52 +00:00
|
|
|
link_count = 0
|
|
|
|
threads = {}
|
2019-11-22 04:15:23 +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-22 17:15:52 +00:00
|
|
|
link_count += 1
|
|
|
|
threads[link_count] = str(line[0])
|
2019-11-24 23:58:44 +00:00
|
|
|
parent_id = line[1] + "+" + str(line[2])
|
|
|
|
replies = len([line for line in link_data if line[3] == parent_id])
|
2019-11-28 12:03:35 +00:00
|
|
|
dt = datetime.utcfromtimestamp(float(line[2])).strftime("%Y-%m-%d")
|
|
|
|
out += "{:4d} {:<15s}{:<12s} [{:3d}] {:s}\n".format(
|
|
|
|
link_count, dt, line[1], replies, line[6]
|
|
|
|
)
|
2019-11-22 04:15:23 +00:00
|
|
|
|
|
|
|
if len(out) > 0:
|
|
|
|
print(header)
|
|
|
|
print("." * len(header))
|
|
|
|
print(out)
|
|
|
|
else:
|
2019-11-28 12:03:35 +00:00
|
|
|
print("\n\nThere are no posts for this category\n")
|
2019-11-18 03:04:57 +00:00
|
|
|
|
2019-11-24 23:58:44 +00:00
|
|
|
while True:
|
|
|
|
pid = input(
|
|
|
|
"Enter a post ID to see its thread, {} to return to the main menu, or {} to quit: ".format(
|
2019-11-28 12:03:35 +00:00
|
|
|
style_text("m", "underline"), style_text("q", "underline")
|
2019-11-24 23:58:44 +00:00
|
|
|
)
|
|
|
|
).lower()
|
|
|
|
if pid == "q": ## HARMLESS BUT UNINTENDED
|
|
|
|
graceful_exit() ## ABILITY HERE IS THAT USERS
|
|
|
|
elif pid == "m": ## CAN PUT ANY PID IN, NOT JUST
|
|
|
|
print_categories() ## FROM WITHIN THIS CATEGORY.
|
|
|
|
break
|
|
|
|
else:
|
|
|
|
try:
|
|
|
|
link = threads[int(pid)]
|
|
|
|
view_thread(link)
|
|
|
|
break
|
2019-11-28 12:03:35 +00:00
|
|
|
except (
|
|
|
|
KeyError,
|
|
|
|
ValueError,
|
|
|
|
): # Catch a pid that is not in the thread list or is not a number
|
2019-11-24 23:58:44 +00:00
|
|
|
print("{}\n\n".format("Invalid category ID/entry", "bold"))
|
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.")
|
2019-11-28 12:03:35 +00:00
|
|
|
view_category_contents() # This should not be necessary via checks from one level up, but if they get here, send them back
|
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-22 06:59:10 +00:00
|
|
|
ftime = time.strftime(
|
|
|
|
"%b %d %Y", time.gmtime(float(parent_timestamp))
|
|
|
|
) ## UGGHH...
|
2019-11-24 23:58:44 +00:00
|
|
|
print("\n\n{:<15}: {}".format(style_text("Title", "bold"), line[6]))
|
|
|
|
print("{:<15}: {}".format(style_text("Link", "bold"), line[5]))
|
|
|
|
print("{:<15}: {}".format(style_text("User", "bold"), line[1]))
|
|
|
|
print("{:<15}: {}".format(style_text("Date", "bold"), ftime))
|
2019-11-17 12:36:50 +00:00
|
|
|
|
2019-11-24 23:58:44 +00:00
|
|
|
print("\n{}:\n".format(style_text("Replies", "underline")))
|
|
|
|
|
|
|
|
replies = [line for line in link_data if line[3] == parent_id]
|
|
|
|
if len(replies):
|
|
|
|
for line in replies:
|
|
|
|
print("{}: {}".format(style_text(line[1], "bold"), line[6]))
|
2019-11-17 12:36:50 +00:00
|
|
|
else:
|
2019-11-25 00:00:52 +00:00
|
|
|
print("No replies yet. Be the first!")
|
2019-11-24 23:58:44 +00:00
|
|
|
|
2019-11-25 00:00:52 +00:00
|
|
|
next_text = "\nType {} to reply, {} to view in {}, {} for main menu, or {} to quit: ".format(
|
2019-11-28 12:03:35 +00:00
|
|
|
style_text("r", "underline"),
|
|
|
|
style_text("b", "underline"),
|
2019-11-30 12:00:37 +00:00
|
|
|
config.USER.browser,
|
2019-11-28 12:03:35 +00:00
|
|
|
style_text("m", "underline"),
|
|
|
|
style_text("q", "underline"),
|
|
|
|
)
|
2019-11-24 23:58:44 +00:00
|
|
|
|
|
|
|
while True:
|
|
|
|
next_step = input(next_text).lower()
|
|
|
|
if next_step == "m":
|
|
|
|
print_categories()
|
|
|
|
break
|
|
|
|
elif next_step == "b":
|
|
|
|
view_link_in_browser(url, post_id)
|
|
|
|
break
|
|
|
|
elif next_step == "r":
|
|
|
|
reply(parent_user, parent_timestamp, post_id)
|
|
|
|
break
|
|
|
|
elif next_step == "q":
|
|
|
|
graceful_exit()
|
|
|
|
else:
|
|
|
|
print("{}\n\n".format("Invalid entry", "bold"))
|
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-30 12:00:37 +00:00
|
|
|
if which(config.USER.browser) is None:
|
2019-11-22 06:59:10 +00:00
|
|
|
print(
|
|
|
|
"Sorry, "
|
2019-11-30 12:00:37 +00:00
|
|
|
+ config.USER.browser
|
2019-11-22 06:59:10 +00:00
|
|
|
+ " is not installed on your system. Ask your sysadmin to install it."
|
|
|
|
)
|
2019-11-20 02:49:34 +00:00
|
|
|
view_thread(post_id)
|
|
|
|
|
2019-11-22 06:59:10 +00:00
|
|
|
if (
|
|
|
|
url.startswith("gopher://")
|
|
|
|
or url.startswith("https://")
|
|
|
|
or url.startswith("http://")
|
|
|
|
):
|
2019-11-30 12:00:37 +00:00
|
|
|
subprocess.call([config.USER.browser, url])
|
2019-11-18 12:22:31 +00:00
|
|
|
else:
|
|
|
|
print("Sorry, that url doesn't start with gopher://, http:// or https://")
|
2019-12-05 01:14:10 +00:00
|
|
|
tryAnyway = input(
|
|
|
|
"Do you want to try it in", config.USER.browser, "anyway? Y/[N] "
|
|
|
|
)
|
2019-11-18 17:49:54 +00:00
|
|
|
if tryAnyway == "Y" or tryAnyway == "y":
|
2019-11-30 12:00:37 +00:00
|
|
|
subprocess.call([config.USER.browser, url])
|
2019-11-18 12:22:31 +00:00
|
|
|
view_thread(post_id)
|
|
|
|
|
|
|
|
|
2019-11-17 12:36:50 +00:00
|
|
|
def reply(owner, tstamp, post_id):
|
|
|
|
global username
|
|
|
|
|
|
|
|
comment = input("Enter your comment: ")
|
|
|
|
|
2019-11-30 12:00:37 +00:00
|
|
|
if os.path.exists(config.USER.datafile):
|
2019-11-22 06:59:10 +00:00
|
|
|
append_write = "a" # append if already exists
|
2019-11-17 12:36:50 +00:00
|
|
|
else:
|
2019-11-22 06:59:10 +00:00
|
|
|
append_write = "w+" # make a new file if not
|
2019-11-30 12:00:37 +00:00
|
|
|
with open(config.USER.datafile, append_write) as file:
|
2019-11-22 06:59:10 +00:00
|
|
|
timestamp = str(time.time())
|
|
|
|
file.write(timestamp + "|" + owner + "+" + tstamp + "|||" + comment + "\r")
|
2019-11-17 12:36:50 +00:00
|
|
|
|
2019-11-22 06:59:10 +00:00
|
|
|
x = input("Reply added. Hit [Enter] to return to thread.")
|
2019-11-17 12:36:50 +00:00
|
|
|
build_menu()
|
|
|
|
view_thread(post_id)
|
2019-11-22 06:59:10 +00:00
|
|
|
|
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-22 06:59:10 +00:00
|
|
|
|
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-22 06:59:10 +00:00
|
|
|
|
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-12-03 01:52:37 +00:00
|
|
|
config.USER.save()
|
2019-11-18 17:49:54 +00:00
|
|
|
exit(0)
|
|
|
|
|
|
|
|
|
|
|
|
def signal_handler(sig, frame):
|
|
|
|
graceful_exit()
|
2019-11-22 06:59:10 +00:00
|
|
|
|
|
|
|
|
2019-11-18 17:49:54 +00:00
|
|
|
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:]
|
2019-11-30 12:00:37 +00:00
|
|
|
config.init()
|
2019-11-15 19:45:01 +00:00
|
|
|
if not len(args):
|
2019-11-26 01:31:13 +00:00
|
|
|
print(" ----------")
|
|
|
|
print(" LINKULATOR")
|
|
|
|
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)
|
2019-11-25 19:14:28 +00:00
|
|
|
elif args[0] in ["-p", "--post", "-p"] and len(sys.argv) > 2:
|
2019-11-26 10:36:07 +00:00
|
|
|
posts.post_link(args[1])
|
2019-11-15 19:45:01 +00:00
|
|
|
elif args[0] in ["-p", "--post", "-p"]:
|
2019-11-26 10:36:07 +00:00
|
|
|
posts.post_link()
|
2019-11-15 19:45:01 +00:00
|
|
|
else:
|
|
|
|
print("Unknown command: {}".format(args[0]))
|
|
|
|
|
2019-11-28 12:03:35 +00:00
|
|
|
|
2019-11-23 18:35:12 +00:00
|
|
|
def style_text(text, *args):
|
|
|
|
styles = {
|
2019-11-28 12:03:35 +00:00
|
|
|
"bold": "\033[1m",
|
|
|
|
"dim": "\033[2m",
|
|
|
|
"underline": "\033[4m",
|
|
|
|
"blink": "\033[5m", # This is here if you REALLY need it...dont use it
|
|
|
|
"inverse": "\033[7m", # Make fg and bg color swap
|
|
|
|
}
|
2019-11-23 18:35:12 +00:00
|
|
|
out = ""
|
|
|
|
for arg in args:
|
|
|
|
if arg in styles:
|
|
|
|
out += styles[arg]
|
|
|
|
out += text
|
|
|
|
out += "\033[0m"
|
|
|
|
return out
|
|
|
|
|
2019-11-15 19:45:01 +00:00
|
|
|
|
2019-11-22 06:59:10 +00:00
|
|
|
if __name__ == "__main__":
|
2019-11-15 19:45:01 +00:00
|
|
|
parse_command()
|