From e13df2948a220c37f99f78d65e5496b1efeaca90 Mon Sep 17 00:00:00 2001 From: opera Date: Wed, 18 Mar 2026 17:27:43 +0100 Subject: [PATCH] bruh --- .gitignore | 6 + .vscode/settings.json | 3 + LICENSE | 13 ++ app.py | 226 +++++++++++++++++++++++++++++ config.ex.py | 1 + eee.py | 2 + filedb.py | 190 +++++++++++++++++++++++++ frontend-gpt/index.html | 51 +++++++ frontend-gpt/login.html | 38 +++++ frontend-gpt/post.html | 24 ++++ frontend-gpt/publish_post.html | 49 +++++++ frontend-gpt/register.html | 37 +++++ frontend-gpt/static/main.js | 250 +++++++++++++++++++++++++++++++++ frontend-gpt/static/style.css | 82 +++++++++++ frontend-gpt/user.html | 24 ++++ frontend/cont.html | 33 +++++ frontend/index.html | 61 ++++++++ frontend/login.html | 38 +++++ frontend/post.html | 24 ++++ frontend/publish_post.html | 49 +++++++ frontend/register.html | 37 +++++ frontend/static/main.js | 250 +++++++++++++++++++++++++++++++++ frontend/static/style.css | 83 +++++++++++ frontend/user.html | 24 ++++ moderation.py | 69 +++++++++ requirements.txt | 4 + 26 files changed, 1668 insertions(+) create mode 100644 .gitignore create mode 100644 .vscode/settings.json create mode 100644 LICENSE create mode 100644 app.py create mode 100644 config.ex.py create mode 100644 eee.py create mode 100644 filedb.py create mode 100644 frontend-gpt/index.html create mode 100644 frontend-gpt/login.html create mode 100644 frontend-gpt/post.html create mode 100644 frontend-gpt/publish_post.html create mode 100644 frontend-gpt/register.html create mode 100644 frontend-gpt/static/main.js create mode 100644 frontend-gpt/static/style.css create mode 100644 frontend-gpt/user.html create mode 100644 frontend/cont.html create mode 100644 frontend/index.html create mode 100644 frontend/login.html create mode 100644 frontend/post.html create mode 100644 frontend/publish_post.html create mode 100644 frontend/register.html create mode 100644 frontend/static/main.js create mode 100644 frontend/static/style.css create mode 100644 frontend/user.html create mode 100644 moderation.py create mode 100644 requirements.txt diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c9bc52f --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +temp_frame*.png +uploads_temp +noprinterdata +config.py +__pycache__/ +*.py[cod] diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..b881eff --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "python.analysis.autoImportCompletions": true +} \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..5c93f45 --- /dev/null +++ b/LICENSE @@ -0,0 +1,13 @@ + DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE + Version 2, December 2004 + + Copyright (C) 2004 Sam Hocevar + + Everyone is permitted to copy and distribute verbatim or modified + copies of this license document, and changing it is allowed as long + as the name is changed. + + DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. You just DO WHAT THE FUCK YOU WANT TO. diff --git a/app.py b/app.py new file mode 100644 index 0000000..b12cda1 --- /dev/null +++ b/app.py @@ -0,0 +1,226 @@ +import filedb +from moderation import moderate #dummy_moderate as moderate #moderate +# Using dummy moderation for testing purposes +from flask import Flask, request, jsonify,send_file,session, redirect,url_for +from werkzeug.utils import secure_filename +import os +import secrets +app = Flask(__name__, static_folder='frontend/static', static_url_path='/static') +app.config['UPLOAD_FOLDER'] = 'uploads_temp/' # Temporary upload folder, filedb will store it anyway +os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True) +app.secret_key = secrets.token_hex(16) # For session management +@app.route('/api/post_content/', methods=['GET']) +def get_post_content(post_id): # Returns the content file of a post + try: + post = filedb.get_post(post_id) + if post.contentloc and os.path.isfile(post.contentloc): + return send_file(post.contentloc) + else: + return jsonify({'error': 'Content not found'}), 404 + except filedb.PostNotFoundError: + return jsonify({'error': 'Post not found'}), 404 + +@app.route('/api/post/', methods=['GET']) +def get_post(post_id): # Returns metadata of a post + try: + post = filedb.get_post(post_id) + return jsonify({ + 'id': post.id, + 'title': post.title, + 'content_type': post.contentType, + 'tags': post.tags, + 'date_created': post.dateCreated, + 'creator_id': post.creator + }), 200 + except filedb.PostNotFoundError: + return jsonify({'error': 'Post not found'}), 404 + +@app.route('/api/publish/post', methods=['POST']) +def publish_post(): + if 'user_id' not in session: + return jsonify({'error': 'Authentication required'}), 401 + if 'title' not in request.form or 'tags' not in request.form or 'file' not in request.files: + return jsonify({'error': 'Missing required fields'}), 400 + title = request.form['title'] + tags = request.form.getlist('tags') + file = request.files['file'] + if file.filename == '': + return jsonify({'error': 'No selected file'}), 400 + filename = secure_filename(file.filename) + temp_path = os.path.join(app.config['UPLOAD_FOLDER'], filename) + os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True) + file.save(temp_path) + if not moderate(temp_path): + os.remove(temp_path) + return jsonify({'error': 'Stop uploading porn'}), 400 + post_id = filedb.createPost(title, temp_path, session['user_id'], tags) + os.remove(temp_path) + return jsonify({'message': 'Post created', 'post_id': post_id}), 201 + +@app.route('/api/getUser/', methods=['GET']) +def get_user(user_id): + try: + user = filedb.lookup(user_id) + return jsonify({ + 'id': user.id, + 'username': user.username + }), 200 + except FileNotFoundError: + return jsonify({'error': 'User not found'}), 404 + +@app.route('/api/create_post', methods=['POST']) +def create_post(): + if 'user_id' not in session: + return jsonify({'error': 'Authentication required'}), 401 + if 'title' not in request.form or 'tags' not in request.form or 'file' not in request.files: + return jsonify({'error': 'Missing required fields'}), 400 + title = request.form['title'] + tags = request.form.getlist('tags') + file = request.files['file'] + if file.filename == '': + return jsonify({'error': 'No selected file'}), 400 + filename = secure_filename(file.filename) + temp_path = os.path.join(app.config['UPLOAD_FOLDER'], filename) + os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True) + file.save(temp_path) + if not moderate(temp_path): + os.remove(temp_path) + return jsonify({'error': 'Content failed moderation'}), 400 + post_id = filedb.createPost(title, temp_path, session['user_id'], tags) + os.remove(temp_path) + return jsonify({'message': 'Post created', 'post_id': post_id}), 201 + +@app.route('/api/login', methods=['POST']) +def login(): + if 'username' not in request.form or 'password' not in request.form: + return jsonify({'error': 'Missing username or password'}), 400 + username = request.form['username'] + password = request.form['password'] + try: + user = filedb.lookupByUsername(username) + if filedb.verify_password(user.password_hash,password): + session['user_id'] = user.id + return jsonify({'message': 'Login successful'}), 200 + else: + return jsonify({'error': 'Invalid credentials'}), 401 + except FileNotFoundError: + return jsonify({'error': 'Invalid credentials'}), 401 + +@app.route('/api/register', methods=['POST']) +def register(): + if int(request.headers.get("Content-Length",0)) > 1000: + return jsonify({'error': 'WHAT THE FUCK'}), 400 + erikoer = request.get_data(parse_form_data=True) # please force form i beg + formdata = request.form.to_dict() + print(formdata) # Debugging line to see incoming form data cuz idk why it doesn't work + print(request.form) # i want to die this is a blank immutablemulti dict + print(erikoer) + if 'username' not in formdata or 'password' not in formdata: + return jsonify({'error': 'Missing username or password'}), 400 + # WHAT THE FUCK + # FORMDATA IS EMPTY WTF + # DEVTOOLS SHOWS THE FORMDATA IS SENT CORRECTLY + # flask waht is wrong with you + username = formdata['username'] + password = formdata['password'] + try: + filedb.lookupByUsername(username) + return jsonify({'error': 'Username already taken'}), 400 + except FileNotFoundError: + user_id = filedb.createUser(username,password) + session['user_id'] = user_id + return jsonify({'message': 'Registration successful', 'user_id': user_id}), 201 + +@app.route('/api/logout', methods=['POST']) +def logout(): + session.pop('user_id', None) + return jsonify({'message': 'Logged out'}), 200 + +# Frontend serving +@app.route('/') +def index(): + return send_file('frontend/index.html') + +@app.route('/post/') +def serve_post_page(post_id): + return send_file('frontend/post.html') + +@app.route('/user/') +def serve_user_page(user_id): + return send_file('frontend/user.html') + +@app.route("/login") +@app.route("/login/") +def serve_login_page(): + return send_file('frontend/login.html') +@app.route("/register") +@app.route("/register/") +def serve_register_page(): + return send_file('frontend/register.html') + +@app.route('/publish/post') +def serve_publish_post_page(): + if 'user_id' not in session: + return redirect('/login', code=302) + return send_file('frontend/publish_post.html') + +# thanks to gpt for helping me with this code and making the frontend +# Thanks to openai for the API that makes moderation possible, even if it's not used right now + + +## SSR endpoints cuz we need extra functionality NOW and js is too boring to write +@app.route('/ssr/usr') +def ssr_user(): + out_html = "

