add motd schema and motd editor

This commit is contained in:
2025-11-27 19:47:26 +03:00
parent 04fd3f5d20
commit fca214dfcf
12 changed files with 156 additions and 32 deletions

View File

@@ -218,10 +218,14 @@ def should_collapse(text, surrounding):
return False return False
def babycode_to_html(s): def babycode_to_html(s, banned_tags=None):
allowed_tags = list(TAGS.keys())
if banned_tags is not None:
for tag in banned_tags:
allowed_tags.remove(tag)
subj = escape(s.strip().replace('\r\n', '\n').replace('\r', '\n')) subj = escape(s.strip().replace('\r\n', '\n').replace('\r', '\n'))
parser = Parser(subj) parser = Parser(subj)
parser.valid_bbcode_tags = TAGS.keys() parser.valid_bbcode_tags = allowed_tags
parser.bbcode_tags_only_text_children = TEXT_ONLY parser.bbcode_tags_only_text_children = TEXT_ONLY
parser.valid_emotes = EMOJI.keys() parser.valid_emotes = EMOJI.keys()

View File

@@ -394,3 +394,19 @@ class BookmarkedThreads(Model):
def get_thread(self): def get_thread(self):
return Threads.find({'id': self.thread_id}) return Threads.find({'id': self.thread_id})
class MOTD(Model):
table = 'motd'
@classmethod
def has_motd(cls):
q = 'SELECT EXISTS(SELECT 1 FROM motd) as e'
res = db.fetch_one(q)['e']
return int(res) == 1
@classmethod
def get_all(cls):
q = 'SELECT id FROM motd'
res = db.query(q)
return [MOTD.find({'id': i['id']}) for i in res]

View File

@@ -40,7 +40,8 @@ def babycode_preview():
markup = request.json.get('markup') markup = request.json.get('markup')
if not markup or not isinstance(markup, str): if not markup or not isinstance(markup, str):
return {'error': 'markup field missing or invalid type'}, 400 return {'error': 'markup field missing or invalid type'}, 400
rendered = babycode_to_html(markup) banned_tags = request.json.get('banned_tags', [])
rendered = babycode_to_html(markup, banned_tags)
return {'html': rendered} return {'html': rendered}

View File

@@ -1,8 +1,11 @@
from flask import ( from flask import (
Blueprint, render_template, request, redirect, url_for Blueprint, render_template, request, redirect, url_for,
flash
) )
from .users import get_active_user, is_logged_in from .users import get_active_user, is_logged_in
from ..models import Users, PasswordResetLinks from ..models import Users, PasswordResetLinks, MOTD
from ..constants import InfoboxKind
from ..lib.babycode import babycode_to_html, BABYCODE_VERSION
from ..db import db from ..db import db
import secrets import secrets
import time import time
@@ -55,3 +58,47 @@ def create_reset_pass(user_id):
@bp.get('/panel') @bp.get('/panel')
def panel(): def panel():
return render_template('mod/panel.html') return render_template('mod/panel.html')
@bp.get('/motd')
def motd_editor():
current = MOTD.get_all()[0] if MOTD.has_motd() else None
return render_template('mod/motd.html', current=current)
@bp.post('/motd')
def motd_editor_form():
orig_body = request.form.get('body', default='')
title = request.form.get('title', default='')
data = {
'title': title,
'body_original_markup': orig_body,
'body_rendered': babycode_to_html(orig_body, banned_tags=['img', 'spoiler']),
'format_version': BABYCODE_VERSION,
'edited_at': int(time.time()),
}
if MOTD.has_motd():
motd = MOTD.get_all()[0]
motd.update(data)
message = 'MOTD updated.'
else:
data['created_at'] = int(time.time())
data['user_id'] = get_active_user().id
motd = MOTD.create(data)
message = 'MOTD created.'
flash(message, InfoboxKind.INFO)
return redirect(url_for('.motd_editor'))
@bp.post('/motd/delete')
def motd_delete():
if not MOTD.has_motd():
flash('No MOTD to delete.', InfoboxKind.WARN)
return redirect(url_for('.motd_editor'))
current = MOTD.get_all()[0]
current.delete()
flash('MOTD deleted.', InfoboxKind.INFO)
return redirect(url_for('.motd_editor'))

