Compare commits

...

5 Commits

10 changed files with 280 additions and 35 deletions

View File

@ -7,6 +7,7 @@ from .constants import (
PermissionLevel, permission_level_string,
InfoboxKind, InfoboxIcons, InfoboxHTMLClass
)
from .lib.babycode import babycode_to_html, EMOJI
from datetime import datetime
import os
import time
@ -103,6 +104,7 @@ def create_app():
"InfoboxHTMLClass": InfoboxHTMLClass,
"InfoboxKind": InfoboxKind,
"__commit": commit,
"__emoji": EMOJI,
}
@app.context_processor
@ -124,4 +126,18 @@ def create_app():
def permission_string(term):
return permission_level_string(term)
@app.template_filter('babycode')
def babycode_filter(markup):
return babycode_to_html(markup)
@app.template_filter('extract_h2')
def extract_h2(content):
import re
pattern = r'<h2\s+id="([^"]+)"[^>]*>(.*?)<\/h2>'
matches = re.findall(pattern, content, re.IGNORECASE | re.DOTALL)
return [
{'id': id_.strip(), 'text': text.strip()}
for id_, text in matches
]
return app

View File

@ -5,7 +5,7 @@ import re
def tag_code(children, attr):
is_inline = children.find('\n') == -1
if is_inline:
return f"<code class=\"inline_code\">{children}</code>"
return f"<code class=\"inline-code\">{children}</code>"
else:
t = children.strip()
button = f"<button type=button class=\"copy-code\" value={t}>Copy</button>"
@ -28,6 +28,53 @@ TAGS = {
"ol": lambda children, attr: f"<ol>{tag_list(children)}</ol>",
}
def make_emoji(name, code):
return f' <img class=emoji src="/static/emoji/{name}.png" alt="{name}" title=":{code}:">'
EMOJI = {
'angry': make_emoji('angry', 'angry'),
'(': make_emoji('frown', '('),
'D': make_emoji('grin', 'D'),
'imp': make_emoji('imp', 'imp'),
'angryimp': make_emoji('impangry', 'angryimp'),
'impangry': make_emoji('impangry', 'impangry'),
'lobster': make_emoji('lobster', 'lobster'),
'|': make_emoji('neutral', '|'),
'pensive': make_emoji('pensive', 'pensive'),
')': make_emoji('smile', ')'),
'smiletear': make_emoji('smiletear', 'smiletear'),
'crytear': make_emoji('smiletear', 'crytear'),
',': make_emoji('sob', ','),
'T': make_emoji('sob', 'T'),
'cry': make_emoji('sob', 'cry'),
'sob': make_emoji('sob', 'sob'),
'o': make_emoji('surprised', 'o'),
'O': make_emoji('surprised', 'O'),
'hmm': make_emoji('think', 'hmm'),
'think': make_emoji('think', 'think'),
'thinking': make_emoji('think', 'thinking'),
'P': make_emoji('tongue', 'P'),
'p': make_emoji('tongue', 'p'),
'weary': make_emoji('weary', 'weary'),
';': make_emoji('wink', ';'),
'wink': make_emoji('wink', 'wink'),
}
TEXT_ONLY = ["code"]
def break_lines(text):
@ -40,8 +87,10 @@ def babycode_to_html(s):
parser = Parser(subj)
parser.valid_bbcode_tags = TAGS.keys()
parser.bbcode_tags_only_text_children = TEXT_ONLY
parser.valid_emotes = EMOJI.keys()
elements = parser.parse()
print(elements)
out = ""
def fold(element, nobr):
if isinstance(element, str):
@ -59,6 +108,8 @@ def babycode_to_html(s):
return res
case "link":
return f"<a href=\"{element['url']}\">{element['url']}</a>"
case 'emote':
return EMOJI[element['name']]
case "rule":
return "<hr>"
for e in elements:

View File

