This commit is contained in:
opera
2026-03-18 17:27:43 +01:00
commit e13df2948a
26 changed files with 1668 additions and 0 deletions

6
.gitignore vendored Normal file
View File

@@ -0,0 +1,6 @@
temp_frame*.png
uploads_temp
noprinterdata
config.py
__pycache__/
*.py[cod]

3
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,3 @@
{
"python.analysis.autoImportCompletions": true
}

13
LICENSE Normal file
View File

@@ -0,0 +1,13 @@
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
Version 2, December 2004
Copyright (C) 2004 Sam Hocevar <sam@hocevar.net>
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.

226
app.py Normal file
View File

@@ -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/<post_id>', 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/<post_id>', 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/<user_id>', 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/<post_id>')
def serve_post_page(post_id):
return send_file('frontend/post.html')
@app.route('/user/<user_id>')
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 = "<h1>Users</h1><br>"
for user in filedb.listUsers():
out_html += f'<a href="/user/{user.id}">{user.username}</a><br><hr>'
return out_html
@app.route('/ssr/post')
def ssr_post():
out_html = "<h1>Posts</h1><br>"
for post in filedb.listPosts():
lookup = "???"
try:
lookup = filedb.lookup(post.creator).username
except:
pass
out_html += f'<a href="/post/{post.id}">{post.title}</a> by <a href="/user/{post.creator}">{lookup}</a><br><hr>'
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 = "<h1>Additional stuff:</h1>"
htm += "<p>This page may seem advanced. Fear not, just use the description to navigate.</p>"
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'<a href="{url}">{url}</a> - {desc}<br>'
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("<!--ReplaceWithContent-->", original_content)
response.set_data(new_content)
return response

1
config.ex.py Normal file
View File

@@ -0,0 +1 @@
oaikey = "yourkeyhere. openai"

2
eee.py Normal file
View File

@@ -0,0 +1,2 @@
from app import app
app.run(host="0.0.0.0",port=6352)

190
filedb.py Normal file
View File

@@ -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

51
frontend-gpt/index.html Normal file
View File

@@ -0,0 +1,51 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title>Home — NoPorn</title>
<meta name="viewport" content="width=device-width,initial-scale=1"/>
<link rel="stylesheet" href="/static/style.css">
</head>
<body>
<div class="container">
<div class="header">
<div>
<div class="brand">NoPorn — <span style="font-weight:400">frontend</span></div>
<div class="meta">Quick demo UI — use login/register to create posts.</div>
</div>
<div class="nav">
<a href="/login">Log in</a>
<a href="/register">Register</a>
<a href="/publish/post" class="button">Publish</a>
</div>
</div>
<div id="message"></div>
<div class="card">
<h3>Open a post by ID 🔎</h3>
<form id="open-post-form" onsubmit="event.preventDefault(); location.href='/post/'+document.getElementById('open-id').value.trim();">
<div class="form-row">
<label for="open-id">Post ID</label>
<input id="open-id" type="text" placeholder="e.g. 1234-abcd" />
</div>
<button class="button" type="submit">Open post</button>
</form>
</div>
<div class="card">
<h3>Quick links</h3>
<ul>
<li><a href="/publish/post">Create / Publish a post</a></li>
<li><a href="/login">Log in</a> — to attach session and publish.</li>
</ul>
</div>
<div class="footer">
Built for your Flask backend — drop files into <code>frontend/</code> and restart Flask. ✨
</div>
</div>
<script src="/static/main.js"></script>
</body>
</html>

38
frontend-gpt/login.html Normal file
View File

@@ -0,0 +1,38 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title>Login — NoPorn</title>
<meta name="viewport" content="width=device-width,initial-scale=1"/>
<link rel="stylesheet" href="/static/style.css">
</head>
<body>
<div class="container">
<div class="header">
<div class="brand">NoPorn — Login</div>
<div class="nav"><a href="/">Home</a> <a href="/register">Register</a></div>
</div>
<div id="message"></div>
<div class="card">
<form id="login-form">
<div class="form-row">
<label>Username</label>
<input name="username" type="text" required />
</div>
<div class="form-row">
<label>Password</label>
<input name="password" type="password" required />
</div>
<div class="form-row">
<button class="button" type="submit">Log in</button>
</div>
</form>
</div>
</div>
<script src="/static/main.js"></script>
</body>
</html>

24
frontend-gpt/post.html Normal file
View File

@@ -0,0 +1,24 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title>Post — NoPorn</title>
<meta name="viewport" content="width=device-width,initial-scale=1"/>
<link rel="stylesheet" href="/static/style.css">
</head>
<body>
<div class="container">
<div class="header">
<div class="brand">Post</div>
<div class="nav"><a href="/">Home</a></div>
</div>
<div id="message"></div>
<div id="post-container"></div>
</div>
<script src="/static/main.js"></script>
</body>
</html>

