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