First fully functional website

This commit is contained in:
BlenderDumbass 2024-11-24 23:28:19 +02:00
parent 0c424ccb3d
commit c5eef007aa
10 changed files with 514 additions and 26 deletions

BIN
icons/user_link.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

BIN
icons/user_new.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

109
modules/Account.py Normal file
View file

@ -0,0 +1,109 @@
# AGPL 3 or any later version
# (C) J.Y.Amihud ( Blender Dumbass )
import os
import sys
import json
import random
import hashlib
import urllib.parse
from datetime import datetime
from getpass import getpass
from modules import Set
from modules import markdown
from modules.Common import *
def Load(name):
folder = Set.Folder()+"/accounts/"
try:
with open(folder+name+".json") as o:
return json.load(o)
except:
return {}
def Save(account):
name = account.get("username", "")
folder = Set.Folder()+"/accounts/"
try:
with open(folder+name+".json", "w") as save:
json.dump(account, save, indent=4)
except Exception as e:
print(clr["bold"]+clr["tdrd"]+"Error:"+clr["norm"]+" Cannot save account!", e)
def Account():
if len(sys.argv) < 3:
from modules import Help
Help.Accounts()
if "--list" in sys.argv:
List()
elif "--add" in sys.argv:
try:
account = sys.argv[ sys.argv.index("--add") + 1]
if "--" in account: 1/0 # Failing this for the error message.
Add(account)
except Exception as e:
print(clr["bold"]+clr["tdrd"]+"Error:"+clr["norm"]+" Account name wasn't specified!")
print('Use: $ python3 run.py --account --add blenderdumbass')
elif "--edit" in sys.argv:
try:
account = sys.argv[ sys.argv.index("--edit") + 1]
if "--" in account: 1/0 # Failing this for the error message.
Edit(account)
except Exception as e:
print(clr["bold"]+clr["tdrd"]+"Error:"+clr["norm"]+" Account name wasn't specified!")
print('Use: $ python3 run.py --account --edit blenderdumbass')
def List():
folder = Set.Folder()+"/accounts"
for account in os.listdir(folder):
if account.endswith(".json"):
print(account.replace(".json", ""))
def Add(name):
# Adds a new account.
account = Load(name)
if account:
print(clr["bold"]+clr["tdrd"]+"Error:"+clr["norm"]+" Account already exists.")
account = {
"username":name,
"bio":"",
"invite_codes":{},
"invited":[],
"invited_by":"",
"password":hashlib.sha512(getpass("Account's Password: ").encode("utf-8")).hexdigest(),
"title":name,
"email":"",
"website":"",
"mastodon":"",
"matrix":"",
"sessions":{}
}
Save(account)
print(clr["bold"]+clr["tbyl"]+name+clr["norm"]+" is added."+clr["norm"])
def Edit(name):
folder = Set.Folder()+"/accounts/"
os.system("nano "+folder+name+".json")

View file

@ -52,7 +52,10 @@ def Simplify(text, extrasimple=True):
good = "QWERTYUIOPLKJHGFDSAZXCVBNMqwertyuiopasdfghjklzxcvbnm.1234567890-_:* "
if extrasimple:
if extrasimple == "file":
good = "QWERTYUIOPLKJHGFDSAZXCVBNMqwertyuiopasdfghjklzxcvbnm.1234567890-_"
elif extrasimple:
good = "qwertyuiopasdfghjklzxcvbnm.1234567890-_:"
text = text.lower()

View file