View File

@@ -0,0 +1,49 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title>Publish Post — NoPorn</title>
<meta name="viewport" content="width=device-width,initial-scale=1"/>
<link rel="stylesheet" href="/static/style.css">
</head>
<body>
<div class="container">
<div class="header">
<div class="brand">Publish a post</div>
<div class="nav">
<a href="/">Home</a>
<a href="#" id="logout-btn" class="button" style="background:#b23">Logout</a>
</div>
</div>
<div id="message"></div>
<div class="card">
<form id="publish-form" enctype="multipart/form-data">
<div class="form-row">
<label>Title</label>
<input name="title" type="text" required />
</div>
<div class="form-row">
<label>Tags — comma separated</label>
<input name="tags" type="text" placeholder="cats, summer, vacation" />
</div>
<div class="form-row">
<label>File</label>
<input name="file" type="file" accept="image/*,video/*,application/pdf" required />
</div>
<div class="form-row">
<button class="button" type="submit">Publish</button>
</div>
</form>
<div class="meta">Note — this page requires you to be logged in. If you are redirected to login, sign in and come back. ✨</div>
</div>
</div>
<script src="/static/main.js"></script>
</body>
</html>

View File

@@ -0,0 +1,37 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title>Register — NoPorn</title>
<meta name="viewport" content="width=device-width,initial-scale=1"/>
<link rel="stylesheet" href="/static/style.css">
</head>
<body>
<div class="container">
<div class="header">
<div class="brand">NoPorn — Register</div>
<div class="nav"><a href="/">Home</a> <a href="/login">Login</a></div>
</div>
<div id="message"></div>
<div class="card">
<form id="register-form">
<div class="form-row">
<label>Username</label>
<input name="username" type="text" required />
</div>
<div class="form-row">
<label>Password</label>
<input name="password" type="password" required />
</div>
<div class="form-row">
<button class="button" type="submit">Register</button>
</div>
</form>
</div>
</div>
<script src="/static/main.js"></script>
</body>
</html>

250
frontend-gpt/static/main.js Normal file
View File