Users


" + for user in filedb.listUsers(): + out_html += f'{user.username}

' + return out_html + +@app.route('/ssr/post') +def ssr_post(): + out_html = "

Posts


" + for post in filedb.listPosts(): + lookup = "???" + try: + lookup = filedb.lookup(post.creator).username + except: + pass + out_html += f'{post.title} by {lookup}

' + return out_html + +def has_no_empty_params(rule): + defaults = rule.defaults if rule.defaults is not None else () + arguments = rule.arguments if rule.arguments is not None else () + return len(defaults) >= len(arguments) + +ssr_endpoint_meta = [ + ("/ssr/usr","List all users"), + ("/ssr/post","List all posts"), +] + +@app.route("/ssr/") # Get all ssr endpoints (auto generate) +def auto(): + htm = "

Additional stuff:

" + htm += "

This page may seem advanced. Fear not, just use the description to navigate.

" + for rule in app.url_map.iter_rules(): + # Filter out rules we can't navigate to in a browser + # and rules that require parameters + if "GET" in rule.methods and has_no_empty_params(rule): + if rule.endpoint.startswith("ssr_"): + url = url_for(rule.endpoint, **(rule.defaults or {})) + desc = next((desc for path,desc in ssr_endpoint_meta if path == url), "No description") + htm += f'{url} - {desc}
' + return htm + +@app.after_request +def wrap(response): + # Get the path of the request + path = request.path + if path.startswith("/ssr/") and response.content_type == "text/html; charset=utf-8": + # Wrap the response in a basic HTML structure + original_content = response.get_data(as_text=True) + with open("frontend/cont.html","r",encoding="utf-8") as f: + cont_html = f.read() + new_content = cont_html.replace("", original_content) + response.set_data(new_content) + return response \ No newline at end of file diff --git a/config.ex.py b/config.ex.py new file mode 100644 index 0000000..8fd1b4c --- /dev/null +++ b/config.ex.py @@ -0,0 +1 @@ +oaikey = "yourkeyhere. openai" diff --git a/eee.py b/eee.py new file mode 100644 index 0000000..a64dfa9 --- /dev/null +++ b/eee.py @@ -0,0 +1,2 @@ +from app import app +app.run(host="0.0.0.0",port=6352) diff --git a/filedb.py b/filedb.py new file mode 100644 index 0000000..901cb61 --- /dev/null +++ b/filedb.py @@ -0,0 +1,190 @@ +import os +import uuid +import shutil +from werkzeug.security import check_password_hash as verify_password +from werkzeug.security import generate_password_hash +fdb_loc = "./noprinterdata/" + +os.makedirs(fdb_loc,exist_ok=True) + +class PostNotFoundError(FileNotFoundError): + pass + +class Post: + def __init__(self,id,title,content_location,type,tags,date_created,creator_id): + self.id = id + self.title = title + self.contentloc = content_location + self.contentType = type + self.tags = tags + self.creator = creator_id + self.dateCreated = date_created + +class User: + def __init__(self,id,username,password_hash): + self.id = id + self.username = username + self.password_hash = password_hash + +def get_post(id): + posts_path = os.path.join(fdb_loc,"posts") + os.makedirs(posts_path,exist_ok=True) + if os.path.isdir(os.path.join(posts_path,id+"/")): + title = "" + if os.path.isfile(os.path.join(posts_path,id,"title.txt")): + with open(os.path.join(posts_path,id,"title.txt"),"r",encoding="utf-8") as f: + title = f.read() + tags = [] + if os.path.isfile(os.path.join(posts_path,id,"tags.txt")): + with open(os.path.join(posts_path,id,"tags.txt"),"r",encoding="utf-8") as f: + tags = f.read().splitlines() + date_created = None + if os.path.isfile(os.path.join(posts_path,id,"date_created.txt")): + with open(os.path.join(posts_path,id,"date_created.txt"),"r",encoding="utf-8") as f: + date_created = f.read() + # Check if theres any content file (file called post with any extension) + content_location = None + content_type = None + creator_id = None + for file in os.listdir(os.path.join(posts_path,id)): + if file.startswith("post."): + content_location = os.path.join(posts_path,id,file) + content_type = file.split(".")[-1] + break + if os.path.isfile(os.path.join(posts_path,id,"creator_id.txt")): + with open(os.path.join(posts_path,id,"creator_id.txt"),"r",encoding="utf-8") as f: + creator_id = f.read() + return Post(id,title,content_location,content_type,tags,date_created,creator_id) + else: + raise PostNotFoundError() + +def createPost(title,file_location,creator,tags): + posts_path = os.path.join(fdb_loc,"posts") + os.makedirs(posts_path,exist_ok=True) + id = str(uuid.uuid4()) + post_path = os.path.join(posts_path,id) + os.makedirs(post_path,exist_ok=True) + # Save title + with open(os.path.join(post_path,"title.txt"),"w",encoding="utf-8") as f: + f.write(title) + # Save tags + with open(os.path.join(post_path,"tags.txt"),"w",encoding="utf-8") as f: + f.write("\n".join(tags)) + # Save creator id + with open(os.path.join(post_path,"creator_id.txt"),"w",encoding="utf-8") as f: + f.write(creator) + # Save date created + from datetime import datetime + with open(os.path.join(post_path,"date_created.txt"),"w",encoding="utf-8") as f: + f.write(datetime.utcnow().isoformat()+"Z") + # Save content file + ext = file_location.split(".")[-1] + content_location = os.path.join(post_path,f"post.{ext}") + with open(file_location,"rb") as src: + with open(content_location,"wb") as dst: + dst.write(src.read()) + return id + +def deletePost(id): + posts_path = os.path.join(fdb_loc,"posts") + post_path = os.path.join(posts_path,id) + if os.path.isdir(post_path): + # Delete all files in the directory + for file in os.listdir(post_path): + os.remove(os.path.join(post_path,file)) + # Delete the directory + shutil.rmtree(post_path) + else: + raise PostNotFoundError() + +def listPosts(): + posts_path = os.path.join(fdb_loc,"posts") + os.makedirs(posts_path,exist_ok=True) + post_ids = [] + for entry in os.listdir(posts_path): + if os.path.isdir(os.path.join(posts_path,entry)): + # Get post id + post_ids.append(get_post(entry)) + return post_ids + +def listPostsByCreator(creator_id): + posts_path = os.path.join(fdb_loc,"posts") + os.makedirs(posts_path,exist_ok=True) + post_ids = [] + for entry in os.listdir(posts_path): + if os.path.isdir(os.path.join(posts_path,entry)): + # Check creator id + if os.path.isfile(os.path.join(posts_path,entry,"creator_id.txt")): + with open(os.path.join(posts_path,entry,"creator_id.txt"),"r",encoding="utf-8") as f: + if f.read() == creator_id: + post_ids.append(entry) + return post_ids + +def createUser(username,password): + password_hash = generate_password_hash(password) + users_path = os.path.join(fdb_loc,"users") + os.makedirs(users_path,exist_ok=True) + id = str(uuid.uuid4()) + user_path = os.path.join(users_path,id) + if os.path.isdir(user_path): + raise FileExistsError("User already exists") + os.makedirs(user_path,exist_ok=True) + with open(os.path.join(user_path,"password_hash.txt"),"w",encoding="utf-8") as f: + f.write(password_hash) + with open(os.path.join(user_path,"username.txt"),"w",encoding="utf-8") as f: + f.write(username) + return username + +def lookup(id): + users_path = os.path.join(fdb_loc,"users") + os.makedirs(users_path,exist_ok=True) + user_path = os.path.join(users_path,id) + if os.path.isdir(user_path): + password_hash = None + usern = None + if os.path.isfile(os.path.join(user_path,"username.txt")): + with open(os.path.join(user_path,"username.txt"),"r",encoding="utf-8") as f: + usern = f.read() + if os.path.isfile(os.path.join(user_path,"password_hash.txt")): + with open(os.path.join(user_path,"password_hash.txt"),"r",encoding="utf-8") as f: + password_hash = f.read() + return User(id,usern,password_hash) + else: + raise FileNotFoundError("User not found") + +def deleteUser(id): + users_path = os.path.join(fdb_loc,"users") + user_path = os.path.join(users_path,id) + if os.path.isdir(user_path): + # Delete all files in the directory + for file in os.listdir(user_path): + os.remove(os.path.join(user_path,file)) + # Delete the directory + shutil.rmtree(user_path) + else: + raise FileNotFoundError("User not found") + +def lookupByUsername(username): + users_path = os.path.join(fdb_loc,"users") + os.makedirs(users_path,exist_ok=True) + for entry in os.listdir(users_path): + if os.path.isdir(os.path.join(users_path,entry)): + user_path = os.path.join(users_path,entry) + if os.path.isfile(os.path.join(user_path,"username.txt")): + with open(os.path.join(user_path,"username.txt"),"r",encoding="utf-8") as f: + if f.read() == username: + password_hash = None + if os.path.isfile(os.path.join(user_path,"password_hash.txt")): + with open(os.path.join(user_path,"password_hash.txt"),"r",encoding="utf-8") as f2: + password_hash = f2.read() + return User(entry,username,password_hash) + raise FileNotFoundError("User not found") + +def listUsers(): + users_path = os.path.join(fdb_loc,"users") + os.makedirs(users_path,exist_ok=True) + user_ids = [] + for entry in os.listdir(users_path): + if os.path.isdir(os.path.join(users_path,entry)): + user_ids.append(lookup(entry)) + return user_ids \ No newline at end of file diff --git a/frontend-gpt/index.html b/frontend-gpt/index.html new file mode 100644 index 0000000..9b69f91 --- /dev/null +++ b/frontend-gpt/index.html @@ -0,0 +1,51 @@ + + + + + Home — NoPorn + + + + +
+
+
+
NoPorn — frontend
+
Quick demo UI — use login/register to create posts.
+
+ +
+ +
+ +
+

