linkulator2/linkulator

408 lines
13 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.
2019-11-22 04:15:23 +00:00
from datetime import datetime
2019-11-15 19:45:01 +00:00
import getpass
import glob
2019-11-15 19:45:01 +00:00
import os
2019-11-18 17:49:54 +00:00
import signal
import stat
2019-11-18 12:22:31 +00:00
import subprocess
import sys
import time
from pathlib import Path
from shutil import which
2019-11-15 19:45:01 +00:00
username = getpass.getuser()
browser = "lynx"
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.
* 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
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-16 12:21:31 +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")
2019-11-15 19:45:01 +00:00
if len(linkulator_files) == 0:
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]
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.
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
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
## 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
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:
if line[4] not in categories and line[4] != "":
categories.append(line[4])
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
def print_categories():
2019-11-16 12:21:31 +00:00
print("Current link post categories include: ")
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-16 12:21:31 +00:00
def view_category_contents():
view_cat = ""
while view_cat not in categories:
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:
print("Sorry, that category does not exist. Try again.")
2019-11-18 03:04:57 +00:00
2019-11-22 04:15:23 +00:00
header = "\n\n{:>4s} {:<15s}{:<12s} #RESP {:<13s}".format("ID#", "DATE", "AUTHOR", "DESC")
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-22 04:15:23 +00:00
dt = datetime.utcfromtimestamp(float(line[2])).strftime('%Y-%m-%d')
2019-11-22 17:15:52 +00:00
out += "{:>4d} {:<15s}{:<12s} [0] {:s}\n".format(link_count, dt, line[1], line[6])
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
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 ()
2019-11-18 12:22:31 +00:00
else:
2019-11-22 17:15:52 +00:00
try:
link = threads[int(pid)]
view_thread(link)
2019-11-22 17:29:09 +00:00
except (KeyError, ValueError): # Catch a pid that is not in the thread list or is not a number
2019-11-22 17:15:52 +00:00
print("Invalid category ID\n- - - - -\n")
print_categories()
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.")
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,
)
print("\nReplies:")
i = 0
for line in link_data:
if line[3] == parent_id:
print(line[1] + ": " + line[6])
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: "
)
if next_step == "M" or next_step == "m":
print_categories()
elif next_step == "B" or next_step == "b":
2019-11-18 12:22:31 +00:00
view_link_in_browser(url, post_id)
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):
if which(browser) is None:
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])
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", 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.'
)
2019-11-18 17:49:54 +00:00
link_url = ""
elif link_url == "":
graceful_exit()
2019-11-18 17:49:54 +00:00
link_category = ""
while link_category == "":
link_category = input("Category: ")
if "|" in link_category:
print(
'Pipes, "|", are illegal characters in Linkulator. Please try again.'
)
2019-11-18 17:49:54 +00:00
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.'
)
2019-11-18 17:49:54 +00:00
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"
2019-11-15 19:45:01 +00:00
if os.path.exists(filename):
append_write = "a" # append if already exists
2019-11-15 19:45:01 +00:00
else:
append_write = "w+" # make a new file if not
2019-11-15 19:45:01 +00:00
with open(filename, append_write) as file:
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-15 19:45:01 +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:
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")
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...
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)
2019-11-18 03:04:57 +00:00
def graceful_exit():
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()
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:]
init()
2019-11-15 19:45:01 +00:00
if not len(args):
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)
elif args[0] in ["-p", "--post", "-p"]:
post_link()
else:
print("Unknown command: {}".format(args[0]))
if __name__ == "__main__":
2019-11-15 19:45:01 +00:00
parse_command()