@@ -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 = `<div class="message ${ok ? 'ok' : 'error'}">${escapeHtml(msg)}</div>`;
setTimeout(()=> {
// optionally fade out after 6s
// container.innerHTML='';
}, 6000);
}
function escapeHtml(s){
return String(s).replace(/[&<>"']/g, c=>({
'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'
}[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 = `
<div class="card">
<h2>${escapeHtml(meta.title)}</h2>
<div class="meta">Post ID — ${escapeHtml(meta.id)} • Created: ${escapeHtml(meta.date_created || '')}</div>
<div id="tags" class="tags"></div>
<div id="media"></div>
<div id="creator" class="meta" style="margin-top:10px"></div>
</div>
`;
// 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 = `<img class="preview" src="${contentUrl}" alt="image">`;
} else if (videoExts.includes(ctype)) {
media.innerHTML = `<video class="preview" controls src="${contentUrl}">Your browser can't play this video.</video>`;
}
else if (ctype === 'pdf') { // embed pdf
media.innerHTML = `<iframe style="height:80vh;width:60%" src="${contentUrl}"></iframe>`;
}
else if (ctype === 'txt') {
// fetch text content and display in <pre>
try {
const res = await fetchJSON(contentUrl);
const txt = await res.text();
media.innerHTML = `<pre style="white-space:pre-wrap;word-break:break-all;">${escapeHtml(txt)}</pre>`;
} catch(e) {
media.innerHTML = `<div class="message error">Could not load text content</div>`;
}
} else {
// fallback - provide download link
media.innerHTML = `<a class="button" href="${contentUrl}">Download content</a>`;
}
// fetch creator username
if (meta.creator_id) {
try {
const u = await fetchJSON(API.getUser(meta.creator_id));
document.getElementById('creator').innerHTML = `By <strong>${escapeHtml(u.username)}</strong> — <a href="/user/${u.id}">View profile</a>`;
} 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 = `
<div class="card">
<h2>${escapeHtml(u.username)}</h2>
<div class="meta">User ID — ${escapeHtml(u.id)}</div>
<div style="margin-top:12px">This app does not yet expose a listing API — but you can open posts by ID. 🎯</div>
</div>
`;
} catch(err){
container.innerHTML = `<div class="card"><div class="message error">User not found</div></div>`;
}
}
/* 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();
});

View File

@@ -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}
}

24
frontend-gpt/user.html Normal file
View File

@@ -0,0 +1,24 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title>User — NoPorn</title>
<meta name="viewport" content="width=device-width,initial-scale=1"/>
<link rel="stylesheet" href="/static/style.css">
</head>
<body>
<div class="container">
<div class="header">
<div class="brand">User profile</div>
<div class="nav"><a href="/">Home</a></div>
</div>
<div id="message"></div>
<div id="user-container"></div>
</div>
<script src="/static/main.js"></script>
</body>
</html>

33
frontend/cont.html Normal file
View File

@@ -0,0 +1,33 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title>Home — NoPorn</title>
<meta name="viewport" content="width=device-width,initial-scale=1"/>
<link rel="stylesheet" href="/static/style.css">
</head>
<body>
<div class="container">
<div class="header">
<div>
<div class="brand">Fax, no printer — <span style="font-weight:400">NoPorn</span></div>
<div class="meta">
This page is server-side! It will work if you have JS disabled.
</div>
</div>
<div class="nav">
<a href="/">Home</a>
<a href="/login">Log in</a>
<a href="/register">Register</a>
<a href="/publish/post" class="button">Publish</a>
</div>
</div>
<div id="message"></div>
<div class="card">
<!--ReplaceWithContent-->
</div>
</div>
</body>
</html>

61
frontend/index.html Normal file
View File

@@ -0,0 +1,61 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title>Home — NoPorn</title>
<meta name="viewport" content="width=device-width,initial-scale=1"/>
<link rel="stylesheet" href="/static/style.css">
</head>
<body>
<div class="container">
<div class="header">
<div>
<div class="brand">Fax, no printer — <span style="font-weight:400">NoPorn</span></div>
<div class="meta">We don't allow porn! Go to another site for that.</div>
<div class="meta">
What you can do here:<br>
- Post Stuff<br>
- Search<br>
- Explore<br>
- Comment
</div>
</div>
<div class="nav">
<a href="/">Home</a>
<a href="/login">Log in</a>
<a href="/register">Register</a>
<a href="/publish/post" class="button">Publish</a>
</div>
</div>
<div id="message"></div>
<div class="card">
<h3>Open a post by ID 🔎</h3>
<form id="open-post-form" onsubmit="event.preventDefault(); location.href='/post/'+document.getElementById('open-id').value.trim();">
<div class="form-row">
<label for="open-id">Post ID</label>
<input id="open-id" type="text" placeholder="e.g. 1234-abcd" />
</div>
<button class="button" type="submit">Open post</button>
</form>
</div>
<div class="card">
<h3>Quick links</h3>
<ul>
<li><a href="/publish/post">Create / Publish a post</a></li>
<li><a href="/login">Log in</a> — to attach session and publish.</li>
<li>
<a href="/ssr/">
Server-side pages
</a> — for miscellaneous stuff
</li>
</ul>
</div>
</div>
<script src="/static/main.js"></script>
</body>
</html>

38
frontend/login.html Normal file
View File

@@ -0,0 +1,38 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title>Login — NoPorn</title>
<meta name="viewport" content="width=device-width,initial-scale=1"/>
<link rel="stylesheet" href="/static/style.css">
</head>
<body>
<div class="container">
<div class="header">
<div class="brand">NoPorn — Login</div>
<div class="nav"><a href="/">Home</a> <a href="/register">Register</a></div>
</div>
<div id="message"></div>
<div class="card">
<form id="login-form">
<div class="form-row">
<label>Username</label>
<input name="username" type="text" required />
</div>
<div class="form-row">
<label>Password</label>
<input name="password" type="password" required />
</div>
<div class="form-row">
<button class="button" type="submit">Log in</button>
</div>
</form>
</div>
</div>
<script src="/static/main.js"></script>
</body>
</html>

24
frontend/post.html Normal file
View File

@@ -0,0 +1,24 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title>Post — NoPorn</title>
<meta name="viewport" content="width=device-width,initial-scale=1"/>
<link rel="stylesheet" href="/static/style.css">
</head>
<body>
<div class="container">
<div class="header">
<div class="brand">Post</div>
<div class="nav"><a href="/">Home</a></div>
</div>
<div id="message"></div>
<div id="post-container"></div>
</div>
<script src="/static/main.js"></script>
</body>
</html>

View File

@@ -0,0 +1,49 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title>Publish Post — NoPorn</title>
<meta name="viewport" content="width=device-width,initial-scale=1"/>
<link rel="stylesheet" href="/static/style.css">
</head>
<body>
<div class="container">
<div class="header">
<div class="brand">Publish a post</div>
<div class="nav">
<a href="/">Home</a>
<a href="#" id="logout-btn" class="button" style="background:#b23">Logout</a>
</div>
</div>
<div id="message"></div>
<div class="card">
<form id="publish-form" enctype="multipart/form-data">
<div class="form-row">
<label>Title</label>
<input name="title" type="text" required />
</div>
<div class="form-row">
<label>Tags — comma separated</label>
<input name="tags" type="text" placeholder="cats, summer, vacation" />
</div>
<div class="form-row">
<label>File</label>
<input name="file" type="file" accept="image/*,video/*,application/pdf" required />
</div>
<div class="form-row">
<button class="button" type="submit">Publish</button>
</div>
</form>
<div class="meta">Note — this page requires you to be logged in. If you are redirected to login, sign in and come back. ✨</div>
</div>
</div>
<script src="/static/main.js"></script>
</body>
</html>

37
frontend/register.html Normal file
View File

@@ -0,0 +1,37 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title>Register — NoPorn</title>
<meta name="viewport" content="width=device-width,initial-scale=1"/>
<link rel="stylesheet" href="/static/style.css">
</head>
<body>
<div class="container">
<div class="header">
<div class="brand">NoPorn — Register</div>
<div class="nav"><a href="/">Home</a> <a href="/login">Login</a></div>
</div>
<div id="message"></div>
<div class="card">
<form id="register-form">
<div class="form-row">
<label>Username</label>
<input name="username" type="text" required />
</div>
<div class="form-row">
<label>Password</label>
<input name="password" type="password" required />
</div>
<div class="form-row">
<button class="button" type="submit">Register</button>
</div>
</form>
</div>
</div>
<script src="/static/main.js"></script>
</body>
</html>

250
frontend/static/main.js Normal file
View File

@@ -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 = `<div class="message ${ok ? 'ok' : 'error'}">${escapeHtml(msg)}</div>`;
setTimeout(()=> {
// optionally fade out after 6s
// container.innerHTML='';
}, 6000);
}
function escapeHtml(s){
return String(s).replace(/[&<>"']/g, c=>({
'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'
}[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 = `
<div class="card">
<h2>${escapeHtml(meta.title)}</h2>
<div class="meta">Post ID — ${escapeHtml(meta.id)} • Created: ${escapeHtml(meta.date_created || '')}</div>
<div id="tags" class="tags"></div>
<div id="media"></div>
<div id="creator" class="meta" style="margin-top:10px"></div>
</div>
`;
// 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 = `<img class="preview" src="${contentUrl}" alt="image">`;
} else if (videoExts.includes(ctype)) {
media.innerHTML = `<video class="preview" controls src="${contentUrl}">Your browser can't play this video.</video>`;
}
else if (ctype === 'pdf') { // embed pdf
media.innerHTML = `<iframe style="height:80vh;width:60%" src="${contentUrl}"></iframe>`;
}
else if (ctype === 'txt') {
// fetch text content and display in <pre>
try {
const res = await fetchJSON(contentUrl);
const txt = await res.text();
media.innerHTML = `<pre style="white-space:pre-wrap;word-break:break-all;">${escapeHtml(txt)}</pre>`;
} catch(e) {
media.innerHTML = `<div class="message error">Could not load text content</div>`;
}
} else {
// fallback - provide download link
media.innerHTML = `<a class="button" href="${contentUrl}">Download content</a>`;
}
// fetch creator username
if (meta.creator_id) {
try {
const u = await fetchJSON(API.getUser(meta.creator_id));
document.getElementById('creator').innerHTML = `By <strong>${escapeHtml(u.username)}</strong> — <a href="/user/${u.id}">View profile</a>`;
} 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 = `
<div class="card">
<h2>${escapeHtml(u.username)}</h2>
<div class="meta">User ID — ${escapeHtml(u.id)}</div>
<div style="margin-top:12px">Profiles are currently incapable of showing posts. Please search for their name at <a href="/ssr/post">/ssr/post</a></div>
</div>
`;
} catch(err){
container.innerHTML = `<div class="card"><div class="message error">User not found</div></div>`;
}
}
/* 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();
});

83
frontend/static/style.css Normal file
View File

@@ -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}
}

24
frontend/user.html Normal file
View File

@@ -0,0 +1,24 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title>User — NoPorn</title>
<meta name="viewport" content="width=device-width,initial-scale=1"/>
<link rel="stylesheet" href="/static/style.css">
</head>
<body>
<div class="container">
<div class="header">
<div class="brand">User profile</div>
<div class="nav"><a href="/">Home</a></div>
</div>
<div id="message"></div>
<div id="user-container"></div>
</div>
<script src="/static/main.js"></script>
</body>
</html>

69
moderation.py Normal file
View File

@@ -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

4
requirements.txt Normal file
View File

@@ -0,0 +1,4 @@
openai # Solely for moderation purposes
flask # Web framework
pillow # Image processing, and get individual frames for moderation
tqdm # Stats