reupload
This commit is contained in:
3
.env.example
Normal file
3
.env.example
Normal file
@@ -0,0 +1,3 @@
|
||||
PROXY=wsrv.nl
|
||||
RULES=./jurisdiction.txt
|
||||
DATABASE="./posts.json"
|
||||
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
__pycache__/
|
||||
testing.json
|
||||
templates/test.html
|
||||
.env
|
||||
24
README.md
Normal file
24
README.md
Normal file
@@ -0,0 +1,24 @@
|
||||
# buddyboard
|
||||
An imageboard for friends written in Flask. Uses a flatfile (json) database, and strongly encourages hotlinking of images.
|
||||
|
||||
# Caveats
|
||||
- No admin panel/way to delete posts autonomously (yet)
|
||||
- jQuery functions are partially wonky
|
||||
- No one's going to use this crap
|
||||
- Written in Flask
|
||||
- *Novice* Flask, may I add
|
||||
|
||||
# How to install
|
||||
*Don't!*
|
||||
|
||||
- `pip install -r requirements.txt`
|
||||
- `py -u ./main.py` or deployable equivalent
|
||||
- *Again, never deploy this code!*
|
||||
|
||||
# How to contribute
|
||||
*Don't!*
|
||||
|
||||
# Credits
|
||||
- Stack Overflow
|
||||
- AI for tips on how to refactor the stupid ass reply route
|
||||
- Early testers who Shall Not Be Named
|
||||
3
jurisdiction.txt
Normal file
3
jurisdiction.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
No slurs
|
||||
No prejudice
|
||||
Be nice
|
||||
117
main.py
Normal file
117
main.py
Normal file
@@ -0,0 +1,117 @@
|
||||
|
||||
from flask import Flask, render_template, request, redirect, url_for
|
||||
from markupsafe import escape
|
||||
import os
|
||||
import json
|
||||
import uuid
|
||||
from dotenv import load_dotenv, dotenv_values
|
||||
# from werkzeug.routing import IntegerConverter
|
||||
from flask_limiter import Limiter
|
||||
from flask_limiter.util import get_remote_address
|
||||
|
||||
app = Flask(__name__)
|
||||
limiter = Limiter(get_remote_address, app=app)
|
||||
load_dotenv()
|
||||
|
||||
database = os.getenv("DATABASE")
|
||||
|
||||
def postsExist():
|
||||
try:
|
||||
with open(os.getenv("DATABASE"), 'a') as f:
|
||||
pass
|
||||
except IOError as e:
|
||||
print(f"Error ensuring posts exist: {e}")
|
||||
|
||||
@app.before_request
|
||||
def before_request():
|
||||
postsExist()
|
||||
|
||||
@app.route('/', methods=['GET'])
|
||||
def index():
|
||||
try:
|
||||
with open(database, "r", encoding='utf-8') as data:
|
||||
posts = json.load(data)
|
||||
except FileNotFoundError:
|
||||
print("posts.json not found, starting fresh.")
|
||||
except Exception as e:
|
||||
print(f"Error reading posts.json: {e}")
|
||||
return "An error occurred while loading posts. Please try again later.", 500
|
||||
|
||||
return render_template("main.html", posts=posts, proxy=os.getenv("PROXY"))
|
||||
|
||||
@app.route('/reply/<post_id>', methods=['GET'])
|
||||
def replyIndex(post_id):
|
||||
try:
|
||||
with open(database, "r", encoding='utf-8') as data:
|
||||
replies = json.load(data)
|
||||
except FileNotFoundError:
|
||||
print("posts.json not found, starting fresh.")
|
||||
except Exception as e:
|
||||
print(f"Error reading posts.json: {e}")
|
||||
return "An error occurred while loading posts. Please try again later.", 500
|
||||
|
||||
parent_post = next((p for p in replies if p.get('id') == post_id), None)
|
||||
if parent_post is None:
|
||||
return "Post not found", 404
|
||||
|
||||
return render_template('reply.html', post=parent_post, replies=replies, proxy=os.getenv("PROXY"))
|
||||
|
||||
@app.route('/vote/<post>/<int(signed=True):rating>', methods=['POST'])
|
||||
@limiter.limit("8/day", key_func=get_remote_address)
|
||||
def rate(post, rating):
|
||||
try:
|
||||
with open(database, "r+", encoding='utf-8') as data:
|
||||
posts = json.load(data)
|
||||
found = False
|
||||
for i in posts:
|
||||
if i['id'] == post:
|
||||
i['yeahs'] += rating
|
||||
found = True
|
||||
break
|
||||
|
||||
if not found:
|
||||
data = {
|
||||
"status": 404
|
||||
}
|
||||
return data
|
||||
|
||||
data.seek(0)
|
||||
json.dump(posts, data, indent=4)
|
||||
data = {
|
||||
"status": 200
|
||||
}
|
||||
return data
|
||||
except Exception as e:
|
||||
print(f"Error reading posts.json: {e}")
|
||||
return "An error occurred while loading posts. Please try again later.", 500
|
||||
|
||||
@app.route('/reply/<post_id>', methods=['POST'])
|
||||
@app.route('/', methods=['POST'])
|
||||
def add_data(post_id=None):
|
||||
|
||||
post = {}
|
||||
post['id'] = str(uuid.uuid1())
|
||||
post['user'] = request.form.get('user', '').strip() or 'anon'
|
||||
post['content'] = escape(request.form.get('data', ''))
|
||||
post['yeahs'] = 0
|
||||
post['replying'] = post_id or None
|
||||
post['image'] = request.form.get('image', '') or None
|
||||
|
||||
if not post:
|
||||
return "Say something!", 400
|
||||
|
||||
|
||||
final_user_name = post['user'] if post['user'] else 'anon'
|
||||
try:
|
||||
with open(database, "r+", encoding='utf-8') as read:
|
||||
file = json.load(read)
|
||||
file.append(post)
|
||||
with open(database, "w", encoding='utf-8') as write:
|
||||
json.dump(file, write, indent=4)
|
||||
return redirect(request.path)
|
||||
except Exception as e:
|
||||
print(f"Error writing to posts.json: {e}")
|
||||
return "An error occurred while saving your post. Please try again.", 500
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(host='0.0.0.0', port=8000, debug=True)
|
||||
10
posts.json
Normal file
10
posts.json
Normal file
@@ -0,0 +1,10 @@
|
||||
[
|
||||
{
|
||||
"id": "34282e73-c446-11f0-a6b9-30d0421378ab",
|
||||
"user": "zav",
|
||||
"content": "Buddyboard is (partially) back in business!\r\n\r\nFrom testing database:\r\n"Fixes include:- less crappy reply function (sorry)- 50% less AI code (also sorry)- Actual image linking! No more bullshit syntax! (also also ALSO sorry)"\r\n\r\nHave fun!",
|
||||
"yeahs": 0,
|
||||
"replying": null,
|
||||
"image": "https://snootbooru.com/data/posts/73546_b28e019fcffbd588.png"
|
||||
}
|
||||
]
|
||||
BIN
requirements.txt
Normal file
BIN
requirements.txt
Normal file
Binary file not shown.
2
static/jquery-3.7.1.slim.min.js
vendored
Normal file
2
static/jquery-3.7.1.slim.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
68
static/main.js
Normal file
68
static/main.js
Normal file
@@ -0,0 +1,68 @@
|
||||
// real shit
|
||||
|
||||
function enlarge(url) {
|
||||
const overlay = document.createElement("div");
|
||||
overlay.className = "overlay";
|
||||
|
||||
const image = document.createElement("img");
|
||||
image.src = url;
|
||||
image.style.position = "fixed"; // Use 'fixed' to position relative to the viewport
|
||||
image.style.top = "50%";
|
||||
image.style.left = "50%";
|
||||
image.style.transform = "translate(-50%, -50%)";
|
||||
image.style.maxWidth = "90vw";
|
||||
image.style.maxHeight = "90vh";
|
||||
image.style.zIndex = "1000";
|
||||
image.style.border = "2px solid #333";
|
||||
image.style.boxShadow = "0 0 10px rgba(0, 0, 0, 0.5)";
|
||||
image.className = "large";
|
||||
|
||||
overlay.appendChild(image);
|
||||
document.body.appendChild(overlay);
|
||||
|
||||
overlay.onclick = function() {
|
||||
document.body.removeChild(overlay);
|
||||
};
|
||||
}
|
||||
|
||||
async function ratePost(post, rating) {
|
||||
const response = await fetch(`../vote/${post}/${rating}`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({ "rating": rating }),
|
||||
});
|
||||
if (response.status == 429) {
|
||||
displayBottomMessage("You already voted today!");
|
||||
}
|
||||
if (!response.ok) {
|
||||
throw new Error(`Response status: ${response.status}`);
|
||||
}
|
||||
console.log(response.status);
|
||||
const responseData = await response.json();
|
||||
console.log(responseData);
|
||||
window.location.reload();
|
||||
|
||||
}
|
||||
|
||||
function displayBottomMessage(messageText) {
|
||||
let messageContainer = document.getElementById('api-message-container');
|
||||
|
||||
if (!messageContainer) {
|
||||
messageContainer = document.createElement('div');
|
||||
messageContainer.id = 'notification';
|
||||
messageContainer.style.position = 'fixed';
|
||||
messageContainer.style.bottom = '10px';
|
||||
messageContainer.style.left = '10px';
|
||||
document.body.appendChild(messageContainer);
|
||||
}
|
||||
|
||||
messageContainer.textContent = messageText;
|
||||
|
||||
setTimeout(() => {
|
||||
if (messageContainer && messageContainer.parentNode) {
|
||||
messageContainer.parentNode.removeChild(messageContainer);
|
||||
}
|
||||
}, 1500);
|
||||
}
|
||||
BIN
static/nah.png
Normal file
BIN
static/nah.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 36 KiB |
57
static/style.css
Normal file
57
static/style.css
Normal file
@@ -0,0 +1,57 @@
|
||||
/* woekwewe */
|
||||
html { background:#595656; color: white;}
|
||||
textarea { resize: none; border: 1px Solid;}
|
||||
h1 { display: inline; margin: 0; padding: 0;}
|
||||
.container { width: 60%; margin: auto; }
|
||||
.nav { padding-left: 0px !important; padding:5px; }
|
||||
.overlay::after {position: fixed; top: 10%; left: 10%; font-size: 24px; color: white; content: "Click away to exit...";}
|
||||
.overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background-color: rgba(0, 0, 0, 0.85);
|
||||
z-index: 9999;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
padding: 20px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.mod {
|
||||
color: rgb(182, 0, 0);
|
||||
}
|
||||
|
||||
img:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
a {
|
||||
color:blueviolet;
|
||||
}
|
||||
|
||||
textarea[disabled] {
|
||||
background: darkgrey;
|
||||
}
|
||||
.replies {
|
||||
margin: 2em;
|
||||
}
|
||||
|
||||
#notification {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.replies {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
|
||||
th {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
a {
|
||||
color: rgb(161, 161, 161);
|
||||
}
|
||||
9
static/threads.js
Normal file
9
static/threads.js
Normal file
@@ -0,0 +1,9 @@
|
||||
// threads.js
|
||||
// by lua (zav@tbdpowered.net)
|
||||
|
||||
function toggleContent(contentId, toggleElement) {
|
||||
$("." + contentId).toggle();
|
||||
|
||||
var $toggle = $(toggleElement);
|
||||
$toggle.text( ($toggle.text() == '[-]' ? '[+]' : '[-]') );
|
||||
}
|
||||
BIN
static/yeah.png
Normal file
BIN
static/yeah.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 39 KiB |
4
templates/header.html
Normal file
4
templates/header.html
Normal file
@@ -0,0 +1,4 @@
|
||||
<div class="nav">
|
||||
<h1>/buddy/</h1>
|
||||
<a href="#">/other/</a>
|
||||
</div>
|
||||
75
templates/main.html
Normal file
75
templates/main.html
Normal file
@@ -0,0 +1,75 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>/buddy/</title>
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='style.css') }}">
|
||||
<script src="{{ url_for('static', filename='jquery-3.7.1.slim.min.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='threads.js') }}"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
{% include 'header.html' %}
|
||||
<form action="/" method="POST" class="post-form">
|
||||
<table>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<td><textarea name="user" rows="1" cols="50" class="form-input"></textarea></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Image</th>
|
||||
<td><textarea name="image" rows="1" cols="50" class="form-input"></textarea></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Content</th>
|
||||
<td><textarea name="data" placeholder="Text" rows="4" cols="50" class="form-input"></textarea></td>
|
||||
<td><button type="submit" class="submit-button">Post</button></td>
|
||||
</tr>
|
||||
</table>
|
||||
</form>
|
||||
|
||||
<div class="posts">
|
||||
{% for post in posts | reverse %}
|
||||
{% if post.replying == "pin" %}
|
||||
<article class="post">
|
||||
<div class="actions">
|
||||
<a href="#" onclick="toggleContent('content-{{ post.id }}', this); return false;">[-]</a> <span class="mod">[Pinned]</span>
|
||||
{% if post.ip == "127.0.0.1" and post.user == "*" %}<b class="mod">{{ mod }}</b> <span class="mod">[M]</span>{% else %}<b>{{ post.user }}</b>{% endif %} •
|
||||
{{post.yeahs}} <img src="{{ url_for('static', filename='yeah.png') }}" height=40 style="vertical-align: middle;" onclick='ratePost("{{ post.id }}", 1)'> <img src="{{ url_for('static', filename='nah.png') }}" height=40 style="vertical-align: middle;" onclick='ratePost("{{ post.id }}", -1)'><a href="/reply/{{ post.id }}">Reply</a>
|
||||
</div>
|
||||
<div class="content-{{ post.id }}">
|
||||
{% if post.image %}
|
||||
<img src="https://{{proxy}}/?url={{ post.image }}" alt="Embedded image from {{ post.image }}" onclick="enlarge('{{post.image}}');" height="100">
|
||||
{% endif %}
|
||||
<p>{{ post.content | replace('\n', '<br>') | safe }}</p>
|
||||
</div>
|
||||
</article>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% for post in posts | reverse %}
|
||||
{% if post.replying == None %}
|
||||
<article class="post">
|
||||
<div class="actions">
|
||||
<a href="#" onclick="toggleContent('content-{{ post.id }}', this); return false;">[-]</a>
|
||||
{% if post.ip == "127.0.0.1" and post.user == "*" %}<b class="mod">{{ mod }}</b> <span class="mod">[M]</span>{% else %}<b>{{ post.user }}</b>{% endif %} •
|
||||
{{post.yeahs}} <img src="{{ url_for('static', filename='yeah.png') }}" height=40 style="vertical-align: middle;" onclick='ratePost("{{ post.id }}", 1)'> <img src="{{ url_for('static', filename='nah.png') }}" height=40 style="vertical-align: middle;" onclick='ratePost("{{ post.id }}", -1)'><a href="/reply/{{ post.id }}">Reply</a>
|
||||
</div>
|
||||
<div class="content-{{ post.id }}">
|
||||
{% if post.image %}
|
||||
<img src="https://{{proxy}}/?url={{ post.image }}" alt="Embedded image from {{ post.image }}" onclick="enlarge('{{post.image}}');" height="300">
|
||||
{% endif %}
|
||||
<p>{{ post.content | replace('\n', '<br>') | safe }}</p>
|
||||
</div>
|
||||
</article>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<p>No messages yet. Be the first to post!</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<hr>
|
||||
<small>This service is ran on buddyboard • <a href="https://codeberg.org/zav/buddyboard">source code</a></small>
|
||||
</div>
|
||||
<script src="{{ url_for('static', filename='main.js') }}"></script>
|
||||
</body>
|
||||
</html>
|
||||
76
templates/reply.html
Normal file
76
templates/reply.html
Normal file
@@ -0,0 +1,76 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>/buddy/</title>
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='style.css') }}">
|
||||
<script src="{{ url_for('static', filename='jquery-3.7.1.slim.min.js') }}"></script>
|
||||
<script src="{{ url_for('static', filename='threads.js') }}"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
{% include 'header.html' %}
|
||||
<form action="/reply/{{ post.id }}" method="POST" class="post-form">
|
||||
<table>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<td><textarea name="user" rows="1" cols="50" class="form-input"></textarea></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Image</th>
|
||||
<td><textarea name="image" rows="1" cols="50" class="form-input"></textarea></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Content</th>
|
||||
<td><textarea name="data" placeholder="Text" rows="4" cols="50" class="form-input"></textarea></td>
|
||||
<input name="replying" value="{{post.id}}" type="hidden"></textarea>
|
||||
<td><button type="submit" class="submit-button">Post</button></td>
|
||||
</tr>
|
||||
</table>
|
||||
</form>
|
||||
|
||||
<div class="posts">
|
||||
<article class="post">
|
||||
<div class="actions">
|
||||
<a href="#" onclick="toggleContent('content-{{ post.id }}', this); return false;">[-]</a>
|
||||
{% if post.ip == "127.0.0.1" and post.user == "*" %}<b class="mod">{{ mod }}</b> <span class="mod">[M]</span>{% else %}<b>{{ post.user }}</b>{% endif %} •
|
||||
{{post.yeahs}} <img src="{{ url_for('static', filename='yeah.png') }}" height=40 style="vertical-align: middle;" onclick='ratePost("{{ post.id }}", 1)'> <img src="{{ url_for('static', filename='nah.png') }}" height=40 style="vertical-align: middle;" onclick='ratePost("{{ post.id }}", -1)'><a href="#">Reply</a>
|
||||
</div>
|
||||
<div class="content-{{ post.id }}">
|
||||
{% if post.image %}
|
||||
<img src="https://{{proxy}}/?url={{ post.image }}" alt="Embedded image from {{ post.image }}" onclick="enlarge('{{post.image}}');" height="300">
|
||||
{% endif %}
|
||||
<p>{{ post.content | replace('\n', '<br>') | safe }}</p>
|
||||
</div>
|
||||
</article>
|
||||
<a href="#" onclick="$('.replies').toggle(); $(this).text() == '[-]' ? '[+]' : '[-]'">[-]</a> {{ replies|count - 1}} replies...
|
||||
<div class="replies">
|
||||
{% for p in replies | reverse %}
|
||||
{% if p.replying == post.id %}
|
||||
<article class="post">
|
||||
<div class="actions">
|
||||
<a href="#" onclick="toggleContent('content-{{ p.id }}', this); return false;">[-]</a>
|
||||
{{p.user}} •
|
||||
{{p.yeahs}} <img src="{{ url_for('static', filename='yeah.png') }}" height=40 style="vertical-align: middle;" onclick='ratePost("{{ p.id }}", 1)'> <img src="{{ url_for('static', filename='nah.png') }}" height=40 style="vertical-align: middle;" onclick='ratePost("{{ p.id }}", -1)'><a href="/reply/{{ p.id }}">Reply</a>
|
||||
</div>
|
||||
<div class="content-{{ p.id }}">
|
||||
{% if p.image %}
|
||||
<img src="https://{{proxy}}/?url={{ p.image }}" alt="Embedded image from {{ p.image }}" onclick="enlarge('{{p.image}}');" height="300">
|
||||
{% endif %}
|
||||
<p>{{ p.content | replace('\n', '<br>') | safe }}</p>
|
||||
</div>
|
||||
</article>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<p>No messages yet. Be the first to post!</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<hr>
|
||||
<small>This service is ran on buddyboard • <a href="https://codeberg.org/zav/buddyboard">source code</a></small>
|
||||
</div>
|
||||
<script src="{{ url_for('static', filename='main.js') }}"></script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user