Open a post by ID 🔎

+
+
+ + +
+ +
+
+ +
+

Quick links

+ +
+ + +
+ + + + diff --git a/frontend-gpt/login.html b/frontend-gpt/login.html new file mode 100644 index 0000000..55e7993 --- /dev/null +++ b/frontend-gpt/login.html @@ -0,0 +1,38 @@ + + + + + Login — NoPorn + + + + +
+
+
NoPorn — Login
+ +
+ +
+ +
+
+
+ + +
+
+ + +
+
+ +
+
+
+ +
+ + + + diff --git a/frontend-gpt/post.html b/frontend-gpt/post.html new file mode 100644 index 0000000..0385646 --- /dev/null +++ b/frontend-gpt/post.html @@ -0,0 +1,24 @@ + + + + + Post — NoPorn + + + + +
+
+
Post
+ +
+ +
+ +
+ +
+ + + + diff --git a/frontend-gpt/publish_post.html b/frontend-gpt/publish_post.html new file mode 100644 index 0000000..2314349 --- /dev/null +++ b/frontend-gpt/publish_post.html @@ -0,0 +1,49 @@ + + + + + Publish Post — NoPorn + + + + +
+
+
Publish a post
+ +
+ +
+ +
+
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ +
+
+
Note — this page requires you to be logged in. If you are redirected to login, sign in and come back. ✨
+
+ +
+ + + + diff --git a/frontend-gpt/register.html b/frontend-gpt/register.html new file mode 100644 index 0000000..5668aaa --- /dev/null +++ b/frontend-gpt/register.html @@ -0,0 +1,37 @@ + + + + + Register — NoPorn + + + + +
+
+
NoPorn — Register
+ +
+ +
+ +
+
+
+ + +
+
+ + +
+
+ +
+
+
+
+ + + + diff --git a/frontend-gpt/static/main.js b/frontend-gpt/static/main.js new file mode 100644 index 0000000..f6e90be --- /dev/null +++ b/frontend-gpt/static/main.js @@ -0,0 +1,250 @@ +// main.js - shared helpers and page handlers +const API = { + login: '/api/login', + register: '/api/register', + logout: '/api/logout', + publish: '/api/publish/post', + create: '/api/create_post', + getPost: (id) => `/api/post/${id}`, + getPostContent: (id) => `/api/post_content/${id}`, + getUser: (id) => `/api/getUser/${id}` +}; + +function fetchJSON(url, opts = {}) { + opts.credentials = 'include'; // include session cookie + opts.headers = opts.headers || {}; + // if sending JSON set header, else for FormData don't set + if (opts.body && !(opts.body instanceof FormData) && !opts.headers['Content-Type']) { + opts.headers['Content-Type'] = 'application/json'; + } + return fetch(url, opts).then(async res => { + const ct = res.headers.get('content-type') || ''; + if (ct.includes('application/json')) { + const j = await res.json(); + if (!res.ok) throw j; + return j; + } else { + if (!res.ok) { + const txt = await res.text(); + throw { error: txt || 'Request failed', status: res.status }; + } + return res; + } + }); +} + +function fetchFORM(url, opts = {}) { // Force form submission + opts.credentials = 'include'; // include session cookie + opts.headers = opts.headers || {}; + // if sending JSON set header, else for FormData don't set + if (opts.body && !(opts.body instanceof FormData) && !opts.headers['Content-Type']) { + opts.headers['Content-Type'] = 'application/x-www-form-urlencoded'; + } + return fetch(url, opts).then(async res => { + const ct = res.headers.get('content-type') || ''; + if (ct.includes('application/json')) { + const j = await res.json(); + if (!res.ok) throw j; + return j; + } else { + if (!res.ok) { + const txt = await res.text(); + throw { error: txt || 'Request failed', status: res.status }; + } + return res; + } + }); +} + + +function showMessage(container, msg, ok = true) { + container.innerHTML = `
${escapeHtml(msg)}
`; + setTimeout(()=> { + // optionally fade out after 6s + // container.innerHTML=''; + }, 6000); +} + +function escapeHtml(s){ + return String(s).replace(/[&<>"']/g, c=>({ + '&':'&','<':'<','>':'>','"':'"',"'":''' + }[c])); +} + +/* -------- Page helpers -------- */ + +async function handleLoginForm(form, messageContainer) { + const data = new FormData(form); + const payload = new URLSearchParams(); + for (const [k,v] of data.entries()) payload.append(k,v); + try { + const res = await fetchFORM(API.login, { method:'POST', body: payload }); + showMessage(messageContainer, res.message || 'Logged in!', true); + // redirect to publish page after short success + setTimeout(()=> window.location = '/', 600); + } catch(err) { + showMessage(messageContainer, err.error || JSON.stringify(err), false); + } +} + +async function handleRegisterForm(form, messageContainer) { + const data = new FormData(form); + const payload = new URLSearchParams(); + for (const [k,v] of data.entries()) payload.append(k,v); + try { + const res = await fetchFORM(API.register, { method:'POST', body: payload }); + showMessage(messageContainer, res.message || 'Registered!', true); + setTimeout(()=> window.location = '/', 600); + } catch(err) { + showMessage(messageContainer, err.error || JSON.stringify(err), false); + } +} + +async function handlePublishForm(form, messageContainer) { + const fd = new FormData(); + const title = form.querySelector('[name=title]').value.trim(); + const tagsRaw = form.querySelector('[name=tags]').value.trim(); + const fileEl = form.querySelector('[name=file]'); + if (!title) return showMessage(messageContainer, 'Title required', false); + if (!fileEl.files || fileEl.files.length === 0) return showMessage(messageContainer, 'Pick a file', false); + fd.append('title', title); + // split tags by comma and append each tag as separate 'tags' field so backend getlist works + const tags = tagsRaw ? tagsRaw.split(',').map(t=>t.trim()).filter(Boolean) : []; + for (const t of tags) fd.append('tags', t); + fd.append('file', fileEl.files[0]); + + try { + // use publish endpoint — it requires session cookie + const res = await fetchFORM(API.publish, { method:'POST', body: fd }); + showMessage(messageContainer, res.message || 'Published!', true); + // Redirect to new post page + if (res.post_id) setTimeout(()=> window.location = `/post/${res.post_id}`, 600); + } catch(err) { + showMessage(messageContainer, err.error || JSON.stringify(err), false); + } +} + +async function handleLogout(button, msgContainer) { + try { + const res = await fetchJSON(API.logout, { method:'POST' }); + showMessage(msgContainer, res.message || 'Logged out', true); + setTimeout(()=> window.location = '/', 500); + } catch(err) { + showMessage(msgContainer, err.error || 'Logout failed', false); + } +} + +/* -------- Pages: simple renderers -------- */ + +async function renderPostPage() { + const message = document.getElementById('message'); + const id = window.location.pathname.split('/').pop(); + try { + const meta = await fetchJSON(API.getPost(id)); + const container = document.getElementById('post-container'); + container.innerHTML = ` +
+