View File

@@ -120,6 +120,18 @@ SCHEMA = [
UNIQUE(collection_id, thread_id) UNIQUE(collection_id, thread_id)
)""", )""",
"""CREATE TABLE IF NOT EXISTS "motd" (
"id" INTEGER NOT NULL PRIMARY KEY,
"title" TEXT NOT NULL,
"body_original_markup" TEXT NOT NULL,
"body_rendered" TEXT NOT NULL,
"markup_language" TEXT NOT NULL DEFAULT 'babycode',
"format_version" INTEGER DEFAULT NULL,
"created_at" INTEGER DEFAULT (unixepoch(CURRENT_TIMESTAMP)),
"edited_at" INTEGER DEFAULT (unixepoch(CURRENT_TIMESTAMP)),
"user_id" REFERENCES users(id) ON DELETE CASCADE
)""",
# INDEXES # INDEXES
"CREATE INDEX IF NOT EXISTS idx_post_history_post_id ON post_history(post_id)", "CREATE INDEX IF NOT EXISTS idx_post_history_post_id ON post_history(post_id)",
"CREATE INDEX IF NOT EXISTS idx_posts_thread ON posts(thread_id, created_at, id)", "CREATE INDEX IF NOT EXISTS idx_posts_thread ON posts(thread_id, created_at, id)",

View File

@@ -47,3 +47,9 @@
<path d="M13 20H6C4.89543 20 4 19.1046 4 18V6C4 4.89543 4.89543 4 6 4H18C19.1046 4 20 4.89543 20 6V13M13 20L20 13M13 20V14C13 13.4477 13.4477 13 14 13H20" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> <path d="M13 20H6C4.89543 20 4 19.1046 4 18V6C4 4.89543 4.89543 4 6 4H18C19.1046 4 20 4.89543 20 6V13M13 20L20 13M13 20V14C13 13.4477 13.4477 13 14 13H20" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg> </svg>
{%- endmacro %} {%- endmacro %}
{% macro icn_megaphone(width=60) -%}
<svg width="{{width}}px" height="{{width}}px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6 18V14M6 14H8L13 17V7L8 10H5C3.89543 10 3 10.8954 3 12V12C3 13.1046 3.89543 14 5 14H6ZM17 7L19 5M17 17L19 19M19 12H21" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
{%- endmacro %}

View File