@ -10,9 +10,13 @@ def Help():
print()
print(clr["tdyl"]+"--help"+clr["norm"]+" - Prints this Help Page.")
print(clr["tdyl"]+"--run"+clr["norm"]+" - Run the server.")
print()
print(clr["tdyl"]+"--set"+clr["norm"]+" - Use for changing settings.")
print(clr["tdyl"]+"--account"+clr["norm"]+" - Manage Accounts.")
print()
print(clr["tdyl"]+"--config"+clr["norm"]+" - Edit config file directly, bypassing --set.")
print(clr["tdyl"]+"--folder"+clr["norm"]+" - Open the folder of the website's data.")
print()
print(clr["tdyl"]+"--transfer"+clr["norm"]+"- Transfers legacy website data to the new format.")
print()
@ -32,3 +36,15 @@ def Set():
print(clr["tdyl"]+"--add_tab"+clr["norm"]+" - Adds a category of articles.")
print(clr["tdyl"]+"--edit_tab"+clr["norm"]+" - Edit the config of a category.")
print()
def Accounts():
print()
print(clr["bold"]+" BDServer Account Help "+clr["norm"])
print()
print(" Account is used to manage website's accounts.")
print()
print(clr["tdyl"]+"--add"+clr["norm"]+" - Add a new account.")
print(clr["tdyl"]+"--list"+clr["norm"]+" - List accounts.")
print()

View file

@ -139,6 +139,13 @@ def Transfer(location):
account = accounts[username]
# We are updating the invites
invite_codes = account.pop("invite_codes")
account["invite_codes"] = {}
for code in invite_codes:
account["invite_codes"][code] = "Unknown"
# We are adding some new stuff to accounts
account["title"] = username # Visible title

View file