${escapeHtml(meta.title)}

+
Post ID — ${escapeHtml(meta.id)} • Created: ${escapeHtml(meta.date_created || '')}
+
+
+
+
+ `; + // tags + const tagsWrap = document.getElementById('tags'); + (meta.tags||[]).forEach(t=> { + const el = document.createElement('div'); el.className='tag'; el.textContent = t; tagsWrap.appendChild(el); + }); + + // show content depending on content_type + const media = document.getElementById('media'); + const ctype = (meta.content_type||'').toLowerCase(); + const contentUrl = API.getPostContent(meta.id); + const imageExts = ['png','jpg','jpeg','gif','webp','bmp','tiff']; + const videoExts = ['mp4','webm','mov','avi','mkv']; + if (imageExts.includes(ctype)) { + media.innerHTML = `image`; + } else if (videoExts.includes(ctype)) { + media.innerHTML = ``; + } + else if (ctype === 'pdf') { // embed pdf + media.innerHTML = ``; + } + else if (ctype === 'txt') { + // fetch text content and display in
+      try {
+        const res = await fetchJSON(contentUrl);
+        const txt = await res.text();
+        media.innerHTML = `
${escapeHtml(txt)}
`; + } catch(e) { + media.innerHTML = `
Could not load text content
`; + } + } else { + // fallback - provide download link + media.innerHTML = `Download content`; + } + + // fetch creator username + if (meta.creator_id) { + try { + const u = await fetchJSON(API.getUser(meta.creator_id)); + document.getElementById('creator').innerHTML = `By ${escapeHtml(u.username)}View profile`; + } catch(e){ + document.getElementById('creator').innerHTML = `By user ${escapeHtml(meta.creator_id)}`; + } + } + } catch(err) { + showMessage(message, err.error || 'Could not load post', false); + console.error(err); + } +} + +async function renderUserPage() { + const uid = window.location.pathname.split('/').pop(); + const container = document.getElementById('user-container'); + try { + const u = await fetchJSON(API.getUser(uid)); + container.innerHTML = ` +
+