@@ -39,18 +39,24 @@
{% macro infobox(message, kind=InfoboxKind.INFO) %} {% macro infobox(message, kind=InfoboxKind.INFO) %}
<div class="{{ "infobox " + InfoboxHTMLClass[kind] }}"> <div class="{{ "infobox " + InfoboxHTMLClass[kind] }}">
<span> <span>
<div class="infobox-icon-container"> <div class="infobox-icon-container">
{%- if kind == InfoboxKind.INFO -%} {%- if kind == InfoboxKind.INFO -%}
{{- icn_info() -}} {{- icn_info() -}}
{%- elif kind == InfoboxKind.LOCK -%} {%- elif kind == InfoboxKind.LOCK -%}
{{- icn_lock() -}} {{- icn_lock() -}}
{%- elif kind == InfoboxKind.WARN -%} {%- elif kind == InfoboxKind.WARN -%}
{{- icn_warn() -}} {{- icn_warn() -}}
{%- elif kind == InfoboxKind.ERROR -%} {%- elif kind == InfoboxKind.ERROR -%}
{{- icn_error() -}} {{- icn_error() -}}
{%- endif -%} {%- endif -%}
</div> </div>
{{ message }} <span>
{% set m = message.split(';', maxsplit=1) %}
<strong>{{ m[0] }}</strong>
{%- if m[1] %}
{{ m[1] -}}
{%- endif -%}
</span>
</span> </span>
</div> </div>
{% endmacro %} {% endmacro %}
@@ -59,27 +65,38 @@
<span class="timestamp" data-utc="{{ unix_ts }}">{{ unix_ts | ts_datetime('%Y-%m-%d %H:%M')}} <abbr title="Server Time">ST</abbr></span> <span class="timestamp" data-utc="{{ unix_ts }}">{{ unix_ts | ts_datetime('%Y-%m-%d %H:%M')}} <abbr title="Server Time">ST</abbr></span>
{%- endmacro %} {%- endmacro %}
{% macro babycode_editor_component(ta_name, ta_placeholder="Post body", optional=False, prefill="") %} {% macro babycode_editor_component(ta_name, ta_placeholder="Post body", optional=False, prefill="", banned_tags=[]) %}
<div class="babycode-editor-container"> <div class="babycode-editor-container">
<input type="hidden" id="babycode-banned-tags" value="{{banned_tags | unique | list | tojson | forceescape}}">
<div class="tab-buttons"> <div class="tab-buttons">
<button type=button class="tab-button active" data-target-id="tab-edit">Write</button> <button type=button class="tab-button active" data-target-id="tab-edit">Write</button>
<button type=button class="tab-button" data-target-id="tab-preview">Preview</button> <button type=button class="tab-button" data-target-id="tab-preview">Preview</button>
</div> </div>
<div class="tab-content active" id="tab-edit"> <div class="tab-content active" id="tab-edit">
<span class="babycode-button-container"> <span class="babycode-button-container">
<button class="babycode-button" type=button id="post-editor-bold" title="Insert Bold"><strong>B</strong></button> <button class="babycode-button" type=button id="post-editor-bold" title="Insert Bold" {{"disabled" if "b" in banned_tags else ""}}><strong>B</strong></button>
<button class="babycode-button" type=button id="post-editor-italics" title="Insert Italics"><em>I</em></button> <button class="babycode-button" type=button id="post-editor-italics" title="Insert Italics" {{"disabled" if "i" in banned_tags else ""}}><em>I</em></button>
<button class="babycode-button" type=button id="post-editor-strike" title="Insert Strikethrough"><del>S</del></button> <button class="babycode-button" type=button id="post-editor-strike" title="Insert Strikethrough" {{"disabled" if "s" in banned_tags else ""}}><del>S</del></button>
<button class="babycode-button" type=button id="post-editor-underline" title="Insert Underline"><u>U</u></button> <button class="babycode-button" type=button id="post-editor-underline" title="Insert Underline" {{"disabled" if "u" in banned_tags else ""}}><u>U</u></button>
<button class="babycode-button" type=button id="post-editor-url" title="Insert Link"><code>://</code></button> <button class="babycode-button" type=button id="post-editor-url" title="Insert Link" {{"disabled" if "url" in banned_tags else ""}}><code>://</code></button>
<button class="babycode-button" type=button id="post-editor-code" title="Insert Code block"><code>&lt;/&gt;</code></button> <button class="babycode-button" type=button id="post-editor-code" title="Insert Code block" {{"disabled" if "code" in banned_tags else ""}}><code>&lt;/&gt;</code></button>
<button class="babycode-button contain-svg" type=button id="post-editor-img" title="Insert Image">{{ icn_image() }}</button> <button class="babycode-button contain-svg" type=button id="post-editor-img" title="Insert Image" {{"disabled" if "img" in banned_tags else ""}}>{{ icn_image() }}</button>
<button class="babycode-button" type=button id="post-editor-ol" title="Insert Ordered list">1.</button> <button class="babycode-button" type=button id="post-editor-ol" title="Insert Ordered list" {{"disabled" if "ol" in banned_tags else ""}}>1.</button>
<button class="babycode-button" type=button id="post-editor-ul" title="Insert Unordered list">&bullet;</button> <button class="babycode-button" type=button id="post-editor-ul" title="Insert Unordered list" {{"disabled" if "u;" in banned_tags else ""}}>&bullet;</button>
<button class="babycode-button contain-svg" type=button id="post-editor-spoiler" title="Insert spoiler">{{ icn_spoiler() }}</button> <button class="babycode-button contain-svg" type=button id="post-editor-spoiler" title="Insert spoiler" {{"disabled" if "spoiler" in banned_tags else ""}}>{{ icn_spoiler() }}</button>
</span> </span>
<textarea class="babycode-editor" name="{{ ta_name }}" id="babycode-content" placeholder="{{ ta_placeholder }}" {{ "required" if not optional else "" }}>{{ prefill }}</textarea> <textarea class="babycode-editor" name="{{ ta_name }}" id="babycode-content" placeholder="{{ ta_placeholder }}" {{ "required" if not optional else "" }} autocomplete="off">{{ prefill }}</textarea>
<a href="{{ url_for("app.babycode_guide") }}" target="_blank">babycode guide</a> <a href="{{ url_for("app.babycode_guide") }}" target="_blank">babycode guide</a>
{% if banned_tags %}
<div>Forbidden tags:</div>
<div>
<ul class="horizontal">
{% for tag in banned_tags | unique %}
<li><code class="inline-code">[{{ tag }}]</code></li>
{% endfor %}
</ul>
</div>
{% endif %}
</div> </div>
<div class="tab-content" id="tab-preview"> <div class="tab-content" id="tab-preview">
<div id="babycode-preview-errors-container">Type something!</div> <div id="babycode-preview-errors-container">Type something!</div>