@ -138,6 +138,22 @@ def moderates(moderator, user):
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 articles(tab):
@ -530,8 +546,16 @@ def AccountPage(server, account):
html = html + '<div class="middle_section_article">'
html = html + '<div class="dark_box">'
html = html +"<center><h1>"+Accounts.get(account, {}).get("title", account)+"</h1></center>"
html = html +"<center><h1>"+Accounts.get(account, {}).get("title", account)+"</h1>"
# Rank
Rank = rank(account)
html = html + Button("Rank "+str(Rank), "", icon="analytics")
html = html + '</center>'
# Website
website = Safe(Accounts.get(account, {}).get("website" , ""))
@ -561,16 +585,19 @@ def AccountPage(server, account):
if mastodon:
# It could be mastodon url and not handle.
if "/" in mastodon:
mastodon = mastodon.replace("https://", "").replace("http://", "")
mastodon = mastodon.split("/")[1]+"@"+mastodon.split("/")[0]
try:
if "/" in mastodon:
mastodon = mastodon.replace("https://", "").replace("http://", "")
mastodon = mastodon.split("/")[1]+"@"+mastodon.split("/")[0]
mastolink = "https://"+mastodon[1:].split("@")[1]+"/@"+mastodon[1:].split("@")[0]
html = html + '<center>'
html = html + '<img style="vertical-align: middle" src="/icon/mastodon">'
html = html + '<a href="'+mastolink+'"> '+mastodon+'</a>'
html = html + '</center>'
mastolink = "https://"+mastodon[1:].split("@")[1]+"/@"+mastodon[1:].split("@")[0]
html = html + '<center>'
html = html + '<img style="vertical-align: middle" src="/icon/mastodon">'
html = html + '<a href="'+mastolink+'"> '+mastodon+'</a>'
html = html + '</center>'
except:
pass
# Matrix
@ -593,11 +620,11 @@ def AccountPage(server, account):
html = html + '</div>'
html = html + '<div class="dark_box">'
html = html +"<center>Invited by:<br>"+User(Accounts.get(account, {}).get("invited_by", account))+"</center>"
html = html + '</div>'
invited_by = Accounts.get(account, {}).get("invited_by", "")
if invited_by:
html = html + '<div class="dark_box">'
html = html +"<center>Invited by:<br>"+User(invited_by)+"</center>"
html = html + '</div>'
bio = Safe(Accounts.get(account, {}).get("bio" , ""))
if bio:
@ -670,13 +697,17 @@ def LoginPage(server):
html = html + """
<div class="middle_section_article">
<div class="dark_box">
<form action="do_login">
<img style="vertical-align: middle" src="/icon/user">
<input class="button" style="width:36.5%" maxlength="500" id="user_name" required="" name="user_name" pattern="[A-Za-z0-9]*" placeholder="Username..."></input>
<input class="button" style="width:90%" maxlength="500" id="user_name" required="" name="user_name" pattern="[A-Za-z0-9\.\-\_\]*" placeholder="Username..."></input>
<br>
<img style="vertical-align: middle" src="/icon/lock">
<input class="button" style="width:36.5%" maxlength="500" id="password" type="password" required="" name="password" placeholder="Password..."></input><br>
<input class="button" style="width:90%" maxlength="500" id="password" type="password" required="" name="password" placeholder="Password..."></input><br>
<button class="button" type="submit">
<img style="vertical-align: middle" src="/icon/unlock">
@ -685,20 +716,123 @@ def LoginPage(server):
</form>
<br>
</div></div>
Don't have an account? <a href="/register">Register</a>.
<div class="checklist_section_article">
<div class="dark_box">
Don't have an account? <br>
"""
html = html + Button("Register", "/register", icon="user_new")
send(server, html, 200)
html = html + '</center>'
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 <head>
html = head(title = "Login",
description = "Login",
config = config
)
html = html + Button(config.get("title", "My Website"), "/", image=config.get("favicon", "/icon/internet"))
html = html + '<center>'
html = html + """
<div class="middle_section_article">
<div class="dark_box">
<form action="do_register">
"""
if wrongcode:
html = html + "Invalid Invite Code.<br><br>"
if not code and not user:
html = html + """
<img style="vertical-align: middle" src="/icon/user_link">
<input class="button" style="width:90%" maxlength="500" id="code" required="" name="code" placeholder="Invite code..."></input>
"""
username = ""
else:
for account in Accounts:
if code in Accounts[account].get("invite_codes", []):
username = Simplify(Accounts[account]["invite_codes"][code], "file")
html = html + '<center>Invited by:<br><br>'
html = html + User(account)+'<br>'
html = html + '<input type="hidden" name="code" value="'+code+'">'
break
if not user:
if userexists:
html = html + "Username is taken.<br><br>"
html = html + """<img style="vertical-align: middle" src="/icon/user">
<input class="button" style="width:90%" maxlength="500" id="user_name" required="" name="user_name" pattern="[A-Za-z0-9\.\-\_\]*" placeholder="Username..." value=\""""+username+""""></input>
<br>
<img style="vertical-align: middle" src="/icon/lock">
<input class="button" style="width:90%" maxlength="500" id="password" type="password" required="" name="password" placeholder="Password..."></input><br>
<button class="button" type="submit">
<img style="vertical-align: middle" src="/icon/user_new">
Register
</button>
"""
elif code:
html = html + "Share this page with those you invited."
else:
html = html + "<br>You already registered and logged in."
html = html + """
</form>
</div></div>
<div class="checklist_section_article">
<div class="dark_box">
Have an account? <br>
"""
html = html + Button("Login", "/login", icon="unlock")
send(server, html, 200)
def SettingsPage(server):
user = validate(server.cookie)
if not user:
Redirect(server, "/login")
return
config = Set.Load()
# Generating <head>
@ -757,12 +891,71 @@ def SettingsPage(server):
</form>
</div>
</div>
"""
# Current Logged in Sessions
sessions = user.get("sessions", {})
html = html + '<div class="dark_box" id="sessions">'
html = html + '<center><h2>Active Sessions</h2>'
for cookie in sessions:
session = sessions[cookie]
CancelButton = Button("Log Out", "/log_out?cookie="+cookie, icon="cancel")
if server.cookie == cookie:
html = html + '<br><img title="This Session" style="vertical-align: middle" src="/icon/checked">'
else:
html = html + '<br><img title="Other Browser" style="vertical-align: middle" src="/icon/unchecked">'
html = html + '<input class="button" style="width:50%" value="' + session + '">'+CancelButton
html = html + '<br><br>'
html = html + '</center>'
html = html + '</div>'
# Invites and Invite codes
invite_codes = user.get("invite_codes", {})
html = html + '<div class="dark_box" id="invites">'
html = html + '<center><h2>Invites</h2></center>'
for code in invite_codes:
nick = invite_codes[code]
Open = ""
if code == server.parsed.get("code", [""])[0]:
Open = "open"
html = html + '<details '+Open+' class="dark_box"><summary id="invite_'+code+'" class="button">'
html = html + '<img style="vertical-align: middle" src="/icon/user">'+nick
html = html + '</summary><br>'
html = html + '<img style="vertical-align: middle" src="/icon/user_link">'
html = html + '<input class="button" style="width:90%" value="' + code + '">'
html = html + Button("Share Link", "/register?code="+code, icon="link")
html = html + Button("Cancel", "/cancel_invite?code="+code, icon="cancel")
html = html + '</details>'
html = html + """
<form action="/create_invite">
<input name="nick" class="button" required="" style="width:50%" placeholder="Name of the person you invite">
<button class="button" type="submit">
<img style="vertical-align: middle" src="/icon/user_new">
Invite
</button>
</form>
"""
send(server, html, 200)
###
@ -1078,7 +1271,7 @@ def LoginButton(server):
if not user:
html = html + '<a class="button" href="/login">'
html = html + '<img style="vertical-align: middle" src="/icon/user"> Login'
html = html + '<img style="vertical-align: middle" src="/icon/unlock"> Login'
html = html + '</a>'
else:
@ -1161,10 +1354,109 @@ def Login(server):
Redirect(server, "/")
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")
def LogOut(server):
user = validate(server.cookie)
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)
if not user:
Redirect(server, "/login")
return
keys = [
"title",
"avatar",
@ -1259,6 +1551,14 @@ def DoComment(server):
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
@ -1311,3 +1611,36 @@ def DeleteComment(server):
redirect = "#comments"
Redirect(server, url+redirect)
def CancelInvite(server):
user = validate(server.cookie)
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)
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, "/")

View file

@ -74,6 +74,9 @@ class handler(BaseHTTPRequestHandler):
elif self.path[1:].startswith("login"):
Render.LoginPage(self)
elif self.path[1:].startswith("register"):
Render.RegisterPage(self)
elif self.path[1:].startswith("settings"):
Render.SettingsPage(self)
@ -86,9 +89,21 @@ class handler(BaseHTTPRequestHandler):
elif self.path[1:].startswith("update_account"):
Render.UpdateAccount(self)
elif self.path[1:].startswith("create_invite"):
Render.CreateInvite(self)
elif self.path[1:].startswith("cancel_invite"):
Render.CancelInvite(self)
elif self.path[1:].startswith("log_out"):
Render.LogOut(self)
elif self.path[1:].startswith("do_login"):
Render.Login(self)
elif self.path[1:].startswith("do_register"):
Render.Register(self)
elif self.path.startswith("/graph/"):
url = self.path[6:]
if "?" in url: url = url[:url.find("?")]

View file

@ -45,7 +45,7 @@ def Save(data):
try:
with open(Folder()+"/config.json", "w") as save:
json.dump(data, save, indent=4, sort_keys=True)
json.dump(data, save, indent=4)
except Exception as e:
print(clr["bold"]+clr["tdrd"]+"Error:"+clr["norm"]+" Cannot save config!", e)
@ -221,3 +221,4 @@ def SetFavicon(filename):
Save(config)
print("New favicon is set at "+clr["bold"]+Folder()+"/pictures/favicon.png"+clr["norm"])

4
run.py
View file

@ -15,6 +15,10 @@ elif "--set" in sys.argv:
from modules import Set
Set.Set()
elif "--account" in sys.argv:
from modules import Account
Account.Account()
elif "--config" in sys.argv:
from modules import Set
data = Set.Load() # Making sure