${escapeHtml(u.username)}

+
User ID — ${escapeHtml(u.id)}
+
This app does not yet expose a listing API — but you can open posts by ID. 🎯
+
+ `; + } catch(err){ + container.innerHTML = `
User not found
`; + } +} + +/* Optional: attach simple behaviors if present on page */ +document.addEventListener('DOMContentLoaded', ()=> { + // attach simple form bindings if forms exist + const loginForm = document.getElementById('login-form'); + if (loginForm) { + const msg = document.getElementById('message'); + loginForm.addEventListener('submit', (e)=>{ e.preventDefault(); handleLoginForm(loginForm, msg); }); + } + + const registerForm = document.getElementById('register-form'); + if (registerForm) { + const msg = document.getElementById('message'); + registerForm.addEventListener('submit', (e)=>{ e.preventDefault(); handleRegisterForm(registerForm, msg); }); + } + + const publishForm = document.getElementById('publish-form'); + if (publishForm) { + const msg = document.getElementById('message'); + publishForm.addEventListener('submit', (e)=>{ e.preventDefault(); handlePublishForm(publishForm, msg); }); + } + + const logoutBtn = document.getElementById('logout-btn'); + if (logoutBtn) { + const msg = document.getElementById('message'); + logoutBtn.addEventListener('click', ()=> handleLogout(logoutBtn, msg)); + } + + // page-specific render hooks + if (document.getElementById('post-container')) renderPostPage(); + if (document.getElementById('user-container')) renderUserPage(); +}); diff --git a/frontend-gpt/static/style.css b/frontend-gpt/static/style.css new file mode 100644 index 0000000..cf009f0 --- /dev/null +++ b/frontend-gpt/static/style.css @@ -0,0 +1,82 @@ +:root{ + --max-width:900px; + --accent:#1565c0; + --muted:#666; + --card-bg:#fff; + --page-bg:#f6f8fb; + --radius:10px; + --pad:18px; + font-family: Inter, ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial; +} + +*{box-sizing:border-box} +html,body{height:100%} +body{ + margin:0; + background:var(--page-bg); + color:#111; + -webkit-font-smoothing:antialiased; +} + +.container{ + max-width:var(--max-width); + margin:28px auto; + padding:12px; +} + +.header{ + display:flex; + justify-content:space-between; + align-items:center; + gap:12px; + margin-bottom:18px; +} +.brand{font-weight:700; font-size:1.3rem; color:var(--accent)} +.nav a{margin-left:10px; text-decoration:none; color:var(--accent)} + +.card{ + background:var(--card-bg); + border-radius:var(--radius); + padding:var(--pad); + box-shadow:0 6px 18px rgba(20,30,60,0.06); + margin-bottom:14px; +} + +.form-row{margin-bottom:12px} +label{display:block; font-weight:600; margin-bottom:6px} +input[type="text"], input[type="password"], textarea, input[type="file"]{ + width:100%; + padding:10px; + border-radius:8px; + border:1px solid #e0e6ef; + font-size:0.975rem; +} + +.button{ + display:inline-block; + padding:10px 14px; + border-radius:8px; + background:var(--accent); + color:white; + text-decoration:none; + border:none; + cursor:pointer; + font-weight:600; +} + +.message{padding:8px 10px; border-radius:8px; margin-bottom:10px} +.message.error{background:#ffecec; color:#900} +.message.ok{background:#e8fff0; color:#064} + +.meta{color:var(--muted); font-size:0.95rem} +.tags{display:flex; gap:8px; flex-wrap:wrap; margin-top:8px} +.tag{background:#eef6ff;color:var(--accent);padding:6px 8px;border-radius:999px;font-weight:600;font-size:0.85rem} + +.preview{max-width:100%; border-radius:8px; margin-top:12px} +.footer{color:var(--muted); font-size:0.9rem; margin-top:22px} + +/* small responsive */ +@media (max-width:520px){ + .header{flex-direction:column; align-items:flex-start; gap:8px} + .brand{font-size:1.1rem} +} diff --git a/frontend-gpt/user.html b/frontend-gpt/user.html new file mode 100644 index 0000000..f23e6ac --- /dev/null +++ b/frontend-gpt/user.html @@ -0,0 +1,24 @@ + + + + + User — NoPorn + + + + +
+
+
User profile
+ +
+ +
+ +
+ +
+ + + + diff --git a/frontend/cont.html b/frontend/cont.html new file mode 100644 index 0000000..b0d183a --- /dev/null +++ b/frontend/cont.html @@ -0,0 +1,33 @@ + + + + + Home — NoPorn + + + + +
+
+
+
Fax, no printer — NoPorn
+
+ This page is server-side! It will work if you have JS disabled. +
+
+ +
+ +
+
+ +
+ +
+ + diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..9ee7b31 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,61 @@ + + + + + Home — NoPorn + + + + +
+
+
+
Fax, no printer — NoPorn
+
We don't allow porn! Go to another site for that.
+
+ What you can do here:
+ - Post Stuff
+ - Search
+ - Explore
+ - Comment +
+
+ +
+ +
+ +
+

Open a post by ID 🔎

+
+
+ + +
+ +
+
+ +
+

Quick links

+ +
+ +
+ + + + diff --git a/frontend/login.html b/frontend/login.html new file mode 100644 index 0000000..55e7993 --- /dev/null +++ b/frontend/login.html @@ -0,0 +1,38 @@ + + + + + Login — NoPorn + + + + +
+
+
NoPorn — Login
+ +
+ +
+ +
+
+
+ + +
+
+ + +
+
+ +
+
+
+ +
+ + + + diff --git a/frontend/post.html b/frontend/post.html new file mode 100644 index 0000000..0385646 --- /dev/null +++ b/frontend/post.html @@ -0,0 +1,24 @@ + + + + + Post — NoPorn + + + + +
+
+
Post
+ +
+ +
+ +
+ +
+ + + + diff --git a/frontend/publish_post.html b/frontend/publish_post.html new file mode 100644 index 0000000..2314349 --- /dev/null +++ b/frontend/publish_post.html @@ -0,0 +1,49 @@ + + + + + Publish Post — NoPorn + + + + +
+
+
Publish a post
+ +
+ +
+ +
+
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ +
+
+
Note — this page requires you to be logged in. If you are redirected to login, sign in and come back. ✨
+
+ +
+ + + + diff --git a/frontend/register.html b/frontend/register.html new file mode 100644 index 0000000..5668aaa --- /dev/null +++ b/frontend/register.html @@ -0,0 +1,37 @@ + + + + + Register — NoPorn + + + + +
+
+
NoPorn — Register
+ +
+ +
+ +
+
+
+ + +
+
+ + +
+
+ +
+
+
+
+ + + + diff --git a/frontend/static/main.js b/frontend/static/main.js new file mode 100644 index 0000000..632ae04 --- /dev/null +++ b/frontend/static/main.js @@ -0,0 +1,250 @@ +// main.js - shared helpers and page handlers +const API = { + login: '/api/login', + register: '/api/register', + logout: '/api/logout', + publish: '/api/publish/post', + create: '/api/create_post', + getPost: (id) => `/api/post/${id}`, + getPostContent: (id) => `/api/post_content/${id}`, + getUser: (id) => `/api/getUser/${id}` +}; + +function fetchJSON(url, opts = {}) { + opts.credentials = 'include'; // include session cookie + opts.headers = opts.headers || {}; + // if sending JSON set header, else for FormData don't set + if (opts.body && !(opts.body instanceof FormData) && !opts.headers['Content-Type']) { + opts.headers['Content-Type'] = 'application/json'; + } + return fetch(url, opts).then(async res => { + const ct = res.headers.get('content-type') || ''; + if (ct.includes('application/json')) { + const j = await res.json(); + if (!res.ok) throw j; + return j; + } else { + if (!res.ok) { + const txt = await res.text(); + throw { error: txt || 'Request failed', status: res.status }; + } + return res; + } + }); +} + +function fetchFORM(url, opts = {}) { // Force form submission + opts.credentials = 'include'; // include session cookie + opts.headers = opts.headers || {}; + // if sending JSON set header, else for FormData don't set + if (opts.body && !(opts.body instanceof FormData) && !opts.headers['Content-Type']) { + opts.headers['Content-Type'] = 'application/x-www-form-urlencoded'; + } + return fetch(url, opts).then(async res => { + const ct = res.headers.get('content-type') || ''; + if (ct.includes('application/json')) { + const j = await res.json(); + if (!res.ok) throw j; + return j; + } else { + if (!res.ok) { + const txt = await res.text(); + throw { error: txt || 'Request failed', status: res.status }; + } + return res; + } + }); +} + + +function showMessage(container, msg, ok = true) { + container.innerHTML = `
${escapeHtml(msg)}
`; + setTimeout(()=> { + // optionally fade out after 6s + // container.innerHTML=''; + }, 6000); +} + +function escapeHtml(s){ + return String(s).replace(/[&<>"']/g, c=>({ + '&':'&','<':'<','>':'>','"':'"',"'":''' + }[c])); +} + +/* -------- Page helpers -------- */ + +async function handleLoginForm(form, messageContainer) { + const data = new FormData(form); + const payload = new URLSearchParams(); + for (const [k,v] of data.entries()) payload.append(k,v); + try { + const res = await fetchFORM(API.login, { method:'POST', body: payload }); + showMessage(messageContainer, res.message || 'Logged in!', true); + // redirect to publish page after short success + setTimeout(()=> window.location = '/', 600); + } catch(err) { + showMessage(messageContainer, err.error || JSON.stringify(err), false); + } +} + +async function handleRegisterForm(form, messageContainer) { + const data = new FormData(form); + const payload = new URLSearchParams(); + for (const [k,v] of data.entries()) payload.append(k,v); + try { + const res = await fetchFORM(API.register, { method:'POST', body: payload }); + showMessage(messageContainer, res.message || 'Registered!', true); + setTimeout(()=> window.location = '/', 600); + } catch(err) { + showMessage(messageContainer, err.error || JSON.stringify(err), false); + } +} + +async function handlePublishForm(form, messageContainer) { + const fd = new FormData(); + const title = form.querySelector('[name=title]').value.trim(); + const tagsRaw = form.querySelector('[name=tags]').value.trim(); + const fileEl = form.querySelector('[name=file]'); + if (!title) return showMessage(messageContainer, 'Title required', false); + if (!fileEl.files || fileEl.files.length === 0) return showMessage(messageContainer, 'Pick a file', false); + fd.append('title', title); + // split tags by comma and append each tag as separate 'tags' field so backend getlist works + const tags = tagsRaw ? tagsRaw.split(',').map(t=>t.trim()).filter(Boolean) : []; + for (const t of tags) fd.append('tags', t); + fd.append('file', fileEl.files[0]); + + try { + // use publish endpoint — it requires session cookie + const res = await fetchFORM(API.publish, { method:'POST', body: fd }); + showMessage(messageContainer, res.message || 'Published!', true); + // Redirect to new post page + if (res.post_id) setTimeout(()=> window.location = `/post/${res.post_id}`, 600); + } catch(err) { + showMessage(messageContainer, err.error || JSON.stringify(err), false); + } +} + +async function handleLogout(button, msgContainer) { + try { + const res = await fetchJSON(API.logout, { method:'POST' }); + showMessage(msgContainer, res.message || 'Logged out', true); + setTimeout(()=> window.location = '/', 500); + } catch(err) { + showMessage(msgContainer, err.error || 'Logout failed', false); + } +} + +/* -------- Pages: simple renderers -------- */ + +async function renderPostPage() { + const message = document.getElementById('message'); + const id = window.location.pathname.split('/').pop(); + try { + const meta = await fetchJSON(API.getPost(id)); + const container = document.getElementById('post-container'); + container.innerHTML = ` +
+