View File

@@ -0,0 +1,17 @@
{% from 'common/macros.html' import babycode_editor_component %}
{% extends 'base.html' %}
{% block title %}editing MOTD{% endblock %}
{% block content %}
<div class="darkbg settings-container">
<h1>Edit Message of the Day</h1>
<p>The Message of the Day will show up on the main page and in every topic.</p>
<form method="POST">
<label for="title">Title</label>
<input name="title" id="title" type="text" required autocomplete="off" placeholder="Required" value="{{ current.title }}"><br>
<label for="body">Body</label>
{{ babycode_editor_component('body', ta_placeholder='MOTD body (required)', banned_tags=['img', 'spoiler'], prefill=current.body_original_markup) }}
<input type="submit" value="Save">
<input class="critical" type="submit" formaction="{{ url_for('mod.motd_delete') }}" value="Delete MOTD" formnovalidate {{"disabled" if not current else ""}}>
</form>
</div>
{% endblock %}

View File

@@ -6,6 +6,7 @@
<ul> <ul>
<li><a href="{{ url_for('mod.user_list') }}">User list</a></li> <li><a href="{{ url_for('mod.user_list') }}">User list</a></li>
<li><a href="{{ url_for('mod.sort_topics') }}">Sort topics</a></li> <li><a href="{{ url_for('mod.sort_topics') }}">Sort topics</a></li>
<li><a href="{{ url_for('mod.motd_editor') }}">Message of the Day</a></li>
</ul> </ul>
</div> </div>
{% endblock %} {% endblock %}

View File

@@ -24,7 +24,7 @@
</nav> </nav>
{% if topic['is_locked'] %} {% if topic['is_locked'] %}
{{ infobox("This topic is locked. Only moderators can create new threads.", InfoboxKind.INFO) }} {{ infobox("This topic is locked.;Only moderators can create new threads.", InfoboxKind.INFO) }}
{% endif %} {% endif %}
{% if threads_list | length == 0 %} {% if threads_list | length == 0 %}

View File

@@ -147,13 +147,17 @@
if (markup === "" || markup === previousMarkup) { if (markup === "" || markup === previousMarkup) {
return; return;
} }
const bannedTags = JSON.parse(document.getElementById('babycode-banned-tags').value);
previousMarkup = markup; previousMarkup = markup;
const req = await fetch(previewEndpoint, { const req = await fetch(previewEndpoint, {
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
}, },
body: JSON.stringify({markup: markup}) body: JSON.stringify({
markup: markup,
banned_tags: bannedTags,
})
}) })
if (!req.ok) { if (!req.ok) {
switch (req.status) { switch (req.status) {

View File

@@ -1082,7 +1082,6 @@ ul.horizontal, ol.horizontal {
} }
} }
.new-concept-notification.hidden { .new-concept-notification.hidden {
display: none; display: none;
} }