# 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 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[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 + """

[LOGO]

"""+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 +"

"+Articles.get(article, {}).get("title", article)+"

" # 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: html = html + Safe(referrer) + " : 👁 "+str(referrers[referrer]) + "
\n" html = html + """
""" html = html + '
' License = Articles.get(article, {}).get("license", "") if License: html = html + '
' html = html + 'License:
' html = html + '' html = html + Licenses.get(License, {}).get("name")+'' html = html + '
' # Audio recording of the article recording = Articles.get(article, {}).get("recording", "") if recording: html = html + '
' html = html + 'If you are going to skim, better listen to it instead.

' html = html + '' html = html + '

' html = html + '
' html = html + markdown.convert(f+"/tabs/"+tab+"/"+article+"/text.md") html = html + '
' # RSS rsslink = "https://"+config.get("domain", "")+"/rss" authorrsslink = rsslink+"?author="+Safe(Articles.get(article, {}).get("author","")) html = html + """
Subscribe RSS
"""+Button("Author", authorrsslink, "link")+"""
"""+Button("Website", rsslink, "link")+"""
""" # 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 + '
' html = html + '
' html = html +"

"+Accounts.get(account, {}).get("title", account)+"

" # 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+'' 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 + '
' html = html + '' for tab in validates: checked = "" if editsIn(account, tab): checked = 'checked=""' html = html + '
' html = html + '' + Tabs[tab].get("title", tab) html = html + '
' html = html + """

""" 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] # 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 + """
""" if wrongcode: html = html + "Invalid Invite Code.

" if not code and not user: html = html + """ """ username = "" else: for account in Accounts: if code in Accounts[account].get("invite_codes", []): username = Simplify(Accounts[account]["invite_codes"][code], "file") html = html + '
Invited by:

' html = html + User(account)+'
' html = html + '' break if not user: if userexists: html = html + "Username is taken.

" html = html + """

""" elif code: html = html + "Share this page with those you invited." else: html = html + "
You already registered and logged in." 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


Bio
""" # 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 + """
""" notifications = user.get("notifications","") if notifications: html = html + '
' for notification in notifications: html = html + '
' html = html + '
' html = html + notification.get("text", "Notification") html = html + '

' html = html + '
' send(server, html, 200) def EditorPage(server): user = validate(server.cookie) # Authorization check if not user: AccessDenied(server) return config = Set.Load() Tabs = tabs() f = Set.Folder() # Generating html = head(title = "Editor", description = "Editor", config = config ) html = html + Button(config.get("title", "My Website"), "/", image=config.get("favicon", "/icon/internet")) tab = server.parsed.get("tab", [""])[0] name = server.parsed.get("name", [""])[0] if tab in Tabs: Articles = articles(tab) else: Articles = {} url = tab+"/"+name article = Articles.get(name, {}) if article: try: text = open(f+"/tabs/"+tab+"/"+name+"/text.md") text = text.read() except: text = "" else: text = "" html = html + """
""" if tab in Tabs: html = html + ' ' html = html + Tabs.get(tab, {}).get("title", tab) html = html + '
' html = html + '' else: html = html + """ """ if name: html = html + '' html = html + """ """ for l in Licenses: html = html + '' html = html + """ """ # Optional release options if rank(user.get("username","")) == 0: html = html + """
[icon unlock] Unlock


For unlockable files that will be published after a certain statistics is reached.


[icon file]
[icon link]
[icon checlist]
[icon ok]
[icon scene]
""" html = html + """

""" html = html + LoginButton(server) send(server, html, 200) ### def Button(text, link, icon="", image=""): html = """ """ if icon: html = html + """ [icon """ if image: html = html + """ [icon """ html = html + """ """+text+""" """ return html def ArticlePreview(article, Tabs, cookie=""): html = """
""" url, tab = article.get("url", ""), article.get("tab","") sup = "" if cookie not in article.get("views", {}).get("viewers", ""): sup = '
  Unread  

' html = html + '

' html = html + '[icon '+tab+']' html = html + article.get("title", "")+"

"+sup+"\n" if article.get("thumbnail"): html = html + '
[thumbnail]
' 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 = '[avatar]  '+title+'
\n' else: html = '[avatar]  '+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 = """

""" if not user: html = html + '' html = html + '' else: html = html + User(user.get("username", ""))+'
' html = html + """

""" return html def CommentEditInput(server, comment, url, n=0, user=None, request=None): Accounts = accounts() html = '
' html = html + """
""" if request: html = html + '' html = html + '
  Pending Approval  

' account = comment.get("username", "") if account in accounts(): html = html + User(Safe(account)) elif account: html = html + '' html = html + '' warning = comment.get("warning", "") html = html + """
""" html = html + """' html = 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 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 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() 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" with open(folder+"/"+username+".json", "w") as save: json.dump(account, save, indent=4) # 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) Redirect(server, "/settings") 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] 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 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): 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] # 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 # 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)) 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 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 + """

Title
Author
Post Text
Description
Comments
""" # 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)