# AGPL 3 or any later version
# (C) J.Y.Amihud ( Blender Dumbass )
import os
import json
import random
import hashlib
import urllib.parse
from datetime import datetime
from modules import Set
from modules import markdown
from modules.Common import *
def guess_type(path):
if "/json/" in path or ".json" in path:
return "application/json"
if "/css" in path or ".css" in path:
return "text/css"
if "/icon" in path or path.endswith(".png"):
return "image/png"
if path.endswith("jpg"):
return "image/jpg"
return "text/html"
def headers(server, code):
# Basic cookie for logins to work
cookie = False
if "Cookie" not in str(server.headers):
cookie = True
server.send_response(code)
server.send_header("Content-type", guess_type(server.path))
if cookie:
server.send_header("Set-Cookie", RandString())
server.end_headers()
def head(title="", description="", image="", config={}):
if image.startswith("/"): image = config.get("url","")+image
favicon = config.get("favicon", "/icon/internet")
html = """
"""+title+"""
"""
return html
def send(server, html, code):
# Add headers
headers(server, code)
server.wfile.write(html.encode("utf-8"))
def tabs():
folder = Set.Folder()+"/tabs"
tabs = {}
for tab in sorted(list(os.walk(folder))[0][1]):
try:
with open(folder+"/"+tab+"/config.json") as o:
data = json.load(o)
tabs[tab] = data
except Exception as e:
print(e)
pass
return tabs
def accounts():
folder = Set.Folder()+"/accounts"
accounts = {}
for account in sorted(list(os.walk(folder))[0][2]):
try:
with open(folder+"/"+account) as o:
data = json.load(o)
data["username"] = account.replace(".json","")
accounts[account.replace(".json","")] = data
except Exception as e:
print(e)
pass
return accounts
def validate(cookie):
Accounts = accounts()
for account in Accounts:
if cookie in Accounts[account].get("sessions", []):
return Accounts[account]
return {}
def moderates(moderator, user):
Accounts = accounts()
if moderator not in Accounts:
return False
if user not in Accounts:
return True
if moderator == user:
return True
def articles(tab):
folder = Set.Folder()+"/tabs/"+tab
articles = {}
for article in list(os.walk(folder))[0][1]:
try:
with open(folder+"/"+article+"/metadata.json") as o:
data = json.load(o)
data["tab"] = tab
data["url"] = "/"+tab+"/"+article
articles[article] = data
except Exception as e:
print(e)
pass
# Sorting articles based on timestamp
articles = {k:articles[k] for k in sorted(articles, key=lambda y: articles[y]["timestamp"], reverse=True)}
return articles
def allArticles():
articles = {}
f = Set.Folder()
for tab in list(os.walk(f+"/tabs/"))[0][1]:
folder = f+"/tabs/"+tab
for article in list(os.walk(folder))[0][1]:
try:
with open(folder+"/"+article+"/metadata.json") as o:
data = json.load(o)
data["tab"] = tab
data["url"] = "/"+tab+"/"+article
articles[article] = data
except Exception as e:
print(e)
pass
# Sorting articles based on timestamp
articles = {k:articles[k] for k in sorted(articles, key=lambda y: articles[y]["timestamp"], reverse=True)}
return articles
def randomArticles():
articles = {}
f = Set.Folder()
for tab in list(os.walk(f+"/tabs/"))[0][1]:
folder = f+"/tabs/"+tab
for article in list(os.walk(folder))[0][1]:
try:
with open(folder+"/"+article+"/metadata.json") as o:
data = json.load(o)
data["tab"] = tab
data["url"] = "/"+tab+"/"+article
articles[article] = data
except Exception as e:
print(e)
pass
# Randomizing Articles.
newarticles = {}
while articles:
article = random.choice(list(articles.keys()))
newarticles[article] = articles.pop(article)
return newarticles
def suggestedArticles(cookie, random=False):
if not random:
articles = allArticles()
else:
articles = randomArticles()
# Suggesting unread articles.
newarticles = {}
move = []
for article in articles:
if cookie not in articles[article].get("views", {}).get("viewers", []):
move.append(article)
for article in move:
newarticles[article] = articles[article]
for article in articles:
if article not in move:
newarticles[article] = articles[article]
return newarticles
def previewsToSize(text):
# Calculates roughly how many previews to fit any
# given article.
# A thousand character article is about 4 articles.
return len(text)/2200
###
def MainPage(server):
# Reading config
config = Set.Load()
# Generating
html = head(title = config.get("title", "Website"),
description = config.get("description", "Description"),
config = config
)
html = html + LoginButton(server)
html = html + """
"""+config.get("title", "My Website")+"""
"""+config.get("tagline", "")+"""
"""
Tabs = tabs()
for tab in Tabs:
html = html + Button(Tabs[tab].get("title", tab),
"/"+tab,
Tabs[tab].get("icon", "folder"))
html = html + "
"
# Trending articles
html = html + '
'
trends = suggestedArticles(server.cookie)
Buffer = 20
for n, article in enumerate(trends):
if n >= Buffer: break
article = trends[article]
html = html + ArticlePreview(article, Tabs, server.cookie)
html = html + '
'
html = html + Footer()
html = html + LoginButton(server)
send(server, html, 200)
def ListPage(server, tab):
Tabs = tabs()
Articles = articles(tab)
config = Set.Load()
try: page = int(server.parsed.get("page", ["0"])[0])
except Exception as e:
print(e)
page = 0
Buffer = 16
From = Buffer*page
To = From+Buffer
# Generating
html = head(title = Tabs.get(tab, {}).get("title", tab),
description = "",
config = config
)
html = html + Button(config.get("title", "My Website"), "/", image=config.get("favicon", "/icon/internet"))
# Scroll thingie
if len(Articles) > Buffer:
if page > 0:
html = html + Button(str(page-1), tab+"?page="+str(page-1), "left")
html = html + '
'+str(page)+'
'
if To < len(Articles)-1:
html = html + Button(str(page+1), tab+"?page="+str(page+1), "right")
html = html + """
"""
print(page)
rendered = 0
for n, article in enumerate(Articles):
if n < From: continue
if n >= To: break
html = html + ArticlePreview(Articles[article], Tabs, server.cookie)
rendered += 1
html = html + '
'
# Bottom pannel for large dialogs
if rendered >= Buffer/2:
html = html + Button(config.get("title", "My Website"), "/", image=config.get("favicon", "/icon/internet"))
# Scroll thingie
if len(Articles) > Buffer:
if page > 0:
html = html + Button(str(page-1), tab+"?page="+str(page-1), "left")
html = html + '
'+str(page)+'
'
if To < len(Articles)-1:
html = html + Button(str(page+1), tab+"?page="+str(page+1), "right")
html = html + LoginButton(server)
send(server, html, 200)
def ArticlePage(server, url):
user = validate(server.cookie)
if url.endswith(".md"):
url = url.replace(".md", "")
config = Set.Load()
tab, article = url.split("/")
Tabs = tabs()
Articles = articles(tab)
f = Set.Folder()
# Generating
html = head(title = Articles.get(article, {}).get("title", article),
description = Articles.get(article, {}).get("description", ""),
config = config
)
html = html + Button(config.get("title", "My Website"), "/", image=config.get("favicon", "/icon/internet"))
html = html + Button(Tabs.get(tab, {}).get("title", tab), "/"+tab, Tabs.get(tab, {}).get("icon", "folder"))
# The article itself
html = html + '
"
# Page author
author = Articles.get(article, {}).get("author", "")
if author:
html = html + '
'+User( author )+'
'
# Page views
# Views are calculated using an iframe which only loads when the page is
# rendered in a browser. It is also looking at whether the same cookie renders
# the same page, to avoid double counting from the same person.
# The iframe itself shows a graph of views.
views = str(Articles.get(article, {}).get("views", {}).get("amount", 0))
html = html + '
👁 '+views+''
html = html + """
"""
html = html + '
'
# Audio recording of the article
recording = Articles.get(article, {}).get("recording", "")
if recording:
html = html + ''
html = html + '
'
html = html + markdown.convert(f+"/tabs/"+tab+"/"+article+"/text.md")
html = html + '
'
# Comments
html = html + CommentInput(server, url)
comments = Articles.get(article, {}).get("comments", {}).get("comments", [])
commentsTextLength = 0
comment_edit = server.parsed.get("comment_edit", [""])[0]
if comments:
for n, comment in enumerate(comments):
if str(n) == comment_edit and moderates(user.get("username"), comment.get("username")):
html = html + CommentEditInput(server, comment, url, n, user)
else:
html = html + Comment(comment, url, n, user)
# Needed to extend the suggestion for pages with many comments
commentsTextLength += previewsToSize(comment.get("text", ""))
# Requests
requests = Articles.get(article, {}).get("comments", {}).get("requests", [])
if requests:
for n, comment in enumerate(requests):
if comment.get("cookie") == server.cookie:
html = html + Comment(comment, url, n, user, request=True)
elif moderates(user.get("username"), comment.get("username")):
html = html + CommentEditInput(server, comment, url, n, user, request=str(n))
html = html + '
'
# Thumbnail and suggestions
html = html + '
'
thumbnail = Articles.get(article, {}).get("thumbnail")
if thumbnail:
html = html + '
'
html = html + ''
html = html + '
'
suggestions = suggestedArticles(server.cookie, random=True)
toomuch = previewsToSize(open(f+"/tabs/"+tab+"/"+article+"/text.md").read())
toomuch += commentsTextLength
for n, title in enumerate(suggestions):
if server.path in suggestions[title].get("url") :
continue
if n > toomuch:
break
article = suggestions[title]
html = html + ArticlePreview(article, Tabs, server.cookie)
html = html + ""
html = html + LoginButton(server)
send(server, html, 200)
def AccountPage(server, account):
config = Set.Load()
Accounts = accounts()
Tabs = tabs()
Articles = allArticles()
f = Set.Folder()
# Generating
html = head(title = Safe(Accounts.get(account, {}).get("title", account)),
description = Safe(Accounts.get(account, {}).get("bio" , "")),
config = config
)
html = html + Button(config.get("title", "My Website"), "/", image=config.get("favicon", "/icon/internet"))
# Name and bio
html = html + '
'
bio = Safe(Accounts.get(account, {}).get("bio" , ""))
if bio:
html = html + '
'
html = html + markdown.convert(bio, False)
html = html + '
'
# Posts by this account
html = html + '
'
for article in Articles:
if Articles[article].get("author") != account:
continue
html = html + ArticlePreview(Articles[article], Tabs, server.cookie)
html = html + '
'
html = html + '
'
# Thumbnail and suggestions
html = html + '
'
avatar = Safe(Accounts.get(account, {}).get("avatar" , ""))
if avatar:
html = html + '
'
html = html + ''
html = html + '
'
# Invited
invited = Accounts.get(account, {}).get("invited", [])
if invited:
html = html + '
Invited:
'
for username in invited:
html = html + '
'
html = html + '
' + User(username) + '
\n'
html = html + '
'
html = html + LoginButton(server)
send(server, html, 200)
def LoginPage(server):
config = Set.Load()
Accounts = accounts()
f = Set.Folder()
wrongname = server.parsed.get("wrong", [""])[0]
# Generating
html = head(title = "Login",
description = "Login",
config = config
)
html = html + Button(config.get("title", "My Website"), "/", image=config.get("favicon", "/icon/internet"))
html = html + '
'
if wrongname:
html = html + '\n Wrong Username / Password \n'
html = html + """
Don't have an account? Register.
"""
html = html + '
"+sup+"\n"
if "thumbnail" in article:
html = html + '
'
author = article.get("author", "")
if author:
html = html + '
'+User( author )+'
'
views = str(article.get("views", {}).get("amount", 0))
try: comments = str(len(article.get("comments", {}).get("comments")))
except: comments = "0"
html = html +'
👁 '+views+' 💬 '+comments+'
'
html = html + " "+markdown.convert(article.get("description", ""), False)+"
"
html = html + '
\n'
return html
def Footer():
html = """
"""
html = html + Button("Powered with BDServer", "https://codeberg.org/blenderdumbass/BDServer/", "codeberg")
html = html + """
"""
return html
def User(username, stretch=False):
try:
with open(Set.Folder()+"/accounts/"+username+".json") as o:
account = json.load(o)
except:
account = {}
# We are doing a lot of reductions in case somebody sneaks html code.
avatar = Safe(account.get("avatar", ""))
if not avatar: avatar = "/pictures/monkey.png"
username = Safe(username)
title = Safe(account.get("title", username))
if account:
html = ' '+title+'\n'
else:
html = ' '+title+'\n'
return html
def Graph(server, url):
html = """
"""
if url.endswith(".md"):
url = url.replace(".md", "")
try:
with open(Set.Folder()+"/tabs"+url+"/metadata.json") as o:
article = json.load(o)
except:
article = {}
dates = article.get("views", {}).get("dates", {})
largest = max(dates.values())
dateformat = "%Y-%m-%d"
startdate = datetime.strptime(sorted(list(dates.keys()), reverse=True)[0], dateformat)
enddate = datetime.strptime(sorted(list(dates.keys()), reverse=True)[-1], dateformat)
alldays = int((startdate - enddate).days)
print(alldays)
for n, date in enumerate(sorted(dates, reverse=True)):
amount = dates[date]
width = 100 / (alldays+1)
height = 60 * (amount / largest)
cd = datetime.strptime(date, dateformat)
nd = int((startdate - cd).days)
html = html + '\n'
# Saving the view
if server.cookie not in article.get("views", {}).get("viewers", []):
try:
article["views"]["amount"] += 1
article["views"]["viewers"].append(server.cookie)
with open(Set.Folder()+"/tabs"+url+"/metadata.json", "w") as save:
json.dump(article, save, indent=4)
except Exception as e:
print(e)
pass
send(server, html, 200)
def CommentInput(server, url):
user = validate(server.cookie)
html = """
"""
return html
def CommentEditInput(server, comment, url, n=0, user=None, request=None):
Accounts = accounts()
html = '
'
html = html + """
'
html = html + '
'
return html
def Comment(comment, url, n=0, user=None, request=False):
if not request:
html = '
'
else:
html = '
'
account = comment.get("username", "Anonymous User")
html = html + User(account) + ' \n'
if request:
html = html + '
Pending Approval
'
warning = comment.get("warning", "")
if warning:
html = html + ''
html = html + ''
html = html + ''
html = html + warning
html = html + ''
html = html + markdown.convert(Safe(comment.get("text", "")), False)+' '
if warning:
html = html + ''
if moderates(user.get("username"), account):
html = html + ''
html = html + ' Edit'
html = html + ''
html = html + ''
html = html + ' Delete'
html = html + ''
html = html + '
\n'
return html
def LoginButton(server):
user = validate(server.cookie)
html = '
'
return html
###
def Redirect(server, url):
html = """"""
send(server, html, 200)
def Login(server):
username = server.parsed.get("user_name", [""])[0]
password = server.parsed.get("password" , [""])[0]
hashed = hashlib.sha512(password.encode("utf-8")).hexdigest()
Accounts = accounts()
if username not in Accounts or hashed != Accounts[username].get("password"):
Redirect(server, "/login?wrong=username")
else:
account = Accounts[username]
if "sessions" not in account:
account["sessions"] = {}
account["sessions"][server.cookie] = server.headers.get("User-Agent")
folder = Set.Folder()+"/accounts"
with open(folder+"/"+username+".json", "w") as save:
json.dump(account, save, indent=4)
Redirect(server, "/")
def DoComment(server):
user = validate(server.cookie)
Accounts = accounts()
url = server.parsed.get("url", ["/"])[0]
if not url.startswith("/"): url = "/" + url
text = server.parsed.get("text", [""])[0]
nick = server.parsed.get("username", [""])[0]
warn = server.parsed.get("warning", [""])[0]
number = server.parsed.get("number", [""])[0]
request = server.parsed.get("request", [""])[0]
metadata = Set.Folder()+"/tabs"+url+"/metadata.json"
try:
with open(metadata) as o:
article = json.load(o)
except:
Redirect(server, "/")
return
if "comments" not in article:
article["comments"] = {}
if "comments" not in article["comments"]:
article["comments"]["comments"] = []
if "requests" not in article["comments"]:
article["comments"]["requests"] = []
comment = {
"text":text
}
placeRedirect = "#comment_"
if warn:
comment["warning"] = warn
if not nick and user:
comment["username"] = user.get("username", "")
place = "comments"
elif request:
if nick in Accounts or not nick:
nick = "Anonymous Guest"
comment["username"] = nick
del article["comments"]["requests"][int(request)]
place = "comments"
number = ""
else:
if nick in Accounts or not nick:
nick = "Anonymous Guest"
comment["username"] = nick
placeRedirect = "#request_"
place = "requests"
if not user:
comment["cookie"] = server.cookie
if not number:
article["comments"][place].append(comment)
number = len(article["comments"][place])-1
else:
number = int(number)
if moderates(user.get("username"), article["comments"]["comments"][number]["username"]):
article["comments"]["comments"][number] = comment
try:
with open(metadata, "w") as save:
json.dump(article, save, indent=4)
except:
pass
if not number:
placeRedirect = "#comments"
number = ""
Redirect(server, url+placeRedirect+str(number))
def DeleteComment(server):
user = validate(server.cookie)
url = server.parsed.get("url", ["/"])[0]
if not url.startswith("/"): url = "/" + url
number = int(server.parsed.get("number", ["0"])[0])
metadata = Set.Folder()+"/tabs"+url+"/metadata.json"
try:
with open(metadata) as o:
article = json.load(o)
except:
Redirect(server, "/")
return
comment = article["comments"]["comments"][number]
if moderates(user.get("username", ""), comment.get("username", "")):
del article["comments"]["comments"][number]
try:
with open(metadata, "w") as save:
json.dump(article, save, indent=4)
except:
pass
if number:
redirect = "#comment_"+str(number-1)
else:
redirect = "#comments"
Redirect(server, url+redirect)