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

[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 ) 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 + """
[icon petition] Sign
""" else: html = html + """
[icon petition] Sign

This petition is signed by increasing the number of
"""+petition.get("api", {}).get("title", "")+"""

"""+Button("Continue", petition.get("api", {}).get("link", ""), "ok")+"""
""" html = html + '
' # License 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+'' 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 + '
' 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] 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 + """




""" if redirect: html = html + '' 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
""" 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 + """

Email Settings


Email """+Safe(user.get("email"))+""" is verified!

""" # 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 + '
' 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 Petition if rank(user.get("username","")) == 0: petition = article.get("petition", {}) petition_goal = petition.get("goal", "") petition_api = petition.get("api", {}).get("api", "") petition_api_key = petition.get("api", {}).get("keys", []) petition_api_title = petition.get("api", {}).get("title", "") petition_api_link = petition.get("api", {}).get("link", "") if len(petition_api_key) == 1: petition_api_key = petition_api_key[0] else: key = petition_api_key[0] for n, i in petition_api_key: if n != 0: key = key + "/" + i petition_api_key = key html = html + """
[icon petition] Petition

[icon ok]
[icon analytics] From API
[icon link]
[icon checlist]
[icon scene]
[icon link]
""" 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]
' 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 = '[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 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 + """

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)