diff --git a/icons/user_link.png b/icons/user_link.png new file mode 100644 index 0000000..f62c277 Binary files /dev/null and b/icons/user_link.png differ diff --git a/icons/user_new.png b/icons/user_new.png new file mode 100644 index 0000000..2aec0da Binary files /dev/null and b/icons/user_new.png differ diff --git a/modules/Account.py b/modules/Account.py new file mode 100644 index 0000000..141685d --- /dev/null +++ b/modules/Account.py @@ -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") diff --git a/modules/Common.py b/modules/Common.py index c53b26c..128821d 100644 --- a/modules/Common.py +++ b/modules/Common.py @@ -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() diff --git a/modules/Help.py b/modules/Help.py index 8ebdf7f..49a0dfb 100644 --- a/modules/Help.py +++ b/modules/Help.py @@ -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() + diff --git a/modules/Legacy.py b/modules/Legacy.py index 8a199bf..ae50801 100644 --- a/modules/Legacy.py +++ b/modules/Legacy.py @@ -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 diff --git a/modules/Render.py b/modules/Render.py index 2f922b8..cf81642 100644 --- a/modules/Render.py +++ b/modules/Render.py @@ -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 + '
' html = html + '
' - html = html +"

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

" + html = html +"

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

" + # Rank + + Rank = rank(account) + html = html + Button("Rank "+str(Rank), "", icon="analytics") + + + html = html + '
' + # 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 + '
' - html = html + '' - html = html + ' '+mastodon+'' - html = html + '
' + mastolink = "https://"+mastodon[1:].split("@")[1]+"/@"+mastodon[1:].split("@")[0] + + html = html + '
' + html = html + '' + html = html + ' '+mastodon+'' + html = html + '
' + except: + pass # Matrix @@ -593,11 +620,11 @@ def AccountPage(server, account): html = html + '
' - html = html + '
' - html = html +"
Invited by:
"+User(Accounts.get(account, {}).get("invited_by", account))+"
" - - - 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: @@ -670,13 +697,17 @@ def LoginPage(server): html = html + """ +
+ +
+
- +
-
+
- Don't have an account? Register. +
+ +
+ Don't have an account?
""" + html = html + Button("Register", "/register", icon="user_new") + + send(server, html, 200) - html = html + '' +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 = "Login", + description = "Login", + 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) + + if not user: + Redirect(server, "/login") + return + config = Set.Load() # Generating @@ -757,12 +891,71 @@ def SettingsPage(server):
-
- """ + # 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 + """ + +
+ + + + + +
+ + """ + send(server, html, 200) + ### @@ -1078,7 +1271,7 @@ def LoginButton(server): if not user: html = html + '' - html = html + ' Login' + html = html + ' Login' html = html + '' 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, "/") diff --git a/modules/Run.py b/modules/Run.py index 254b06f..bbfd6ba 100644 --- a/modules/Run.py +++ b/modules/Run.py @@ -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("?")] diff --git a/modules/Set.py b/modules/Set.py index 84c73c4..315eb6e 100644 --- a/modules/Set.py +++ b/modules/Set.py @@ -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"]) + diff --git a/run.py b/run.py index a9d8c70..35d3df7 100644 --- a/run.py +++ b/run.py @@ -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