# AGPL 3 or any later version
# (C) J.Y.Amihud ( Blender Dumbass )
import os
import json
import time
import email
import random
import hashlib
import urllib.parse
from datetime import datetime
from modules import Set
from modules import API
from modules import markdown
from modules.Common import *
KnownCookies = []
RecentArticles = {}
RefferedArticles = {}
ProblematicRefreshes = []
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 "/rss" in path or ".rss" in path:
return "application/rss+xml"
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
server.send_response(code)
server.send_header("Content-type", guess_type(server.path))
if not server.cookie:
cookie = RandString(200)
KnownCookies.append(cookie)
server.send_header("Set-Cookie", "temp_id="+cookie)
server.end_headers()
def head(title="", description="", image="", config={}, author=""):
if image.startswith("/"): image = config.get("url","")+image
favicon = config.get("favicon", "/icon/internet")
html = """
"""+title+"""
"""
# Author tags.
if author:
account = accounts().get(author, {})
name = account.get("title", account)
mastodon = account.get("mastodon", "")
try:
if "/" in mastodon:
mastodon = mastodon.replace("https://", "").replace("http://", "")
mastodon = mastodon.split("/")[1]+"@"+mastodon.split("/")[0]
except:
pass
if mastodon:
html = html + """
"""
# Tor tags.
tor = config.get("tor", "")
if tor:
if not tor.startswith("http://"): tor = "http://"+tor
html = html + ''
html = html + """
"""
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
if rank(moderator, Accounts) < rank(user, Accounts):
return True
def rank(account, Accounts=None):
if not Accounts:
Accounts = accounts()
if account not in Accounts:
return 1000000
if not Accounts[account].get("invited_by") or Accounts[account].get("invited_by") == account:
return 0
return 1 + rank(Accounts[account].get("invited_by"), Accounts)
def editsIn(account, tab):
# Determents whether the user
# can edit an article.
Accounts = accounts()
# If the user is not registered
# per cannot edit anything.
if account not in Accounts:
return False
# If the user is the owner of the
# site, per can edit everything.
if rank(account, Accounts) == 0:
return True
# Not all users can edit in all
# tabs.
user = Accounts[account]
if tab in user.get("editsIn", []):
return True
return False
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[data["url"]] = 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):
user = validate(server.cookie)
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"))
if editsIn(user.get("username", ""), tab):
html = html + Button("New Post", "/editor?tab="+tab, icon="new")
# 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 + """
"""
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)
referrer = server.headers.get("referer", "")
if url.endswith(".md"):
url = url.replace(".md", "")
# Recording when was the last time
# the article loaded.
RecentArticles["/"+url] = time.time()
RefferedArticles["/"+url] = referrer
config = Set.Load()
tab, article, *rest = 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", ""),
image = Articles.get(article, {}).get("thumbnail", ""),
config = config,
author = Articles.get(article, {}).get("author")
)
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 + '
'
html = html + '
'
# Edit button
if editsIn(user.get("username"), tab) and moderates(user.get("username"), Articles.get(article, {}).get("author", "")):
html = html + '
'
html = html + Button("Edit", "/editor?tab="+tab+"&name="+article, icon="edit")
html = html + '
"
# Page author
author = Articles.get(article, {}).get("author", "")
if author:
html = html + '
'+User( author )+'
'
timestamp = Articles.get(article, {}).get("timestamp", "")
arttime = str(datetime.fromtimestamp(timestamp).strftime("%B %d, %Y"))
html = html + "
"+str(arttime)+"
"
# 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 + """
"""
referrers = Articles.get(article, {}).get("views", {}).get("referrers", {})
if referrers:
for referrer in referrers:
# Filtering out probably bad links.
if ( "https://" in referrer and ".onion" not in referrer ) or rank(user.get("username", "")) == 0:
html = html + Safe(referrer) + " : 👁 "+str(referrers[referrer]) + " \n"
html = html + """
"""
html = html + '
'
# Petition
petition = Articles.get(article, {}).get("petition", "")
if petition:
petition_error = False
if petition.get("api"):
try:
API.Petition(Articles[article])
except:
petition_error = True
html = html + '
'
html = html + '
'
html = html + 'Petition
'
try:
frac = petition.get("signed", 0) / int(petition.get("goal", 1))
except:
frac = 0
html = html + ProgressBar(frac)
html = html + "
"+str(petition.get("signed", 0))+" / "+Safe(str(petition.get("goal", 1)))+" Signatures"
# Last update
if petition.get("api"):
lastUpdate = petition.get("api", {}).get("timestamp", {})
nowTime = time.time()
html = html + '
Last updated: '+TimeDifference(lastUpdate, nowTime)+'
'
if not petition.get("api"):
html = html + """
Sign
"""
else:
html = html + """
Sign
This petition is signed by increasing the number of """+petition.get("api", {}).get("title", "")+"""
"""
# 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):
user = validate(server.cookie)
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 + '
"
# Rank
Rank = rank(account)
html = html + Button("Rank "+str(Rank), "", icon="analytics")
html = html + '
'
# Protecting emails and stuff from scrubbers
if server.cookie in KnownCookies:
# Website
website = Safe(Accounts.get(account, {}).get("website" , ""))
if website:
webtitle = website.replace("https://", "").replace("http://", "")
if not website.startswith("http"): website = "http://"+website
html = html + '
'
html = html + ''
html = html + ' '+webtitle+''
html = html + '
'
# Email
email = Safe(Accounts.get(account, {}).get("email" , ""))
if email:
html = html + '
'
html = html + ''
html = html + ' '+email+''
if Accounts.get(account, {}).get("email_verified"):
html = html + ''
html = html + '
'
# Mastodon
mastodon = Safe(Accounts.get(account, {}).get("mastodon" , ""))
if mastodon:
# It could be mastodon url and not handle.
try:
if "/" in mastodon:
mastodon = mastodon.replace("https://", "").replace("http://", "")
mastodon = mastodon.split("/")[1]+"@"+mastodon.split("/")[0]
if not mastodon.startswith("@"): mastodon = "@"+mastodon
mastolink = "https://"+mastodon[1:].split("@")[1]+"/@"+mastodon[1:].split("@")[0]
html = html + '
'
html = html + ''
html = html + ' '+mastodon+''
html = html + '
'
except:
pass
# Matrix
matrix = Safe(Accounts.get(account, {}).get("matrix" , ""))
if matrix:
# Matrix could be the matrix.to link
if "/" in matrix:
matrix = matrix[matrix.rfind("/")+1:]
matrixlink = "https://matrix.to/#/"+matrix
html = html + '
'
html = html + ''
html = html + ' '+matrix+''
html = html + '
'
if any((website, email, mastodon, matrix)):
html = html + ' '
html = html + '
'
invited_by = Accounts.get(account, {}).get("invited_by", "")
if invited_by:
html = html + '
'
html = html +"
Invited by: "+User(invited_by)+"
"
html = html + '
'
bio = Safe(Accounts.get(account, {}).get("bio" , ""))
if bio:
html = html + '
'
html = html + markdown.convert(bio, False)+' '
html = html + '
'
# Validating this account
validates = []
if user.get("username", "") != account and moderates(user.get("username", ""), account):
for tab in Tabs:
if editsIn(user.get("username", ""), tab):
validates.append(tab)
if validates:
html = html + '
'
html = html + "You can grant publication rights to this account. "
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:
if username in Accounts:
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]
redirect = server.parsed.get("redirect", [""])[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?
"""
html = html + Button("Register", "/register", icon="user_new")
send(server, html, 200)
def RegisterPage(server):
user = validate(server.cookie)
config = Set.Load()
Accounts = accounts()
f = Set.Folder()
code = server.parsed.get("code", [""])[0]
userexists = server.parsed.get("userexists", [""])[0]
wrongcode = server.parsed.get("wrongcode", [""])[0]
# Generating
html = head(title = "Register",
description = "Register",
config = config
)
html = html + Button(config.get("title", "My Website"), "/", image=config.get("favicon", "/icon/internet"))
html = html + '
'
html = html + """
Have an account?
"""
html = html + Button("Login", "/login", icon="unlock")
send(server, html, 200)
def SettingsPage(server):
user = validate(server.cookie)
# Authorization check
if not user:
AccessDenied(server)
return
config = Set.Load()
# Generating
html = head(title = "Settings",
description = "Settings",
config = config
)
html = html + Button(config.get("title", "My Website"), "/", image=config.get("favicon", "/icon/internet"))
html = html + Button("Public Profile", "/account/"+user.get("username", ""), image="/pictures/monkey.png")
# Main settings
html = html + """
Public Info
"""
if user.get("email"):
if not user.get("email_verified"):
html = html + """
Email Settings
Email """+Safe(user.get("email"))+""" is not verified.
"""+Button("Verify", "/email_verify", "ok")+"""
"""
else:
html = html + """
"""
# Current Logged in Sessions
sessions = user.get("sessions", {})
html = html + '
'
html = html + '
Active Sessions
'
for cookie in sessions:
session = sessions[cookie]
CancelButton = Button("Log Out", "/log_out?cookie="+cookie, icon="cancel")
if server.cookie == cookie:
html = html + ' '
else:
html = html + ' '
html = html + ''+CancelButton
html = html + '
'
html = html + '
'
html = html + '
'
# Invites and Invite codes
invite_codes = user.get("invite_codes", {})
html = html + '
'
html = html + '
Invites
'
for code in invite_codes:
nick = invite_codes[code]
Open = ""
if code == server.parsed.get("code", [""])[0]:
Open = "open"
html = html + ''
html = html + ''+nick
html = html + ' '
html = html + ''
html = html + ''
html = html + Button("Share Link", "/register?code="+code, icon="link")
html = html + Button("Cancel", "/cancel_invite?code="+code, icon="cancel")
html = html + ''
html = html + """
Change Password
"""
notifications = user.get("notifications","")
if notifications:
html = html + '
'
for notification in notifications:
html = html + '
"+sup+"\n"
if article.get("thumbnail"):
html = html + '
'
petition = article.get("petition", "")
if petition:
try:
frac = petition.get("signed", 0) / int(petition.get("goal", 1))
except:
frac = 0
html = html + ProgressBar(frac)
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):
if url.endswith(".md"):
url = url.replace(".md", "")
# If there are any values after ? in the path
# which means, that somebody is sending the old
# version of the graph link from the legacy code
# we should not count it as a view.
if "?" in server.path:
AccessDenied(server)
return
# Since /graph/ is used to count views
# we need the cookie to be generated and
# used by the user's browser before we load
# it, since a lot of people might just click
# the link once. In which case the entire page
# including graph loads before the cookie.
if not server.cookie and server.headers.get("user-agent", "") not in ProblematicRefreshes:
Redirect(server, server.path)
ProblematicRefreshes.append(server.headers.get("user-agent", ""))
return
if server.headers.get("user-agent", "") in ProblematicRefreshes:
ProblematicRefreshes.remove(server.headers.get("user-agent", ""))
# Sometimes scrapers try to load graph without
# loading the article first. We don't want to count
# it as a view.
if time.time()-20 > RecentArticles.get(url, 0):
print(consoleForm(server.cookie), "Article wasn't loaded, scrapers!")
AccessDenied(server)
return
user = validate(server.cookie)
# Store general analytics about which search engines were used.
# To get to this article.
referrer = RefferedArticles.get(url, "")
html = """
"""
try:
with open(Set.Folder()+"/tabs"+url+"/metadata.json") as o:
article = json.load(o)
except:
article = {}
dateformat = "%Y-%m-%d"
dates = article.get("views", {}).get("dates", {})
if dates:
largest = max(dates.values())
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)
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
cookies = [server.cookie]
if user:
# If user is logged in, we want to record
# per reading the article, on all pers
# sessions.
for cookie in user.get("sessions"):
if cookie not in cookies:
cookies.append(cookie)
for cookie in cookies:
if cookie and cookie not in article.get("views", {}).get("viewers", []):
article["views"]["amount"] += 1
article["views"]["viewers"].append(cookie)
if "referrers" not in article["views"]:
article["views"]["referrers"] = {}
if referrer and referrer not in article["views"]["referrers"]:
article["views"]["referrers"][referrer] = 0
if referrer:
article["views"]["referrers"][referrer] += 1
dates = article["views"]["dates"]
date = datetime.now().strftime(dateformat)
dates[date] = dates.get(date, 0) + 1
server.newview = True
with open(Set.Folder()+"/tabs"+url+"/metadata.json", "w") as save:
json.dump(article, save, indent=4)
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 ProgressBar(frac):
title = str(round(frac*100,1))+"%"
frac = min(1, frac)
frac = max(0, frac)
html = '
'
html = html + '
'
html = html + '
'
return html
def NotFound(server):
config = Set.Load()
html = head(title = "404 Not Found",
description = "404 Not Found",
config = config
)
html = html + Button(config.get("title", "My Website"), "/", image=config.get("favicon", "/icon/internet"))
html = html + """
404 Not Found
"""
send(server, html, 404)
def AccessDenied(server):
config = Set.Load()
html = head(title = "403 Access Denied",
description = "403 Access Denied",
config = config
)
html = html + Button(config.get("title", "My Website"), "/", image=config.get("favicon", "/icon/internet"))
html = html + """
403 Access Denied
"""
send(server, html, 404)
def Error(server, text="Some Error Happened."):
config = Set.Load()
html = head(title = "501 Error",
description = "501 Error",
config = config
)
html = html + Button(config.get("title", "My Website"), "/", image=config.get("favicon", "/icon/internet"))
html = html + """
501 Error
"""+text+"""
"""
send(server, html, 501)
###
def Redirect(server, url, time=0):
print(consoleForm(server.cookie), "Redirecting to: "+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()
redirect = server.parsed.get("redirect" , [""])[0]
logout = server.parsed.get("logout" , [""])[0]
Accounts = accounts()
# Failed authentication
if username not in Accounts or hashed != Accounts[username].get("password"):
Redirect(server, "/login?wrong=username")
# Succesfull authentication
else:
account = Accounts[username]
if "sessions" not in account:
account["sessions"] = {}
account["sessions"][server.cookie] = server.headers.get("User-Agent")
f = Set.Folder()
folder = f+"/accounts"
# Move the cookie arround
# When a login happens, we want to make the server know
# which articles the person already read and stuff. So
# we want to come all the cookies. Arround.
articles = allArticles()
for title in articles:
article = articles[title]
for cookie in account["sessions"]:
if cookie != server.cookie:
# Making it so it knows what you were watching previously.
if cookie in article.get("views", {}).get("viewers", [])\
and server.cookie not in article["views"]["viewers"]:
article["views"]["viewers"].append(server.cookie)
# Making it so previously logged in account would know.
# what you were watching thus far from this cookie.
if server.cookie in article.get("views", {}).get("viewers", [])\
and cookie not in article["views"]["viewers"]:
article["views"]["viewers"].append(cookie)
with open(f+"/tabs"+article.get("url")+"/metadata.json", "w") as save:
json.dump(article, save, indent=4)
if logout:
for cookie in list(account["sessions"].keys()):
if cookie != server.cookie:
del account["sessions"][cookie]
with open(folder+"/"+username+".json", "w") as save:
json.dump(account, save, indent=4)
if not redirect:
Redirect(server, "/settings")
else:
Redirect(server, redirect)
def Register(server):
# If by mistake we are logged in
user = validate(server.cookie)
if user:
Redirect(server, "/register")
username = Simplify(server.parsed.get("user_name", [""])[0], "file")
code = server.parsed.get("code", [""])[0]
password = server.parsed.get("password" , [""])[0]
hashed = hashlib.sha512(password.encode("utf-8")).hexdigest()
Accounts = accounts()
# We avoid username swappage
if username in Accounts or not username:
if code:
Redirect(server, "/register?code="+code+"&userexists=True#user_name")
else:
Redirect(server, "/register?userexists=True#user_name")
return
# Validating the invite code
invited_by = ""
for account in Accounts:
if code in Accounts[account].get("invite_codes", []):
invited_by = account
break
if not invited_by:
Redirect(server, "/register?wrongcode=True")
return
# Now we can finally make our account.
# New account first
account = {
"username":username,
"bio":"",
"invite_codes":{},
"invited":[],
"invited_by":invited_by,
"password":hashed,
"title":username,
"email":"",
"website":"",
"mastodon":"",
"matrix":"",
"sessions":{
server.cookie:server.headers.get("User-Agent")
}
}
f = Set.Folder()
folder = f+"/accounts"
with open(folder+"/"+username+".json", "w") as save:
json.dump(account, save, indent=4)
# Now the invitor changes
account = Accounts[invited_by]
del account["invite_codes"][code]
account["invited"].append(username)
with open(folder+"/"+account.get("username", "")+".json", "w") as save:
json.dump(account, save, indent=4)
Redirect(server, "/settings")
# Notification
Notify(invited_by, "/account/"+Safe(username), "@"+Safe(username)+" has registered from your invitation.")
def LogOut(server):
user = validate(server.cookie)
# Authorization check
if not user:
AccessDenied(server)
return
cookie = server.parsed.get("cookie", [""])[0]
# This might be an attack. So we don't want that.
if cookie not in user.get("sessions",{}):
Redirect(server, "/")
return
del user["sessions"][cookie]
f = Set.Folder()
folder = f+"/accounts"
with open(folder+"/"+user.get("username", "")+".json", "w") as save:
json.dump(user, save, indent=4)
# If the user logged out this session
if cookie == server.cookie:
Redirect(server, "/")
else:
Redirect(server, "/settings#sessions")
def UpdateAccount(server):
user = validate(server.cookie)
# Authorization check
if not user:
AccessDenied(server)
return
keys = [
"title",
"avatar",
"bio",
"website",
"email",
"mastodon",
"matrix"
]
for key in keys:
data = server.parsed.get(key, [""])[0]
# Making sure to reverify email.
if key == "email" and user[key] != data:
user["email_verified"] = False
user[key] = Safe(data)
f = Set.Folder()
folder = f+"/accounts"
with open(folder+"/"+user.get("username", "")+".json", "w") as save:
json.dump(user, save, indent=4)
Redirect(server, "/settings")
def UpdatePassword(server):
user = validate(server.cookie)
old_password = server.parsed.get("password", [""])[0]
new_password = server.parsed.get("new_password", [""])[0]
old_hashed = hashlib.sha512(old_password.encode("utf-8")).hexdigest()
new_hashed = hashlib.sha512(new_password.encode("utf-8")).hexdigest()
# Validating the user's password
if user.get("password", "") == old_hashed:
user["password"] = new_hashed
f = Set.Folder()
folder = f+"/accounts"
with open(folder+"/"+user.get("username", "")+".json", "w") as save:
json.dump(user, save, indent=4)
Redirect(server, "/settings")
AccessDenied(server)
def UpdatePublicationRights(server):
user = validate(server.cookie)
# Authorization check
if not user:
AccessDenied(server)
return
Accounts = accounts()
account = server.parsed.get("account", [""])[0]
if account not in Accounts:
NotFound(server)
return
if not moderates(user.get("username", ""), account):
AccessDenied(server)
return
Tabs = tabs()
Account = Accounts[account]
if "editsIn" not in Account:
Account["editsIn"] = []
granted = []
revoked = []
for tab in Tabs:
if not editsIn(user.get("username", ""), tab):
AccessDenied(server)
return
tabOn = server.parsed.get(tab, [""])[0]
if tabOn and tab not in Account["editsIn"]:
Account["editsIn"].append(tab)
granted.append(tab)
elif not tabOn and tab in Account["editsIn"]:
Account["editsIn"].remove(tab)
revoked.append(tab)
f = Set.Folder()
folder = f+"/accounts"
with open(folder+"/"+account+".json", "w") as save:
json.dump(Account, save, indent=4)
Redirect(server, "/account/"+account)
# Notification
text = "@"+user.get("username", "")+" "
if granted:
text = text + "granted you publication rights in: "
for n, i in enumerate(granted):
text = text + i
if n != len(granted)-1:
text = text + ", "
if revoked:
text = text + " and "
if revoked:
text = text + "revoked your publication rights in: "
for n, i in enumerate(revoked):
text = text + i
if n != len(revoked)-1:
text = text + ", "
text = text + ". "
if granted or revoked:
Notify(account, "/account/"+account, text)
def DoComment(server):
# Limiting bots from commenting
if not server.cookie:
AccessDenied()
return
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]
wasnumber = number
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"]):
# Making sure moderators done get credit for small edits
# in comments.
originalcommet = article["comments"]["comments"][number]
if originalcommet.get("username") in Accounts:
comment["username"] = originalcommet["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))
if not wasnumber:
# Notification
username = user.get("username", nick)
if username != article.get("author"):
Notify(article.get("author"), url+placeRedirect+str(number), "@"+Safe(username)+" commented:
"+article.get("title", "")+"
"+Safe(text[:200])+"")
# Mention notifications
searchText = text.lower()
for account in Accounts:
# The author already got the notification.
if account == article.get("author"):
continue
name = Accounts[account].get("title", account)
if account.lower() in searchText or name.lower() in searchText:
Notify(account,
url+placeRedirect+str(number),
"@"+Safe(username)+" mentioned you:
"+article.get("title", "")+"
"+Safe(text[:200])+"")
def Publish(server):
user = validate(server.cookie)
# Authorization check
if not user:
AccessDenied(server)
return
config = Set.Load()
Tabs = tabs()
f = Set.Folder()
# Getting data to create
tab = server.parsed.get("tab", [""])[0]
name = server.parsed.get("name", [""])[0]
text = server.parsed.get("text", [""])[0]
title = server.parsed.get("title", [""])[0]
description = server.parsed.get("description", [""])[0]
thumbnail = server.parsed.get("thumbnail", [""])[0]
License = server.parsed.get("license", [""])[0]
recording = server.parsed.get("recording", [""])[0]
# Petition data
petition_goal = server.parsed.get("petition_goal", [""])[0]
petition_api = server.parsed.get("petition_api", [""])[0]
petition_api_key = server.parsed.get("petition_api_key", [""])[0]
petition_api_title = server.parsed.get("petition_api_title", [""])[0]
petition_api_link = server.parsed.get("petition_api_link", [""])[0]
# If this tab doesn't exist, this is an error.
if tab not in Tabs:
AccessDenied(server)
return
# Checking if the user has rights to post in here.
if not editsIn(user.get("username"), tab):
AccessDenied(server)
return
Articles = articles(tab)
if not name:
name = Simplify(title)
# Reading the file
if name in Articles:
metadata = Articles[name]
else:
metadata = {
"title":"",
"timestamp":time.time(),
"description":"",
"author":user.get("username", ""),
"thumbnail":"",
"license":"",
"views":{
"amount":0,
"viewers":[],
"dates":{}
},
"recording":"",
"comments":{
"comments":[],
"requests":[]
}
}
# Checking if the user can edit the posts of the
# author of this article.
if not moderates(user.get("username"), metadata.get("author", "")):
AccessDenied(server)
return
metadata["title"] = title
metadata["description"] = description
metadata["license"] = License
metadata["recording"] = recording
metadata["thumbnail"] = thumbnail
# Petition
if petition_goal:
petition = metadata.get("petition", {
"signed":0,
"signatures":[]
})
try:
petition["goal"] = int(petition_goal)
except:
petition["goal"] = 1
# API petition
if petition_api:
petition["api"] = {
"api" :petition_api,
"keys" :petition_api_key.split("/"),
"title":petition_api_title,
"link" :petition_api_link
}
metadata["petition"] = petition
else:
try:
del metadata["petition"]
except:
pass
# Save the changes
try:
os.makedirs(f+"/tabs/"+tab+"/"+name)
except:pass
with open(f+"/tabs/"+tab+"/"+name+"/metadata.json", "w") as save:
json.dump(metadata, save, indent=4)
with open(f+"/tabs/"+tab+"/"+name+"/text.md", "w") as save:
# Enabling HTML embedding only for the owner
if rank(user.get("username", "")) == 0:
save.write(text)
else:
save.write(Safe(text))
if metadata.get("petition"):
metadata["url"] = "/"+tab+"/"+name
try:
API.Petition(metadata)
except Exception as e:
Error(server, "Cannot Load API Value \n"+Safe(str(e)))
return
Redirect(server, "/"+tab+"/"+name)
def DeleteComment(server):
user = validate(server.cookie)
# Authorization check
if not user:
AccessDenied(server)
return
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)
def CancelInvite(server):
user = validate(server.cookie)
# Authorization check
if not user:
AccessDenied(server)
return
code = server.parsed.get("code", [""])[0]
if user:
del user["invite_codes"][code]
f = Set.Folder()
folder = f+"/accounts"
with open(folder+"/"+user.get("username", "")+".json", "w") as save:
json.dump(user, save, indent=4)
Redirect(server, "/settings#invites")
else:
Redirect(server, "/")
def CreateInvite(server):
user = validate(server.cookie)
# Authorization check
if not user:
AccessDenied(server)
return
nick = server.parsed.get("nick", [""])[0]
if not nick: nick = "Unknown"
code = RandString()
if user:
user["invite_codes"][code] = nick
f = Set.Folder()
folder = f+"/accounts"
with open(folder+"/"+user.get("username", "")+".json", "w") as save:
json.dump(user, save, indent=4)
Redirect(server, "/settings?code="+code+"#invite_"+code)
else:
Redirect(server, "/")
def Notify(username, link, text):
Accounts = accounts()
try:
account = Accounts[username]
if "notifications" not in account:
account["notifications"] = []
notification = {
"link":link,
"text":text,
"code":RandString(20)
}
account["notifications"].append(notification)
f = Set.Folder()
folder = f+"/accounts"
with open(folder+"/"+account.get("username", "")+".json", "w") as save:
json.dump(account, save, indent=4)
except Exception as e:
print(clr["bold"]+clr["tdrd"]+"Error:"+clr["norm"]+" Unable to set notification!", e, link, text)
def ReadNotification(server):
user = validate(server.cookie)
# Authorization check
if not user:
AccessDenied(server)
return
code = server.parsed.get("code", [""])[0]
try:
# Apparently I'm stupid to use a link here.
# But I already commited to it and I'm editing
# on a live server. So here we go... O.o
for n, notification in enumerate(user.get("notifications")):
if notification.get("code") == code:
break
n = user["notifications"].pop(n)
f = Set.Folder()
folder = f+"/accounts"
with open(folder+"/"+user.get("username", "")+".json", "w") as save:
json.dump(user, save, indent=4)
Redirect(server, n.get("link", "/"))
except Exception as e:
print(clr["bold"]+clr["tdrd"]+"Error:"+clr["norm"]+" Unable to read notification!", e)
def TimeDifference(timeA, timeB):
text = ""
if timeA < timeB:
if timeB - timeA < 10:
text = "now"
return text
elif timeB - timeA < 60:
text = str(int(timeB - timeA))+" seconds ago"
return text
else:
if int( ( timeB - timeA ) / 60 ) == 1:
text = str(int( ( timeB - timeA ) / 60 ))+" minute ago"
else:
text = str(int( ( timeB - timeA ) / 60 ))+" minutes ago"
return text
else:
if timeA - timeA < 10:
text= "now"
return text
elif timeA - timeB < 60:
text = "in "+str(int(timeA - timeB))+" seconds"
return text
else:
if int( ( timeA - timeB ) / 60 ) == 1:
text = "in "+str(int( ( timeA - timeB ) / 60 ))+" minute"
else:
text = "in "+str(int( ( timeA - timeB ) / 60 ))+" minutes"
return text
def RSS(server):
# Rendering rss feed.
Articles = allArticles()
config = Set.Load()
favicon = config.get("favicon", "")
if favicon.startswith("/"):
favicon = "https://"+config.get("domain", "example.com")+favicon
author = server.parsed.get("author", [""])[0]
title = config.get("title", "My Website")
if author:
Accounts = accounts()
account = Accounts.get(author, {})
title = account.get("title", author)+" at: "+title
rss = """
"""+title+"""
https://"""+config.get("domain", "example.com")+"""
"""+config.get("tagline", "")+""""""+favicon+""""""+title+"""
https://"""+config.get("domain", "example.com")+"""
"""
n = 0
for article in Articles:
if author and author != Articles[article].get("author", ""):
continue
n += 1
if n > 10:
break
pubDate = Articles[article].get("timestamp", 0)
pubDate = datetime.fromtimestamp(pubDate)
pubDate = email.utils.format_datetime(pubDate)
rss = rss + """
"""+Articles[article].get("title", article).replace("&", "&")+"""
https://"""+config.get("domain", "example.com")+Articles[article].get("url", "")+"""
https://"""+config.get("domain", "example.com")+Articles[article].get("url", "")+""""""+pubDate+"""
"""
rss = rss + """
"""
send(server, rss, 200)
def Search(server):
Articles = allArticles()
Tabs = tabs()
config = Set.Load()
# Generating
html = head(title = "Search",
description = "",
config = config
)
html = html + Button(config.get("title", "My Website"), "/", image=config.get("favicon", "/icon/internet"))
# The place where you can type your search
text = server.parsed.get("text",[""])[0]
try: page = int(server.parsed.get("page", ["0"])[0])
except Exception as e:
print(e)
page = 0
searchtitle = server.parsed.get("title",[""])[0]
searchauthor = server.parsed.get("author",[""])[0]
searchpost = server.parsed.get("post",[""])[0]
searchdescription = server.parsed.get("description",[""])[0]
searchcomments = server.parsed.get("comments",[""])[0]
# Supporting legacy search links
if not any([searchtitle,
searchauthor,
searchpost,
searchdescription,
searchcomments
]):
searchtitle = True
searchpost = True
searchdescription = True
searchcomments = True
checkedtitle = ""
if searchtitle: checkedtitle = " checked "
checkedauthor = ""
if searchauthor: checkedauthor = " checked "
checkedpost = ""
if searchpost: checkedpost = " checked "
checkeddescription = ""
if searchdescription: checkeddescription = " checked "
checkedcomments = ""
if searchcomments: checkedcomments = " checked "
html = html + """
"""
# Acutally doing the searching
counted = []
for article in Articles:
points = 0
# Title x 100 points
if searchtitle:
title = Articles[article].get("title", article)
points += title.lower().count(text.lower()) * 100
# Description x 10 points
if searchdescription:
description = Articles[article].get("description", "")
points += description.lower().count(text.lower()) * 10
# Author
if searchauthor:
author = Articles[article].get("author", "")
if author == text:
points += 2 # Perfect match with username preffered
# People might also look at the username
Accounts = accounts()
if text.lower() == Accounts.get(author, {}).get("title", "").lower():
points += 1
# Comments x 1
if searchcomments:
comments = Articles[article].get("comments", {}).get("comments", {})
for comment in comments:
commentText = comment.get("text", "")
points += commentText.lower().count(text.lower())
# Post Text x 1
if searchpost:
try:
f = Set.Folder()
url = Articles[article].get("url")
postText = open(f+"/tabs/"+url+"/text.md").read()
points += postText.lower().count(text.lower())
except Exception as e:
print(e)
if points:
counted.append([points, article])
counted = list(reversed(sorted(counted)))
Buffer = 16
From = Buffer*page
To = From+Buffer
urlNoPage = server.path
if "page=" in urlNoPage: urlNoPage = urlNoPage[:urlNoPage.rfind("&")]
if len(list(counted)) > Buffer:
if page > 0:
html = html + Button(str(page-1), urlNoPage+"&page="+str(page-1), "left")
html = html + '
'+str(page)+'
'
if To < len(list(counted))-1:
html = html + Button(str(page+1), urlNoPage+"&page="+str(page+1), "right")
html = html + """
"""
rendered = 0
for n, article in enumerate(counted):
points, article = article
if n < From: continue
if n >= To: break
html = html + ArticlePreview(Articles[article], Tabs, server.cookie)
rendered += 1
html = html + '
'
if len(list(counted)) > Buffer:
if page > 0:
html = html + Button(str(page-1), urlNoPage+"&page="+str(page-1), "left")
html = html + '
'+str(page)+'
'
if To < len(list(counted))-1:
html = html + Button(str(page+1), urlNoPage+"&page="+str(page+1), "right")
html = html + LoginButton(server)
send(server, html, 200)