#!/bin/python3 ''' Basic guestbook server with owner reply feature. Copyright (C) 2025 Swirly "Stoner" Curly This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . this is a basic guestbook server. it takes only two batteries: a base.html with a `` somewhere in it that will be replaced, and a config.json file with the following as a base: { "ownerName": "Swirly", "writeProtect": false, "email": { "enabled": true, "server": "disroot.org" }, "sound": { "enabled": false, "file": "/opt/gb.wav" }, "blockList": ["hate"] } there's also an optional battery: a emaillogin.txt with the username then the password both in plaintext on separate lines (only needed if email on comment is enabled) this html file cannot access other files within the server it is on -- you should use inline css/js if possible or use some external server for other files (catbox, sgfs...) this reads from a data.json with a specific format and formats it for use in a webpage, then distributes that webpage. the data.json is in the following format: { "signatures": [ { "name": "name", "date": "just a string", "content": "example with no reply from creator", "reply": null }, { "name": "name 2", "date": "atesteg", "content": "example with creator reply", "reply": "hi guy" } ] } if anything bad or confusing arises, contact me! (https://swirly.moe) ''' from http.server import ThreadingHTTPServer, BaseHTTPRequestHandler import os, socket, sys, json, cgi, datetime, threading, subprocess from redmail import EmailSender with open("config.json", "r") as f: config = json.load(f) def intersperse(lst, item): result = [item] * (len(lst) * 2 - 1) result[0::2] = lst return result def generate_gb(): #configure everything here! if not os.path.exists("data.json"): with open("data.json", "w") as f: f.write('{"signatures": []}') with open("data.json", "r") as f: funkyshit = json.load(f) myfuckinggod = [] for signature in funkyshit['signatures']: myfuckinggod.append(f"""

{signature['name']}

{signature['date']}

{signature['content']}

{'

    From '+config["ownerName"]+': '+signature['reply']+'

' if signature['reply'] else ""} """) return '\n'.join(myfuckinggod) class bitch(BaseHTTPRequestHandler): def do_GET(self): if self.path == "/": self.send_response(200) self.send_header('content-type', 'text/html; charset=utf-8') with open("./base.html", "r") as f: contents = f.read() contents = bytes(contents.replace("", f""" {generate_gb()} """), "utf-8") self.end_headers() self.wfile.write(contents) elif self.path == "/source.py": self.send_response(200) self.send_header('content-type', 'text/plain') self.end_headers() with open(os.path.abspath(os.path.realpath(sys.argv[0])), "rb") as f: contents = f.read() self.wfile.write(contents) else: self.send_response(400) self.send_header('content-type', 'text/plain') self.end_headers() self.wfile.write(bytes("mrrp :3c", "utf-8")) def do_HEAD(self): bitch.do_GET(self) def do_POST(self): if self.path == "/": if config["writeProtect"]: self.send_response(403) self.send_header("Content-type", "text/html") self.end_headers() self.wfile.write(bytes('

WRITE-PROTECT ENABLED


Write-protect has been enabled for this instance.

', 'utf-8')) else: email_on_write = config["email"]["enabled"] sound_on_write = config["sound"]["enabled"] content_type, _ = cgi.parse_header(self.headers['content-type']) if content_type == 'multipart/form-data': form_data = cgi.FieldStorage( fp=self.rfile, headers=self.headers, environ={'REQUEST_METHOD': 'POST'} ) if 'name' in form_data and 'content' in form_data: cname = form_data['name'].value content = form_data['content'].value reject = False for i in config["blockList"]: if i.lower() in cname or i.lower() in content: reject = True if reject: self.send_response(403) self.send_header("Content-type", "text/html") self.end_headers() self.wfile.write(bytes('

FILTERED


This instance has filtered your entry. This will be recorded.

', 'utf-8')) if not os.path.exists("filter-log.txt"): with open("filter-log.txt", "w") as f: f.write("") with open("filter-log.txt", "a") as f: f.write(f"""------- FILTERED ENTRY FROM {cname} on {datetime.datetime.now().strftime("%G-%m-%d %H:%M")}: {content} """) return else: replacelist = { "&": "&", "<": "<", ">": ">", "'": "'", "\"": """, "\n": "
" } for i1, i2 in replacelist.items(): cname = cname.replace(i1, i2) content = content.replace(i1, i2) with open("data.json", "r") as f: funkyshitepisode2 = json.load(f) cdate = datetime.datetime.now().strftime("%G-%m-%d %H:%M") funkyshitepisode2['signatures'].insert(0, {"name": cname[:15] + "..." if len(cname) > 15 else cname, "date": cdate, "content": (content if len(content.split('
')) < 6 else "
".join(content.split("
")[:5]) + "

Lines past line 5 omitted...") if len(content) < 1024 else content[:1023] + "...

truncated past 1023 chars", "reply": None}) with open("data.json", "w") as f: f.write(json.dumps(funkyshitepisode2, indent=4)) def email_send(): try: if email_on_write: with open("emaillogin.txt", "r") as f: bals = f.read() un = bals.split("\n")[0] pswd = bals.split("\n")[1] email = EmailSender( host=config["email"]["server"], port=587, #should stay the same unless chosen email server changed port (in that case why????????) username=f"{un}@{config['email']['server']}", password=pswd ) print(f"{un}@{config['email']['server']}") email.send( sender=f"{un}@{config['email']['server']}", receivers=[f"{un}@{config['email']['server']}"], subject="New signature!", text=f"From {cname} on {cdate}\n\n{content}", html=f"

New signature!


From {cname} on {cdate}

{content}

" ) except: print("NO") def sound_play(): try: if sound_on_write: subprocess.Popen("machinectl shell --uid=1000 .host /usr/bin/paplay " + config["sound"]["file"], shell=True) except: print("NO") threading.Thread(target=email_send, daemon=True).start() threading.Thread(target=sound_play, daemon=True).start() else: self.send_response_only(400) return else: self.send_response_only(400) return self.send_response(303) self.send_header("Location", "/") self.send_header("Content-type", "text/html") self.end_headers() self.wfile.write(bytes('

YOUR BROWSER HAS NO SUPPORT FOR HTTP 303


please go to /

', 'utf-8')) httpd = ThreadingHTTPServer(("0.0.0.0", 4004), bitch) #this will serve at port 4004. replace with 80 if you don't want to type in :4004 at the end of your address. if port 80 fails, likely another (probably better) web server is using port 80. on windows you must disable the HTTP service/feature. httpd.serve_forever()