linkulator2/linkulator

321 lines
10 KiB
Plaintext
Raw Normal View History

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
import sys
import time
from datetime import datetime
from glob import glob
from pathlib import Path, PurePath
from shutil import which
import data
import config
2019-11-26 10:36:07 +00:00
import posts
2019-11-15 19:45:01 +00:00
username = getpass.getuser()
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.
* 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.
* 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.
"""
2019-11-15 19:45:01 +00:00
pipe_count = 4 ## A PROPERLY FORMATED LINE IN linkulator.data HAS EXACTLY FOUR PIPES.
2019-11-18 17:49:54 +00:00
2019-11-16 12:21:31 +00:00
link_data = []
## username, datestamp, parent-id, category, link-url, link-title
2019-11-16 12:21:31 +00:00
categories = []
category_counts = {}
ignore_names = []
# READS THE CURRENT USER'S IGNORE FILE AND ADDS ANY ENTRIES TO THE GLOBAL VARIABLE
# IGNORE NAMES.
def parse_ignore_file():
global ignore_names
if config.USER.ignorefile.exists():
s = config.USER.ignorefile.read_text()
l = s.splitlines()
for line in l:
name = line.split(" ")[0]
ignore_names.append(name)
2019-11-16 12:21:31 +00:00
def build_menu():
global link_data
global categories
global category_counts
link_data, categories, category_counts = data.get(config, ignore_names)
if len(link_data) == 0:
print("It looks link there are no links yet. Run 'linkulator -p' to add one.")
graceful_exit()
2019-11-18 03:04:57 +00:00
def print_categories():
print("\n{:>4s} New {:<25s}".format("ID#", "Category"))
2019-11-24 23:58:44 +00:00
for i, cat in enumerate(categories):
new_links = [1 for line in link_data if line[2] >= config.USER.lastlogin and cat == line[4]]
count = len(new_links)
print("{:4d} {} {} ({})".format(i + 1, "x" if count else " ", cat, category_counts[cat]))
2019-11-16 12:21:31 +00:00
view_category_contents()
2019-11-16 12:21:31 +00:00
def view_category_contents():
view_cat = ""
2019-11-24 23:58:44 +00:00
while True:
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
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:
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 = [line for line in link_data if line[3] == parent_id]
new_replies = len([line for line in replies if line[2] >= config.USER.lastlogin])
newmarker = "*" if new_replies or line[2] >= config.USER.lastlogin else ""
dt = datetime.utcfromtimestamp(float(line[2])).strftime("%Y-%m-%d")
out += "{:4d} {:<15s}{:<12s} [{:3d}] {:s}{}\n".format(
link_count, dt, line[1], len(replies), line[6], newmarker
)
2019-11-22 04:15:23 +00:00
2019-11-22 04:15:23 +00:00
if len(out) > 0:
print(header)
print("." * len(header))
print(out)
else:
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(
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
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"))
def view_thread(post_id):
parent_id = ""
2019-11-18 12:22:31 +00:00
url = ""
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
if parent_id == "":
print("Sorry, no thread found with that ID.")
view_category_contents() # This should not be necessary via checks from one level up, but if they get here, send them back
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...
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-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]))
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(
style_text("r", "underline"),
style_text("b", "underline"),
config.USER.browser,
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):
if which(config.USER.browser) is None:
print(
"Sorry, "
+ config.USER.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([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://")
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":
subprocess.call([config.USER.browser, url])
2019-11-18 12:22:31 +00:00
view_thread(post_id)
def reply(owner, tstamp, post_id):
global username
comment = input("Enter your comment: ")
if os.path.exists(config.USER.datafile):
append_write = "a" # append if already exists
else:
append_write = "w+" # make a new file if not
with open(config.USER.datafile, append_write) as file:
timestamp = str(time.time())
file.write(timestamp + "|" + owner + "+" + tstamp + "|||" + comment + "\r")
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():
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-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:]
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(" ----------")
parse_ignore_file()
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-23 18:35:12 +00:00
def style_text(text, *args):
styles = {
"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
if __name__ == "__main__":
2019-11-15 19:45:01 +00:00
parse_command()