@ -71,6 +71,18 @@ class Users(Model):
subscriptions.user_id = ?"""
return db.query(q, self.id)
def can_post_to_topic(self, topic):
if self.is_guest():
return False
if self.is_mod():
return True
if topic['is_locked']:
return False
return True
class Topics(Model):
table = "topics"

View File

@ -1,4 +1,4 @@
from flask import Blueprint, redirect, url_for
from flask import Blueprint, redirect, url_for, render_template
bp = Blueprint("app", __name__, url_prefix = "/")
@ -9,4 +9,4 @@ def index():
@bp.route("/babycode")
def babycode_guide():
return "not yet"
return render_template('babycode.html')

View File

@ -18,7 +18,7 @@ def thread(slug):
POSTS_PER_PAGE = 10
thread = Threads.find({"slug": slug})
if not thread:
return "no"
return redirect(url_for('topics.all_topics'))
post_count = Posts.count({"thread_id": thread.id})
page_count = max(math.ceil(post_count / POSTS_PER_PAGE), 1)
@ -69,12 +69,12 @@ def thread(slug):
def reply(slug):
thread = Threads.find({"slug": slug})
if not thread:
return "no"
return redirect(url_for('topics.all_topics'))
user = get_active_user()
if user.is_guest():
return "no"
return redirect(url_for('.thread', slug=slug))
if thread.locked() and not user.is_mod():
return "no"
return redirect(url_for('.thread', slug=slug))
post_content = request.form['post_content']
post = create_post(thread.id, user.id, post_content)
@ -102,10 +102,12 @@ def create_form():
topic = Topics.find({"id": request.form['topic_id']})
user = get_active_user()
if not topic:
return "no"
flash('Invalid topic', InfoboxKind.ERROR)
return redirect(url_for('.create'))
if topic.is_locked and not get_active_user().is_mod():
return "no"
flash(f'Topic "{topic.name}" is locked', InfoboxKind.ERROR)
return redirect(url_for('.create'))
title = request.form['title'].strip()
now = int(time.time())
@ -128,8 +130,10 @@ def create_form():
def lock(slug):
user = get_active_user()
thread = Threads.find({'slug': slug})
if not thread:
return redirect(url_for('topics.all_topics'))
if not ((thread.user_id == user.id) or user.is_mod()):
return 'no'
return redirect(url_for('.thread', slug=slug))
target_op = request.form.get('target_op')
thread.update({
'is_locked': target_op
@ -143,8 +147,10 @@ def lock(slug):
def sticky(slug):
user = get_active_user()
thread = Threads.find({'slug': slug})
if not thread:
return redirect(url_for('topics.all_topics'))
if not ((thread.user_id == user.id) or user.is_mod()):
return 'no'
return redirect(url_for('.thread', slug=slug))
target_op = request.form.get('target_op')
thread.update({
'is_stickied': target_op
@ -167,12 +173,12 @@ def move(slug):
'id': new_topic_id
})
if not new_topic:
return 'no'
return redirect(url_for('topics.all_topics'))
thread = Threads.find({
'slug': slug
})
if not thread:
return 'no'
return redirect(url_for('topics.all_topics'))
if new_topic.id == thread.topic_id:
flash('Thread is already in this topic.', InfoboxKind.ERROR)
return redirect(url_for('.thread', slug=slug))
@ -189,7 +195,7 @@ def subscribe(slug):
user = get_active_user()
thread = Threads.find({'slug': slug})
if not thread:
return 'no'
return redirect(url_for('topics.all_topics'))
subscription = Subscriptions.find({
'user_id': user.id,
'thread_id': thread.id,
@ -204,11 +210,11 @@ def subscribe(slug):
})
elif request.form['subscribe'] == 'unsubscribe':
if not subscription:
return 'no'
return redirect(url_for('.thread', slug=slug))
subscription.delete()
elif request.form['subscribe'] == 'read':
if not subscription:
return 'no'
return redirect(url_for('.thread', slug=slug))
subscription.update({
'last_seen': int(time.time())
})

View File

@ -51,7 +51,7 @@ def topic(slug):
"slug": slug
})
if not target_topic:
return "no"
return redirect(url_for('.all_topics'))
threads_count = Threads.count({
"topic_id": target_topic.id
@ -77,7 +77,7 @@ def topic(slug):
def edit(slug):
topic = Topics.find({"slug": slug})
if not topic:
return "no"
return redirect(url_for('.all_topics'))
return render_template("topics/edit.html", topic=topic)
@ -87,7 +87,7 @@ def edit(slug):
def edit_post(slug):
topic = Topics.find({"slug": slug})
if not topic:
return "no"
return redirect(url_for('.all_topics'))
topic.update({
"name": request.form.get('name', default = topic.name).strip(),
@ -104,7 +104,7 @@ def edit_post(slug):
def delete(slug):
topic = Topics.find({"slug": slug})
if not topic:
return "no"
return redirect(url_for('.all_topics'))
topic.delete()

View File

@ -9,6 +9,7 @@ from ..constants import InfoboxKind, PermissionLevel
from ..auth import digest, verify
from wand.image import Image
from wand.exceptions import WandException
from datetime import datetime, timedelta
import secrets
import time
import re
@ -64,7 +65,18 @@ def create_session(user_id):
return Sessions.create({
"key": secrets.token_hex(16),
"user_id": user_id,
"expires_at": int(time.time()) + 32 * 24 * 60 * 60,
"expires_at": int(time.time()) + 31 * 24 * 60 * 60,
})
def extend_session(user_id):
session_obj = Sessions.find({'key': session['pyrom_session_key']})
if not session_obj:
return
new_duration = timedelta(31)
current_app.permanent_session_lifetime = new_duration
session.modified = True
session_obj.update({
'expires_at': int(time.time()) + 31 * 24 * 60 * 60
})
@ -269,14 +281,17 @@ def settings_form(username):
def set_avatar(username):
user = get_active_user()
if user.is_guest():
return 'no'
flash('You must be logged in to perform this action.', InfoboxKind.ERROR)
return redirect(url_for('.settings', user.username))
if 'avatar' not in request.files:
return 'no!...'
flash('Avatar missing.', InfoboxKind.ERROR)
return redirect(url_for('.settings', user.username))
file = request.files['avatar']
if file.filename == '':
return 'no..?'
flash('Avatar missing.', InfoboxKind.ERROR)
return redirect(url_for('.settings', user.username))
file_bytes = file.read()
@ -300,7 +315,30 @@ def set_avatar(username):
old_avatar.delete()
return redirect(url_for('.settings', username=user.username))
else:
return 'uhhhh no'
flash('Something went wrong. Please try again later.', InfoboxKind.WARN)
return redirect(url_for('.settings', user.username))
@bp.post('/<username>/change_password')
@login_required
def change_password(username):
user = get_active_user()
password = request.form.get('new_password')
password2 = request.form.get('new_password2')
if not validate_password(password):
flash("Invalid password.", InfoboxKind.ERROR)
return redirect(url_for('.settings', username=user.username))
if password != password2:
flash("Passwords do not match.", InfoboxKind.ERROR)
return redirect(url_for('.settings', username=user.username))
hashed = digest(password)
user.update({'password_hash': hashed})
extend_session(user.id)
flash('Password updated.', InfoboxKind.INFO)
return redirect(url_for('.settings', username=user.username))
@bp.post('/<username>/clear_avatar')
@ -308,7 +346,7 @@ def set_avatar(username):
def clear_avatar(username):
user = get_active_user()
if user.is_default_avatar():
return 'no'
return redirect(url_for('.settings', user.username))
old_avatar = Avatars.find({'id': user.avatar_id})
user.update({'avatar_id': 1})
@ -336,9 +374,9 @@ def log_out():
def confirm_user(user_id):
target_user = Users.find({"id": user_id})
if not target_user:
return "no"
return redirect(url_for('.all_topics'))
if int(target_user.permission) > PermissionLevel.GUEST.value:
return "no"
return redirect(url_for('.page', username=target_user.username))
target_user.update({
"permission": PermissionLevel.USER.value,
@ -353,9 +391,9 @@ def confirm_user(user_id):
def mod_user(user_id):
target_user = Users.find({"id": user_id})
if not target_user:
return "no"
return redirect(url_for('.all_topics'))
if target_user.is_mod():
return "no"
return redirect(url_for('.page', username=target_user.username))
target_user.update({
"permission": PermissionLevel.MODERATOR.value,
@ -369,9 +407,9 @@ def mod_user(user_id):
def demod_user(user_id):
target_user = Users.find({"id": user_id})
if not target_user:
return "no"
return redirect(url_for('.all_topics'))
if not target_user.is_mod():
return "no"
return redirect(url_for('.page', username=target_user.username))
target_user.update({
"permission": PermissionLevel.USER.value,
@ -385,9 +423,9 @@ def demod_user(user_id):
def guest_user(user_id):
target_user = Users.find({"id": user_id})
if not target_user:
return "no"
return redirect(url_for('.all_topics'))
if target_user.is_mod():
return "no"
return redirect(url_for('.page', username=target_user.username))
target_user.update({
"permission": PermissionLevel.GUEST.value,

114
app/templates/babycode.html Normal file
View File

@ -0,0 +1,114 @@
<!-- kate: remove-trailing-space off; -->
{% extends 'base.html' %}
{% block title %}babycode guide{% endblock %}
{% block content %}
<div class=darkbg>
<h1 class="thread-title">Babycode guide</h1>
</div>
<div class="babycode-guide-container">
<div class="guide-topics">
{% set sections %}
<section class="babycode-guide-section">
<h2 id="what-is-babycode">What is babycode?</h2>
<p>You may be familiar with BBCode, a loosely related family of markup languages popular on forums. Babycode is another, simplified, dialect of those languages. It is a way of formatting text by enclosing parts of it in special tags.</p>
</section>
<section class="babycode-guide-section">
<h2 id="text-formatting-tags">Text formatting tags</h2>
<ul>
<li>To make some text <strong>bold</strong>, enclose it in <code class="inline-code">[b][/b]</code>:<br>
[b]Hello World[/b]<br>
Will become<br>
<strong>Hello World</strong>
</ul>
<ul>
<li>To <em>italicize</em> text, enclose it in <code class="inline-code">[i][/i]</code>:<br>
[i]Hello World[/i]<br>
Will become<br>
<em>Hello World</em>
</ul>
<ul>
<li>To make some text <del>strikethrough</del>, enclose it in <code class="inline-code">[s][/s]</code>:<br>
[s]Hello World[/s]<br>
Will become<br>
<del>Hello World</del>
</ul>
</section>
<section class="babycode-guide-section">
<h2 id="emoji">Emoji</h2>
<p>There are a few emoji in the style of old forum emotes:</p>
<table class="emoji-table">
<tr>
<th>Short code</th>
<th>Emoji result</th>
</tr>
{% for emoji in __emoji %}
<tr>
<td>:{{ emoji }}:</td>
<td>{{ __emoji[emoji] | safe }}</td>
</tr>
{% endfor %}
</table>
<p>Special thanks to the <a href="https://gh.vercte.net/forumoji/">Forumoji project</a> and its contributors for these graphics.</p>
</section>
<section class="babycode-guide-section">
<h2 id="paragraph-rules">Paragraph rules</h2>
<p>Line breaks in babycode work like Markdown: to start a new paragraph, use two line breaks:</p>
{{ '[code]paragraph 1\n\nparagraph 2[/code]' | babycode | safe }}
Will produce:<br>
{{ 'paragraph 1\n\nparagraph 2' | babycode | safe }}
<p>To break a line without starting a new paragraph, end a line with two spaces:</p>
{{ '[code]paragraph 1 \nstill paragraph 1[/code]' | babycode | safe }}
That will produce:<br>
{{ 'paragraph 1 \nstill paragraph 1' | babycode | safe }}
</section>
<section class="babycode-guide-section">
<h2 id="links">Links</h2>
<p>Loose links (starting with http:// or https://) will automatically get converted to clickable links. To add a label to a link, use<br><code class="inline-code">[url=https://example.com]Link label[/url]</code>:<br>
<a href="https://example.com">Link label</a></p>
</section>
<section class="babycode-guide-section">
<h2 id="attaching-an-image">Attaching an image</h2>
<p>To add an image to your post, use the <code class="inline-code">[img]</code> tag:<br>
<code class="inline-code">[img=https://forum.poto.cafe/avatars/default.webp]the Python logo with a cowboy hat[/img]</code>
{{ '[img=/static/avatars/default.webp]the Python logo with a cowboy hat[/img]' | babycode | safe }}
</p>
<p>Text inside the tag becomes the alt text. The attribute is the image URL.</p>
<p>Images will always break up a paragraph and will get scaled down to a maximum of 400px. The text inside the tag will become the image's alt text.</p>
</section>
<section class="babycode-guide-section">
<h2 id="adding-code-blocks">Adding code blocks</h2>
{% set code = 'func _ready() -> void:\n\tprint("hello world!")' %}
<p>There are two kinds of code blocks recognized by babycode: inline and block. Inline code blocks do not break a paragraph. They can be added with <code class="inline-code">[code]your code here[/code]</code>. As long as there are no line breaks inside the code block, it is considered inline. If there are any, it will produce this:</p>
{{ ('[code]%s[/code]' % code) | babycode | safe }}
<br>
<p>Inline code tags look like this: {{ '[code]Inline code[/code]' | babycode | safe }}</p>
<p>Babycodes are not parsed inside code blocks.</p>
</section>
<section class="babycode-guide-section">
<h2 id="quoting">Quoting</h2>
<p>Text enclosed within <code class="inline-code">[quote][/quote]</code> will look like a quote:</p>
<blockquote>A man provided with paper, pencil, and rubber, and subject to strict discipline, is in effect a universal machine.</blockquote>
</section>
<section class="babycode-guide-section">
<h2 id="lists">Lists</h2>
{% set list = '[ul]\nitem 1\n\nitem 2\n\nitem 3 \nstill item 3 (break line without inserting a new item by using two spaces at the end of a line)\n[/ul]' %}
<p>There are two kinds of lists, ordered (1, 2, 3, ...) and unordered (bullet points). Ordered lists are made with <code class="inline-code">[ol][/ol]</code> tags, and unordered with <code class="inline-code">[ul][/ul]</code>. Every new paragraph according to the <a href="#paragraph-rules">usual paragraph rules</a> will create a new list item. For example:</p>
{{ ('[code]%s[/code]' % list) | babycode | safe }}
Will produce the following list:
{{ list | babycode | safe }}
</section>
{% endset %}
{{ sections | safe }}
</div>
<div class="guide-toc">
<h2>Table of contents</h2>
{% set toc = sections | extract_h2 %}
<ul>
{% for heading in toc %}
<li><a href='#{{ heading.id }}'>{{ heading.text }}</a></li>
{% endfor %}
</ul>
</div>
</div>
{% endblock %}

View File

@ -8,7 +8,8 @@
<label for="topic_id">Topic</label>
<select name="topic_id" id="topic_id" autocomplete="off">
{% for topic in all_topics %}
<option value="{{ topic['id'] }}" {{"selected" if (request.args.get('topic_id')) == (topic['id'] | string) else ""}}>{{ topic['name'] }}</option>
{% set disable_topic = active_user and not active_user.can_post_to_topic(topic) %}
<option value="{{ topic['id'] }}" {{"selected" if (request.args.get('topic_id')) == (topic['id'] | string) else ""}} {{'disabled' if disable_topic else ''}} >{{ topic['name'] }}{{ ' (locked)' if topic.is_locked }}</option>
{% endfor %}
</select><br>
<label for="title">Thread title</label>

View File

@ -28,5 +28,12 @@
<label for='subscribe_by_default'>Subscribe to thread by default when responding</label><br>
<input type='submit' value='Save settings'>
</form>
<form method='post' action='{{ url_for('users.change_password', username=active_user.username) }}'>
<label for="new_password">Change password</label><br>
<input type="password" id="new_password" name="new_password" pattern="(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[\W_])(?!.*\s).{10,}" title="10+ chars with: 1 uppercase, 1 lowercase, 1 number, 1 special char, and no spaces" required autocomplete="new-password"><br>
<label for="new_password2">Confirm new password</label><br>
<input type="password" id="new_password2" name="new_password2" pattern="(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[\W_])(?!.*\s).{10,}" title="10+ chars with: 1 uppercase, 1 lowercase, 1 number, 1 special char, and no spaces" required autocomplete="new-password"><br>
<input class="warn" type="submit" value="Change password">
</form>
</div>
{% endblock %}