bruh
This commit is contained in:
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
temp_frame*.png
|
||||
uploads_temp
|
||||
noprinterdata
|
||||
config.py
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
3
.vscode/settings.json
vendored
Normal file
3
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"python.analysis.autoImportCompletions": true
|
||||
}
|
||||
13
LICENSE
Normal file
13
LICENSE
Normal 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
226
app.py
Normal 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
1
config.ex.py
Normal file
@@ -0,0 +1 @@
|
||||
oaikey = "yourkeyhere. openai"
|
||||
2
eee.py
Normal file
2
eee.py
Normal file
@@ -0,0 +1,2 @@
|
||||
from app import app
|
||||
app.run(host="0.0.0.0",port=6352)
|
||||
190
filedb.py
Normal file
190
filedb.py
Normal 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
51
frontend-gpt/index.html
Normal 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
38
frontend-gpt/login.html
Normal 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
24
frontend-gpt/post.html
Normal 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>
|
||||
49
frontend-gpt/publish_post.html
Normal file
49
frontend-gpt/publish_post.html
Normal 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-gpt/register.html
Normal file
37
frontend-gpt/register.html
Normal 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
250
frontend-gpt/static/main.js
Normal 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=>({
|
||||
'&':'&','<':'<','>':'>','"':'"',"'":'''
|
||||
}[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();
|
||||
});
|
||||
82
frontend-gpt/static/style.css
Normal file
82
frontend-gpt/static/style.css
Normal 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
24
frontend-gpt/user.html
Normal 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
33
frontend/cont.html
Normal 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
61
frontend/index.html
Normal 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
38
frontend/login.html
Normal 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
24
frontend/post.html
Normal 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>
|
||||
49
frontend/publish_post.html
Normal file
49
frontend/publish_post.html
Normal 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
37
frontend/register.html
Normal 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
250
frontend/static/main.js
Normal 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=>({
|
||||
'&':'&','<':'<','>':'>','"':'"',"'":'''
|
||||
}[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
83
frontend/static/style.css
Normal 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
24
frontend/user.html
Normal 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
69
moderation.py
Normal 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
4
requirements.txt
Normal file
@@ -0,0 +1,4 @@
|
||||
openai # Solely for moderation purposes
|
||||
flask # Web framework
|
||||
pillow # Image processing, and get individual frames for moderation
|
||||
tqdm # Stats
|
||||
Reference in New Issue
Block a user