${escapeHtml(meta.title)}

+
Post ID — ${escapeHtml(meta.id)} • Created: ${escapeHtml(meta.date_created || '')}
+
+
+
+
+ `; + // tags + const tagsWrap = document.getElementById('tags'); + (meta.tags||[]).forEach(t=> { + const el = document.createElement('div'); el.className='tag'; el.textContent = t; tagsWrap.appendChild(el); + }); + + // show content depending on content_type + const media = document.getElementById('media'); + const ctype = (meta.content_type||'').toLowerCase(); + const contentUrl = API.getPostContent(meta.id); + const imageExts = ['png','jpg','jpeg','gif','webp','bmp','tiff']; + const videoExts = ['mp4','webm','mov','avi','mkv']; + if (imageExts.includes(ctype)) { + media.innerHTML = `image`; + } else if (videoExts.includes(ctype)) { + media.innerHTML = ``; + } + else if (ctype === 'pdf') { // embed pdf + media.innerHTML = ``; + } + else if (ctype === 'txt') { + // fetch text content and display in
+      try {
+        const res = await fetchJSON(contentUrl);
+        const txt = await res.text();
+        media.innerHTML = `
${escapeHtml(txt)}
`; + } catch(e) { + media.innerHTML = `
Could not load text content
`; + } + } else { + // fallback - provide download link + media.innerHTML = `Download content`; + } + + // fetch creator username + if (meta.creator_id) { + try { + const u = await fetchJSON(API.getUser(meta.creator_id)); + document.getElementById('creator').innerHTML = `By ${escapeHtml(u.username)}View profile`; + } catch(e){ + document.getElementById('creator').innerHTML = `By user ${escapeHtml(meta.creator_id)}`; + } + } + } catch(err) { + showMessage(message, err.error || 'Could not load post', false); + console.error(err); + } +} + +async function renderUserPage() { + const uid = window.location.pathname.split('/').pop(); + const container = document.getElementById('user-container'); + try { + const u = await fetchJSON(API.getUser(uid)); + container.innerHTML = ` +
+

