gempost/gempost

364 lines
14 KiB
Python
Executable File

#!/usr/bin/python
# gempost - experimental gemlog manager for tilde.team, written by ~desertmouse
from os import system as exec
from os import listdir
from os import getlogin
from sys import argv
from hashlib import sha1
from random import random
# colored output functions
def prRed(skk): print("\033[91m {}\033[00m" .format(skk))
def prGreen(skk): print("\033[92m {}\033[00m" .format(skk))
def prYellow(skk): print("\033[93m {}\033[00m" .format(skk))
def prPurple(skk): print("\033[95m {}\033[00m" .format(skk))
# main variable declarations
default_post_page_header = """
# posts
"""
wdir = f"/home/{getlogin()}/public_gemini/" # working/main directory
postDir = wdir + "postdir/" # all post files along with the archive.gmi are stored here
indexFile = wdir + "postIndex" # the index is maintained in this file
editor = "nano"
def checkInit():
""" Checks if the public_gemini/ directory has been initialized for use with gempost or is missing some files. """
contents = listdir(wdir)
valid_contents = ["postIndex","trash","postdir"]
validity = 0
missing = []
for i in contents:
if i in valid_contents:
validity += 1
else:
missing.append(i)
if (validity != len(valid_contents)):
if (validity == 0):
prRed(f'\n Your "public_gemini/" directory has not been initialized yet. Use "gempost qs" to read the quickstart.')
else:
prRed(f'\n Your "public_gemini/" directory is missing some required files and directories / has not been properly initialized.\n\n The following files/directories are missing: {missing}\n Validity score: {validity}/{len(valid_contents)}')
exit()
def rebuildReferences(filename = None, title = None, callingFrom = "default"):
""" reads {wdir}postIndex and rebuilds the {wdir}posts.gmi and {postDir}archive.gmi files """
customized = False
allposts = [] # storing all indexFile entries inside a list
with open(f"{indexFile}",'r') as i:
allposts = i.readlines()
if (len(allposts) == 0): # if postIndex is empty, there is nothing to to
# reset posts.gmi and delete archive.gmi
# ie, customize the function
customized = True
headerText = []
try:
with open(f"{wdir}postPageHeader.gmi",'r') as h: # the file postPageHeader should be defined by the user in {wdir}. It should contain the user's custom post page header
headerText = h.readlines()
except FileNotFoundError: # If user hasn't created that file, a default line "# posts" is added to posts.gmi
headerText.append(default_post_page_header)
if (customized == True): # if postIndex was empty, then do not do more than this. Just remove archive.gmi after this
exec(f"rm {postDir}archive.gmi") # delete archive.gmi
with open(f"{wdir}posts.gmi",'w') as po: # writing the header into posts.gmi
po.writelines(headerText)
return 0
headerText.append("\n## Latest\n")
if (callingFrom == "delete"): # the function's callingFrom switch is to perform special operations based on where it is invoked from
headerText.append(f"{allposts[-1]}")
else:
headerText.append(f"=> postdir/{filename}.gmi {title}\n")
headerText.append("\n## Recent\n") # adding the five most recent posts to posts.gmi
try:
counter = 0
for i in allposts[::-1]:
if (counter == 5):
break
headerText.append(i)
counter += 1
except IndexError:
pass
allposts = [] # storing all indexFile entries inside a list
with open(f"{indexFile}",'r') as i:
allposts = i.readlines()
headerText.append("\n## Older\n") # for older entries, refer to another file, archive.gmi, which is basically a reversed copy of {indexFile} to show the most recent posts on top
with open(f"{postDir}archive.gmi","w") as ar: # creating all posts list
ar.writelines(allposts[::-1])
headerText.append(f"=> postdir/archive.gmi Posts Archive")
with open(f"{wdir}posts.gmi",'w') as po: # finally writing into posts.gmi to "bring it all together"
po.writelines(headerText)
def newpost(title, existing_content = None):
modified = False
""" Write and submit a new post """
if (existing_content == None or existing_content == []):
pass
else:
modified = True # modify the function if doing this for existing file
filename = sha1(str(f"{title}{random()}").encode("utf-8")).hexdigest() # generating a unique filename
exec(f"touch {wdir + filename}.gmi") # creating empty file
if modified: # add content to file using the existing_content list instead of opening editor
with open(f"{wdir+filename}.gmi", 'w') as o:
o.writelines(existing_content)
else:
temp = "# " + title # adding title to empty file
exec(f'echo "{temp}" | cat > {wdir + filename}.gmi')
exec(f"{editor} {wdir+filename}.gmi") # opening editor
ch = input("\nWould you like to review before posting? (c to cancel) (y/[n]): ")
if (ch == 'y' or ch == 'Y'):
exec(f"less {wdir+filename}.gmi") # opening the file in editor for review
exec(f"mv {wdir+filename}.gmi {postDir+filename}.gmi") # moving file to postdir
elif (ch == 'c'):
exec(f"mv {wdir+filename}.gmi {wdir}trash/")
print("Cancelled")
return None
else:
exec(f"mv {wdir+filename}.gmi {postDir+filename}.gmi") # moving file to postdir without review
# updating postIndex
exec(f'echo "=> postdir/{filename}.gmi {title}" | cat >> {indexFile}')
# updating posts.gmi, preserving the user's custom header - rbfn begin
rebuildReferences(filename, title)
# changing file permissions
exec(f"chmod 745 {wdir}posts.gmi")
exec(f"chmod 745 {wdir}postdir && chmod 745 {wdir}postdir/*")
prGreen("\nYour post is live!") # yay?
def manage():
allposts = [] # storing all indexFile entries inside a list
with open(f"{indexFile}",'r') as i:
allposts = i.readlines()
if (len(allposts) == 0): # exit function if no files in index
return 0
postsList = []
for i in allposts:
postsList.append([i[56:-1],i[11:51]])
print() # line break
try:
counter = 0
for i in postsList:
print(f"{counter}. {i[0]} | {i[1]}")
counter += 1
which = int(input("\nSelect which post to manage (any other key to cancel): ")) # getting the index of the menu entry the user wants to edit
temp = allposts[which]
except:
prRed("\n Invalid selection. Exiting...")
return 0
try:
mode = int(input("\nSELECT MODE:\n1 - EDIT\n2 - DELETE\n3 - CANCEL\n-> ")) # what does the user want to do with the selection?
except ValueError:
print("\nCancelling...")
return 0
if (mode == 1): # if mode is EDIT, open editor with the post file
print(f'\nEditing "{postsList[which][0]}" ...')
exec(f"{editor} {postDir}{postsList[which][1]}.gmi")
prGreen("Post updated.")
elif (mode == 2): # for DELETE mode, first delete post file, delete its reference from {wdir}postIndex, and rebuild the {wdir}posts.gmi and {postDir}archive.gmi files
exec(f"mv {postDir}{postsList[which][1]}.gmi {wdir}trash/")
print(f"\nfile moved to trash")
exec(f"sed -i '/{postsList[which][1]}.gmi/d' {indexFile}")
print(f"postIndex updated")
rebuildReferences(None, None, "delete")
print("rebuilt posts.gmi, archive.gmi")
prGreen("All done.")
elif (mode == 3):
print("\nCancelled.")
else:
pass
# information strings
helpText = f"""
gempost (v0.6ish) - *experimental* gemlog manager for tildes
Available arguments:
post - create a new post
Usage:
post (without arguments) Write a blog post from scratch.
post [path to file without braces] Post from .gmi file. First line will be used as heading.
manage - edit or delete your posts
purge - premanently delete the files in trash/
init - set up your public_gemini/ directory for gempost
reset - delete all posts and re-initialize public_gemini/
help - display this help text
desc - a brief description of how the program works
\033[93mqs - Quick Start for new users\033[00m
"""
quickStart = """
Basically, gempost manages a "posts.gmi" file and maintains it in a clean format
such that it shows your latest post, your five most recent posts, along with
an archive of all your posts, arranged from newest to oldest.
Doing all this manually would be *very* tedious.
If you are an existing user, please back up your public_gemini/ directory and then
proceed with the given steps.
Quickstart:
STEP 1
Wih only your index.gmi file in public_gemini/ (and optionally "postPageHeader.gmi"), run the following to set everyting up:
gempost init
STEP 2
gempost doesn't touch your index.gmi file, so you will have to manually add a link to "posts.gmi"
To do that, just add the following line to your index.gmi file:
=> ./posts.gmi Posts
CONGRATULATIONS!
You are ready to use gempost. Please just use "gempost help" from now on to only get a list of commands.
You may run "gempost desc" to get to know a bit more about how gempost works, along with a single optional feature.
"""
description = """
Directory Structure:
gempost expects the following structure of your ~/public_gemini/ directory:
public_gemini/
├── index.gmi
├── postdir/
├── postIndex
├── postPageHeader.gmi (optional)
├── posts.gmi
└── trash/
2 directories, 4 files
- "postdir/" is where all your gemlog posts are stored.
- "postIndex" is the file that gempost uses to maintain an index of your posts.
- "postPageHeader.gmi" is a file that you can populate with your custom ascii artwork or other text.
This will come before the auto-generated links to your posts.
Note that this is optional.
- "posts.gmi" is where the program will maintain links to:
- your latest post
- your 5 most recent posts
- a link to "archive.gmi" that contains a list of all your posts
- "trash/" is the directory where gempost keeps your deleted posts. use "gempost purge" to delete its contents.
"""
# Process command-line arguments
try:
arg = argv[1]
except IndexError:
print(helpText) # print the help text if no arguments passed
exit()
if (arg == "help" or arg == "-h" or arg == "--help"): # help text listing all available arguments
print(helpText)
elif (arg == "desc"): # description of how the program uses different files and dierctories
print(description)
elif (arg == "qs"):
print(quickStart) # a simple 2-step quick start for new users
elif (arg == "post"):
checkInit()
if (len(argv) == 3): # checking if more than 1 arg (except python) to see if source file has been supplied
filePath = argv[2] # storing that file's path in a var
print(f"Using file {filePath}") # tell the user that the program is using a source file
exiting_content, firstLine = [], "" # content of the file, and title respectively
try:
with open(filePath, 'r') as f: # populating file content and title variables
existing_content = f.readlines()
firstLine = existing_content[0][2:] # first line, used as title
if (firstLine[-1] == '\n'): # if there is a NewLine character at the end of title line (probably will be), remove it from post title
firstLine = firstLine[:-1]
if (len(firstLine) == 0): # message and exit if title empty
prRed("Post title cannot be empty.")
exit()
else:
newpost(firstLine, existing_content)
except FileNotFoundError:
prRed("Invalid file path / file does not exist.")
else:
print("\nAfter entering a title, an editor will be launched for you to write your blog post.")
print("The process will continue after you save your post (Ctrl+S) and exit the editor (Ctrl+X).")
title = input("\nPOST TITLE: ")
if (len(title) == 0):
prRed("Post title cannot be empty.")
else:
newpost(title)
elif (arg == "manage"):
checkInit()
manage()
elif (arg == "purge"): # permanently delete trashed posts
checkInit()
if (len(listdir(f"{wdir}trash/")) == 0):
prYellow("Trash is already empty.")
else:
exec(f"rm {wdir}trash/*")
prGreen("Trash cleared.")
elif (arg == "init"): # initialize public_gemini/ for use with gempost
if (len(listdir(f"{wdir}")) <= 2):
exec(f"touch {wdir}postIndex && mkdir {wdir}postdir/ {wdir}trash/ && chmod 745 {wdir}* && chmod 700 {wdir}trash/")
ch = input("Also create index.gmi ? (y/[n]) ")
if (ch == 'y' or ch == 'Y'):
index_init = [f"# {getlogin()}'s page\n", "\nThis is your homepage. You can customize it however you want by editing this file.\n", "\nJust remember keeping around the link below to point your visitors to your posts!\n", "\n=> ./posts.gmi Posts"]
exec(f"touch {wdir}index.gmi")
with open(f"{wdir}index.gmi", 'w') as i:
i.writelines(index_init)
prGreen('Done. Use "gempost help" to view available arguments.')
else:
prRed('Only your "index.gmi" (and optionally "postPageHeader.gmi") should be there in public_gemini/')
elif (arg == "reset"): # delete all posts and re-initialize public_gemini/ for use with gempost
prRed("Warning: This will delete all your posts.")
if (input('Type "yes, I understand" to continue: ') == "yes, I understand"):
exec(f"rm -rf {wdir}postdir/ {wdir}trash/ && rm -f {wdir}posts.gmi {indexFile}")
exec(f"touch {wdir}postIndex && mkdir {wdir}postdir/ {wdir}trash/ && chmod 745 {wdir}* && chmod 700 {wdir}trash/")
ch = input("Also create index.gmi ? (y/[n]) ")
if (ch == 'y' or ch == 'Y'):
index_init = [f"# {getlogin()}'s page\n", "\nThis is your homepage. You can customize it however you want by editing this file.\n", "\nJust remember keeping around the link below to point your visitors to your posts!\n", "\n=> ./posts.gmi Posts"]
exec(f"touch {wdir}index.gmi")
with open(f"{wdir}index.gmi", 'w') as i:
i.writelines(index_init)
prYellow("public_gemini/ has been re-initialized successfully.")
else:
print("Cancelled.")
else:
print(f"Unknown argument '{arg}'")