This commit is contained in:
Jake Paul
2025-11-23 00:01:12 -06:00
commit 1c45c7f7cd
16 changed files with 452 additions and 0 deletions

3
.env.example Normal file
View File

@@ -0,0 +1,3 @@
PROXY=wsrv.nl
RULES=./jurisdiction.txt
DATABASE="./posts.json"

4
.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
__pycache__/
testing.json
templates/test.html
.env

24
README.md Normal file
View 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
View File

@@ -0,0 +1,3 @@
No slurs
No prejudice
Be nice

117
main.py Normal file
View 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
View 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&#34;Fixes include:- less crappy reply function (sorry)- 50% less AI code (also sorry)- Actual image linking! No more bullshit syntax! (also also ALSO sorry)&#34;\r\n\r\nHave fun!",
"yeahs": 0,
"replying": null,
"image": "https://snootbooru.com/data/posts/73546_b28e019fcffbd588.png"
}
]

BIN
requirements.txt Normal file

Binary file not shown.

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

57
static/style.css Normal file
View 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
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

4
templates/header.html Normal file
View File

@@ -0,0 +1,4 @@
<div class="nav">
<h1>/buddy/</h1>
<a href="#">/other/</a>
</div>

75
templates/main.html Normal file
View 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 %} &bull;
{{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 %} &bull;
{{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 &bull; <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
View 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 %} &bull;
{{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}} &bull;
{{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 &bull; <a href="https://codeberg.org/zav/buddyboard">source code</a></small>
</div>
<script src="{{ url_for('static', filename='main.js') }}"></script>
</body>
</html>