${escapeHtml(u.username)}

+
User ID — ${escapeHtml(u.id)}
+
Profiles are currently incapable of showing posts. Please search for their name at /ssr/post
+
+ `; + } catch(err){ + container.innerHTML = `
User not found
`; + } +} + +/* Optional: attach simple behaviors if present on page */ +document.addEventListener('DOMContentLoaded', ()=> { + // attach simple form bindings if forms exist + const loginForm = document.getElementById('login-form'); + if (loginForm) { + const msg = document.getElementById('message'); + loginForm.addEventListener('submit', (e)=>{ e.preventDefault(); handleLoginForm(loginForm, msg); }); + } + + const registerForm = document.getElementById('register-form'); + if (registerForm) { + const msg = document.getElementById('message'); + registerForm.addEventListener('submit', (e)=>{ e.preventDefault(); handleRegisterForm(registerForm, msg); }); + } + + const publishForm = document.getElementById('publish-form'); + if (publishForm) { + const msg = document.getElementById('message'); + publishForm.addEventListener('submit', (e)=>{ e.preventDefault(); handlePublishForm(publishForm, msg); }); + } + + const logoutBtn = document.getElementById('logout-btn'); + if (logoutBtn) { + const msg = document.getElementById('message'); + logoutBtn.addEventListener('click', ()=> handleLogout(logoutBtn, msg)); + } + + // page-specific render hooks + if (document.getElementById('post-container')) renderPostPage(); + if (document.getElementById('user-container')) renderUserPage(); +}); diff --git a/frontend/static/style.css b/frontend/static/style.css new file mode 100644 index 0000000..7e3a9e8 --- /dev/null +++ b/frontend/static/style.css @@ -0,0 +1,83 @@ +:root{ + --max-width:900px; + --accent:#1565c0; + --accent-low:#0b3566; + --muted:#999; + --card-bg:#303030; + --page-bg:#202020; + --radius:10px; + --pad:18px; + font-family: Inter, ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial; +} + +*{box-sizing:border-box} +html,body{height:100%} +body{ + margin:0; + background:var(--page-bg); + color:#fff; + -webkit-font-smoothing:antialiased; +} + +.container{ + max-width:var(--max-width); + margin:28px auto; + padding:12px; +} + +.header{ + display:flex; + justify-content:space-between; + align-items:center; + gap:12px; + margin-bottom:18px; +} +.brand{font-weight:700; font-size:1.3rem; color:var(--accent)} +a{margin-left:10px; text-decoration:none; color:var(--accent)} + +.card{ + background:var(--card-bg); + border-radius:var(--radius); + padding:var(--pad); + box-shadow:0 6px 18px rgba(20,30,60,0.06); + margin-bottom:14px; +} + +.form-row{margin-bottom:12px} +label{display:block; font-weight:600; margin-bottom:6px} +input[type="text"], input[type="password"], textarea, input[type="file"]{ + width:100%; + padding:10px; + border-radius:8px; + border:1px solid #e0e6ef; + font-size:0.975rem; +} + +.button{ + display:inline-block; + padding:10px 14px; + border-radius:8px; + background:var(--accent); + color:white !important; + text-decoration:none; + border:none; + cursor:pointer; + font-weight:600; +} + +.message{padding:8px 10px; border-radius:8px; margin-bottom:10px} +.message.error{background:#ffecec; color:#900} +.message.ok{background:#e8fff0; color:#064} + +.meta{color:var(--muted); font-size:0.95rem} +.tags{display:flex; gap:8px; flex-wrap:wrap; margin-top:8px} +.tag{background:#eef6ff;color:var(--accent);padding:6px 8px;border-radius:999px;font-weight:600;font-size:0.85rem} + +.preview{max-width:100%; border-radius:8px; margin-top:12px} +.footer{color:var(--muted); font-size:0.9rem; margin-top:22px} + +/* small responsive */ +@media (max-width:520px){ + .header{flex-direction:column; align-items:flex-start; gap:8px} + .brand{font-size:1.1rem} +} diff --git a/frontend/user.html b/frontend/user.html new file mode 100644 index 0000000..f23e6ac --- /dev/null +++ b/frontend/user.html @@ -0,0 +1,24 @@ + + + + + User — NoPorn + + + + +
+
+
User profile
+ +
+ +
+ +
+ +
+ + + + diff --git a/moderation.py b/moderation.py new file mode 100644 index 0000000..b5f9631 --- /dev/null +++ b/moderation.py @@ -0,0 +1,69 @@ +from openai import OpenAI +from PIL import Image +import random +from tqdm import tqdm +from PIL import ImageSequence +import os +import base64 +import mimetypes +import config +client = OpenAI(api_key=config.oaikey) + +def check_image(floc): + print("MODERATION: Checking image",floc) + mime, encoding = mimetypes.guess_type(floc) + if mime is None: + return False + with open(floc,"rb") as f: + response = client.moderations.create( + model="omni-moderation-latest", + input=[ + { + "type": "image_url", + "image_url": { + "url": f"data:{mime};base64,{base64.b64encode(f.read()).decode()}" + } + }, + ], + ) + results = response.results[0] + flagged_categories = vars(results.categories) + print("MODDEBUG: Flagged categories for image:", flagged_categories) + return flagged_categories["sexual"] or flagged_categories.get("sexual_minors", False) # Some models may not have the "sexual/minors" category + + + +def moderate(content_path): # Returns True if content is safe, False otherwise or if unsupported + if content_path.lower().endswith(('.png', '.jpg', '.jpeg', '.bmp','.webp')): + return not check_image(content_path) + elif content_path.lower().endswith('.txt'): + with open(content_path, 'r') as file: + text = file.read() + response = client.moderations.create( + model="omni-moderation-latest", + input=text + ) + results = response.results[0] + flagged_categories = vars(results.categories) + return flagged_categories["sexual"] or flagged_categories.get("sexual_minors", False) + elif content_path.lower().endswith(('.gif')): + # Currently, OpenAI does not support moderation for GIFs, so we use a hacky workaround + # by moderating all frames individually and flagging if any frame is flagged. + + unsafe = False # Assume safe until proven otherwise in any frame + with Image.open(content_path) as img: + for frame in tqdm(ImageSequence.Iterator(img)): + # Save frame to a temporary file + temp_frame_path = f"temp_frame{random.randint(1,int(9e7))}.png" + frame.save(temp_frame_path) + if check_image(temp_frame_path): # Checks if an image contains adult content + unsafe = True + break + return not unsafe + return False # Unsupported file type, assume unsafe to protect users + +def dummy_moderate(content_path): # Dummy moderation function that always returns True (for testing purposes) + return True + +def dummy_moderate_schizo(content_path): # Dummy moderation function that randomly returns True or False (for testing purposes) + return random.choice([True, False]) # Called schizo because it has a schizophrenic behavior diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..37be6cf --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +openai # Solely for moderation purposes +flask # Web framework +pillow # Image processing, and get individual frames for moderation +tqdm # Stats \ No newline at end of file