Compare commits
11 Commits
5c03ba3d3a
...
eb76338c4a
| Author | SHA1 | Date | |
|---|---|---|---|
|
eb76338c4a
|
|||
|
9951ed3fae
|
|||
|
a7876ca410
|
|||
|
7c037d1593
|
|||
|
24fe0aba30
|
|||
|
a185208fc1
|
|||
|
1d5d5a8c64
|
|||
|
414298b4b4
|
|||
|
c3a3ead852
|
|||
|
54907db896
|
|||
|
db2d09cb03
|
@@ -1,4 +1,4 @@
|
|||||||
from flask import Flask, session, request
|
from flask import Flask, session, request, render_template
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
from .models import Avatars, Users, PostHistory, Posts, MOTD
|
from .models import Avatars, Users, PostHistory, Posts, MOTD
|
||||||
from .auth import digest
|
from .auth import digest
|
||||||
@@ -7,7 +7,7 @@ from .routes.threads import get_post_url
|
|||||||
from .constants import (
|
from .constants import (
|
||||||
PermissionLevel, permission_level_string,
|
PermissionLevel, permission_level_string,
|
||||||
InfoboxKind, InfoboxHTMLClass,
|
InfoboxKind, InfoboxHTMLClass,
|
||||||
REACTION_EMOJI,
|
REACTION_EMOJI, MOTD_BANNED_TAGS,
|
||||||
)
|
)
|
||||||
from .lib.babycode import babycode_to_html, EMOJI, BABYCODE_VERSION
|
from .lib.babycode import babycode_to_html, EMOJI, BABYCODE_VERSION
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
@@ -40,10 +40,11 @@ def create_admin():
|
|||||||
|
|
||||||
def create_deleted_user():
|
def create_deleted_user():
|
||||||
username = "DeletedUser"
|
username = "DeletedUser"
|
||||||
if Users.count({"username": username}) == 0:
|
if Users.count({"username": username.lower()}) == 0:
|
||||||
print("Creating DeletedUser")
|
print("Creating DeletedUser")
|
||||||
Users.create({
|
Users.create({
|
||||||
"username": username,
|
"username": username.lower(),
|
||||||
|
"display_name": username,
|
||||||
"password_hash": "",
|
"password_hash": "",
|
||||||
"permission": PermissionLevel.SYSTEM.value,
|
"permission": PermissionLevel.SYSTEM.value,
|
||||||
})
|
})
|
||||||
@@ -61,7 +62,7 @@ def reparse_babycode():
|
|||||||
with db.transaction():
|
with db.transaction():
|
||||||
for ph in post_histories:
|
for ph in post_histories:
|
||||||
ph.update({
|
ph.update({
|
||||||
'content': babycode_to_html(ph['original_markup']),
|
'content': babycode_to_html(ph['original_markup']).result,
|
||||||
'format_version': BABYCODE_VERSION,
|
'format_version': BABYCODE_VERSION,
|
||||||
})
|
})
|
||||||
print('Re-parsing posts done.')
|
print('Re-parsing posts done.')
|
||||||
@@ -76,7 +77,7 @@ def reparse_babycode():
|
|||||||
with db.transaction():
|
with db.transaction():
|
||||||
for user in users_with_sigs:
|
for user in users_with_sigs:
|
||||||
user.update({
|
user.update({
|
||||||
'signature_rendered': babycode_to_html(user['signature_original_markup']),
|
'signature_rendered': babycode_to_html(user['signature_original_markup']).result,
|
||||||
'signature_format_version': BABYCODE_VERSION,
|
'signature_format_version': BABYCODE_VERSION,
|
||||||
})
|
})
|
||||||
print(f'Re-parsed {len(users_with_sigs)} user sigs.')
|
print(f'Re-parsed {len(users_with_sigs)} user sigs.')
|
||||||
@@ -90,7 +91,7 @@ def reparse_babycode():
|
|||||||
with db.transaction():
|
with db.transaction():
|
||||||
for motd in stale_motds:
|
for motd in stale_motds:
|
||||||
motd.update({
|
motd.update({
|
||||||
'body_rendered': babycode_to_html(motd['body_original_markup'], banned_tags=MOTD_BANNED_TAGS),
|
'body_rendered': babycode_to_html(motd['body_original_markup'], banned_tags=MOTD_BANNED_TAGS).result,
|
||||||
'format_version': BABYCODE_VERSION,
|
'format_version': BABYCODE_VERSION,
|
||||||
})
|
})
|
||||||
print('Re-parsing MOTDs done.')
|
print('Re-parsing MOTDs done.')
|
||||||
@@ -175,6 +176,7 @@ def create_app():
|
|||||||
"__commit": commit,
|
"__commit": commit,
|
||||||
"__emoji": EMOJI,
|
"__emoji": EMOJI,
|
||||||
"REACTION_EMOJI": REACTION_EMOJI,
|
"REACTION_EMOJI": REACTION_EMOJI,
|
||||||
|
"MOTD_BANNED_TAGS": MOTD_BANNED_TAGS,
|
||||||
}
|
}
|
||||||
|
|
||||||
@app.context_processor
|
@app.context_processor
|
||||||
@@ -206,7 +208,7 @@ def create_app():
|
|||||||
|
|
||||||
@app.template_filter('babycode')
|
@app.template_filter('babycode')
|
||||||
def babycode_filter(markup):
|
def babycode_filter(markup):
|
||||||
return babycode_to_html(markup)
|
return babycode_to_html(markup).result
|
||||||
|
|
||||||
@app.template_filter('extract_h2')
|
@app.template_filter('extract_h2')
|
||||||
def extract_h2(content):
|
def extract_h2(content):
|
||||||
@@ -225,7 +227,7 @@ def create_app():
|
|||||||
elif request.path.startswith('/api/'):
|
elif request.path.startswith('/api/'):
|
||||||
return {'error': 'not found'}, e.code
|
return {'error': 'not found'}, e.code
|
||||||
else:
|
else:
|
||||||
return e
|
return render_template('common/404.html'), e.code
|
||||||
|
|
||||||
# this only happens at build time but
|
# this only happens at build time but
|
||||||
# build time is when updates are done anyway
|
# build time is when updates are done anyway
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ REACTION_EMOJI = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
MOTD_BANNED_TAGS = [
|
MOTD_BANNED_TAGS = [
|
||||||
'img', 'spoiler',
|
'img', 'spoiler', '@mention'
|
||||||
]
|
]
|
||||||
|
|
||||||
def permission_level_string(perm):
|
def permission_level_string(perm):
|
||||||
|
|||||||
24
app/db.py
24
app/db.py
@@ -189,6 +189,13 @@ class Model:
|
|||||||
raise AttributeError(f"No column '{key}'")
|
raise AttributeError(f"No column '{key}'")
|
||||||
|
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_data(cls, data):
|
||||||
|
instance = cls(cls.table)
|
||||||
|
instance._data = dict(data)
|
||||||
|
return instance
|
||||||
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def find(cls, condition):
|
def find(cls, condition):
|
||||||
row = db.QueryBuilder(cls.table)\
|
row = db.QueryBuilder(cls.table)\
|
||||||
@@ -196,9 +203,7 @@ class Model:
|
|||||||
.first()
|
.first()
|
||||||
if not row:
|
if not row:
|
||||||
return None
|
return None
|
||||||
instance = cls(cls.table)
|
return cls.from_data(row)
|
||||||
instance._data = dict(row)
|
|
||||||
return instance
|
|
||||||
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -207,11 +212,7 @@ class Model:
|
|||||||
.where(condition, operator)\
|
.where(condition, operator)\
|
||||||
.all()
|
.all()
|
||||||
res = []
|
res = []
|
||||||
for row in rows:
|
return [cls.from_data(row) for row in rows]
|
||||||
instance = cls(cls.table)
|
|
||||||
instance._data = dict(row)
|
|
||||||
res.append(instance)
|
|
||||||
return res
|
|
||||||
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -223,9 +224,7 @@ class Model:
|
|||||||
row = db.insert(cls.table, columns, *values.values())
|
row = db.insert(cls.table, columns, *values.values())
|
||||||
|
|
||||||
if row:
|
if row:
|
||||||
instance = cls(cls.table)
|
return cls.from_data(row)
|
||||||
instance._data = row
|
|
||||||
return instance
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
@@ -243,7 +242,8 @@ class Model:
|
|||||||
def select(cls, sel = "*"):
|
def select(cls, sel = "*"):
|
||||||
qb = db.QueryBuilder(cls.table).select(sel)
|
qb = db.QueryBuilder(cls.table).select(sel)
|
||||||
result = qb.all()
|
result = qb.all()
|
||||||
return result if result else []
|
# return result if result else []
|
||||||
|
return [cls.from_data(data) for data in (result if result else [])]
|
||||||
|
|
||||||
|
|
||||||
def update(self, data):
|
def update(self, data):
|
||||||
|
|||||||
@@ -6,6 +6,16 @@ from pygments.lexers import get_lexer_by_name
|
|||||||
from pygments.util import ClassNotFound as PygmentsClassNotFound
|
from pygments.util import ClassNotFound as PygmentsClassNotFound
|
||||||
import re
|
import re
|
||||||
|
|
||||||
|
class BabycodeParseResult:
|
||||||
|
def __init__(self, result, mentions=[]):
|
||||||
|
self.result = result
|
||||||
|
self.mentions = mentions
|
||||||
|
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.result
|
||||||
|
|
||||||
|
|
||||||
BABYCODE_VERSION = 5
|
BABYCODE_VERSION = 5
|
||||||
|
|
||||||
NAMED_COLORS = [
|
NAMED_COLORS = [
|
||||||
@@ -78,6 +88,9 @@ def tag_list(children):
|
|||||||
return " ".join([f"<li>{x}</li>" for x in list_body.split("\1") if x])
|
return " ".join([f"<li>{x}</li>" for x in list_body.split("\1") if x])
|
||||||
|
|
||||||
def tag_color(children, attr, surrounding):
|
def tag_color(children, attr, surrounding):
|
||||||
|
if not attr:
|
||||||
|
return f"[color]{children}[/color]"
|
||||||
|
|
||||||
hex_re = r"^#?([0-9a-f]{6}|[0-9a-f]{3})$"
|
hex_re = r"^#?([0-9a-f]{6}|[0-9a-f]{3})$"
|
||||||
potential_color = attr.lower().strip()
|
potential_color = attr.lower().strip()
|
||||||
|
|
||||||
@@ -206,6 +219,24 @@ def is_inline(e):
|
|||||||
|
|
||||||
return e['type'] != 'rule'
|
return e['type'] != 'rule'
|
||||||
|
|
||||||
|
def make_mention(e, mentions):
|
||||||
|
from ..models import Users
|
||||||
|
from flask import url_for
|
||||||
|
target_user = Users.find({'username': e['name'].lower()})
|
||||||
|
if not target_user:
|
||||||
|
return f"@{e['name']}"
|
||||||
|
|
||||||
|
mention_data = {
|
||||||
|
'mention_text': f"@{e['name']}",
|
||||||
|
'mentioned_user_id': int(target_user.id),
|
||||||
|
"start": e['start'],
|
||||||
|
"end": e['end'],
|
||||||
|
}
|
||||||
|
if mention_data not in mentions:
|
||||||
|
mentions.append(mention_data)
|
||||||
|
|
||||||
|
return f"<a class='mention{' display' if target_user.has_display_name() else ''}' href='{url_for('users.page', username=target_user.username)}' title='@{target_user.username}' data-init='highlightMentions' data-username='{target_user.username}'>{'@' if not target_user.has_display_name() else ''}{target_user.get_readable_name()}</a>"
|
||||||
|
|
||||||
def should_collapse(text, surrounding):
|
def should_collapse(text, surrounding):
|
||||||
if not isinstance(text, str):
|
if not isinstance(text, str):
|
||||||
return False
|
return False
|
||||||
@@ -218,15 +249,19 @@ def should_collapse(text, surrounding):
|
|||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def babycode_to_html(s, banned_tags=None):
|
def sanitize(s):
|
||||||
allowed_tags = list(TAGS.keys())
|
return escape(s.strip().replace('\r\n', '\n').replace('\r', '\n'))
|
||||||
|
|
||||||
|
def babycode_to_html(s, banned_tags={}):
|
||||||
|
allowed_tags = set(TAGS.keys())
|
||||||
if banned_tags is not None:
|
if banned_tags is not None:
|
||||||
for tag in banned_tags:
|
for tag in banned_tags:
|
||||||
allowed_tags.remove(tag)
|
allowed_tags.discard(tag)
|
||||||
subj = escape(s.strip().replace('\r\n', '\n').replace('\r', '\n'))
|
subj = sanitize(s)
|
||||||
parser = Parser(subj)
|
parser = Parser(subj)
|
||||||
parser.valid_bbcode_tags = allowed_tags
|
parser.valid_bbcode_tags = allowed_tags
|
||||||
parser.bbcode_tags_only_text_children = TEXT_ONLY
|
parser.bbcode_tags_only_text_children = TEXT_ONLY
|
||||||
|
parser.mentions_allowed = '@mention' not in banned_tags
|
||||||
parser.valid_emotes = EMOJI.keys()
|
parser.valid_emotes = EMOJI.keys()
|
||||||
|
|
||||||
uncollapsed = parser.parse()
|
uncollapsed = parser.parse()
|
||||||
@@ -241,6 +276,7 @@ def babycode_to_html(s, banned_tags=None):
|
|||||||
elements.append(e)
|
elements.append(e)
|
||||||
|
|
||||||
out = ""
|
out = ""
|
||||||
|
mentions = []
|
||||||
def fold(element, nobr, surrounding):
|
def fold(element, nobr, surrounding):
|
||||||
if isinstance(element, str):
|
if isinstance(element, str):
|
||||||
if nobr:
|
if nobr:
|
||||||
@@ -266,6 +302,8 @@ def babycode_to_html(s, banned_tags=None):
|
|||||||
return EMOJI[element['name']]
|
return EMOJI[element['name']]
|
||||||
case "rule":
|
case "rule":
|
||||||
return "<hr>"
|
return "<hr>"
|
||||||
|
case "mention":
|
||||||
|
return make_mention(element, mentions)
|
||||||
|
|
||||||
for i in range(len(elements)):
|
for i in range(len(elements)):
|
||||||
e = elements[i]
|
e = elements[i]
|
||||||
@@ -274,4 +312,4 @@ def babycode_to_html(s, banned_tags=None):
|
|||||||
elements[i + 1] if i+1 < len(elements) else None
|
elements[i + 1] if i+1 < len(elements) else None
|
||||||
)
|
)
|
||||||
out = out + fold(e, False, surrounding)
|
out = out + fold(e, False, surrounding)
|
||||||
return out
|
return BabycodeParseResult(out, mentions)
|
||||||
|
|||||||
@@ -6,12 +6,14 @@ PAT_EMOTE = r"[^\s:]"
|
|||||||
PAT_BBCODE_TAG = r"\w"
|
PAT_BBCODE_TAG = r"\w"
|
||||||
PAT_BBCODE_ATTR = r"[^\]]"
|
PAT_BBCODE_ATTR = r"[^\]]"
|
||||||
PAT_LINK = r"https?:\/\/[\w\-_.?:\/=&~@#%]+[\w\-\/]"
|
PAT_LINK = r"https?:\/\/[\w\-_.?:\/=&~@#%]+[\w\-\/]"
|
||||||
|
PAT_MENTION = r'[a-zA-Z0-9_-]'
|
||||||
|
|
||||||
class Parser:
|
class Parser:
|
||||||
def __init__(self, src_str):
|
def __init__(self, src_str):
|
||||||
self.valid_bbcode_tags = []
|
self.valid_bbcode_tags = {}
|
||||||
self.valid_emotes = []
|
self.valid_emotes = []
|
||||||
self.bbcode_tags_only_text_children = [],
|
self.bbcode_tags_only_text_children = []
|
||||||
|
self.mentions_allowed = True
|
||||||
self.source = src_str
|
self.source = src_str
|
||||||
self.position = 0
|
self.position = 0
|
||||||
self.position_stack = []
|
self.position_stack = []
|
||||||
@@ -206,6 +208,25 @@ class Parser:
|
|||||||
"url": word
|
"url": word
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def parse_mention(self):
|
||||||
|
if not self.mentions_allowed:
|
||||||
|
return None
|
||||||
|
|
||||||
|
self.save_position()
|
||||||
|
|
||||||
|
if not self.check_char('@'):
|
||||||
|
self.restore_position()
|
||||||
|
return None
|
||||||
|
|
||||||
|
mention = self.match_pattern(PAT_MENTION)
|
||||||
|
self.forget_position()
|
||||||
|
return {
|
||||||
|
"type": "mention",
|
||||||
|
"name": mention,
|
||||||
|
"start": self.position - len(mention) - 1,
|
||||||
|
"end": self.position,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def parse_element(self, siblings):
|
def parse_element(self, siblings):
|
||||||
if self.is_end_of_source():
|
if self.is_end_of_source():
|
||||||
@@ -214,7 +235,8 @@ class Parser:
|
|||||||
element = self.parse_emote() \
|
element = self.parse_emote() \
|
||||||
or self.parse_bbcode() \
|
or self.parse_bbcode() \
|
||||||
or self.parse_rule() \
|
or self.parse_rule() \
|
||||||
or self.parse_link()
|
or self.parse_link() \
|
||||||
|
or self.parse_mention()
|
||||||
|
|
||||||
if element is None:
|
if element is None:
|
||||||
if len(siblings) > 0:
|
if len(siblings) > 0:
|
||||||
|
|||||||
@@ -22,6 +22,18 @@ def create_default_bookmark_collections():
|
|||||||
for user in user_ids_without_default_collection:
|
for user in user_ids_without_default_collection:
|
||||||
BookmarkCollections.create_default(user['id'])
|
BookmarkCollections.create_default(user['id'])
|
||||||
|
|
||||||
|
def add_display_name():
|
||||||
|
dq = 'ALTER TABLE "users" ADD COLUMN "display_name" TEXT NOT NULL DEFAULT ""'
|
||||||
|
db.execute(dq)
|
||||||
|
from .models import Users
|
||||||
|
for user in Users.select():
|
||||||
|
data = {
|
||||||
|
'username': user.username.lower(),
|
||||||
|
}
|
||||||
|
if user.username.lower() != user.username:
|
||||||
|
data['display_name'] = user.username
|
||||||
|
user.update(data)
|
||||||
|
|
||||||
# format: [str|tuple(str, any...)|callable]
|
# format: [str|tuple(str, any...)|callable]
|
||||||
MIGRATIONS = [
|
MIGRATIONS = [
|
||||||
migrate_old_avatars,
|
migrate_old_avatars,
|
||||||
@@ -30,6 +42,7 @@ MIGRATIONS = [
|
|||||||
'ALTER TABLE "post_history" ADD COLUMN "format_version" INTEGER DEFAULT NULL',
|
'ALTER TABLE "post_history" ADD COLUMN "format_version" INTEGER DEFAULT NULL',
|
||||||
add_signature_format,
|
add_signature_format,
|
||||||
create_default_bookmark_collections,
|
create_default_bookmark_collections,
|
||||||
|
add_display_name,
|
||||||
]
|
]
|
||||||
|
|
||||||
def run_migrations():
|
def run_migrations():
|
||||||
|
|||||||
@@ -53,7 +53,8 @@ class Users(Model):
|
|||||||
COUNT(DISTINCT threads.id) AS thread_count,
|
COUNT(DISTINCT threads.id) AS thread_count,
|
||||||
MAX(threads.title) FILTER (WHERE threads.created_at = latest.created_at) AS latest_thread_title,
|
MAX(threads.title) FILTER (WHERE threads.created_at = latest.created_at) AS latest_thread_title,
|
||||||
MAX(threads.slug) FILTER (WHERE threads.created_at = latest.created_at) AS latest_thread_slug,
|
MAX(threads.slug) FILTER (WHERE threads.created_at = latest.created_at) AS latest_thread_slug,
|
||||||
inviter.username AS inviter_username
|
inviter.username AS inviter_username,
|
||||||
|
inviter.display_name AS inviter_display_name
|
||||||
FROM users
|
FROM users
|
||||||
LEFT JOIN posts ON posts.user_id = users.id
|
LEFT JOIN posts ON posts.user_id = users.id
|
||||||
LEFT JOIN threads ON threads.user_id = users.id
|
LEFT JOIN threads ON threads.user_id = users.id
|
||||||
@@ -106,6 +107,15 @@ class Users(Model):
|
|||||||
res = db.query(q, self.id)
|
res = db.query(q, self.id)
|
||||||
return [BookmarkCollections.find({'id': bc['id']}) for bc in res]
|
return [BookmarkCollections.find({'id': bc['id']}) for bc in res]
|
||||||
|
|
||||||
|
def get_readable_name(self):
|
||||||
|
if self.display_name:
|
||||||
|
return self.display_name
|
||||||
|
|
||||||
|
return self.username
|
||||||
|
|
||||||
|
def has_display_name(self):
|
||||||
|
return self.display_name != ''
|
||||||
|
|
||||||
|
|
||||||
class Topics(Model):
|
class Topics(Model):
|
||||||
table = "topics"
|
table = "topics"
|
||||||
@@ -116,6 +126,7 @@ class Topics(Model):
|
|||||||
SELECT
|
SELECT
|
||||||
topics.id, topics.name, topics.slug, topics.description, topics.is_locked,
|
topics.id, topics.name, topics.slug, topics.description, topics.is_locked,
|
||||||
users.username AS latest_thread_username,
|
users.username AS latest_thread_username,
|
||||||
|
users.display_name AS latest_thread_display_name,
|
||||||
threads.title AS latest_thread_title,
|
threads.title AS latest_thread_title,
|
||||||
threads.slug AS latest_thread_slug,
|
threads.slug AS latest_thread_slug,
|
||||||
threads.created_at AS latest_thread_created_at
|
threads.created_at AS latest_thread_created_at
|
||||||
@@ -141,7 +152,7 @@ class Topics(Model):
|
|||||||
SELECT
|
SELECT
|
||||||
threads.topic_id, threads.id AS thread_id, threads.title AS thread_title, threads.slug AS thread_slug,
|
threads.topic_id, threads.id AS thread_id, threads.title AS thread_title, threads.slug AS thread_slug,
|
||||||
posts.id AS post_id, posts.created_at AS post_created_at,
|
posts.id AS post_id, posts.created_at AS post_created_at,
|
||||||
users.username,
|
users.username, users.display_name,
|
||||||
ROW_NUMBER() OVER (PARTITION BY threads.topic_id ORDER BY posts.created_at DESC) AS rn
|
ROW_NUMBER() OVER (PARTITION BY threads.topic_id ORDER BY posts.created_at DESC) AS rn
|
||||||
FROM
|
FROM
|
||||||
threads
|
threads
|
||||||
@@ -154,7 +165,7 @@ class Topics(Model):
|
|||||||
topic_id,
|
topic_id,
|
||||||
thread_id, thread_title, thread_slug,
|
thread_id, thread_title, thread_slug,
|
||||||
post_id, post_created_at,
|
post_id, post_created_at,
|
||||||
username
|
username, display_name
|
||||||
FROM
|
FROM
|
||||||
ranked_threads
|
ranked_threads
|
||||||
WHERE
|
WHERE
|
||||||
@@ -170,6 +181,7 @@ class Topics(Model):
|
|||||||
'thread_slug': thread['thread_slug'],
|
'thread_slug': thread['thread_slug'],
|
||||||
'post_id': thread['post_id'],
|
'post_id': thread['post_id'],
|
||||||
'username': thread['username'],
|
'username': thread['username'],
|
||||||
|
'display_name': thread['display_name'],
|
||||||
'post_created_at': thread['post_created_at']
|
'post_created_at': thread['post_created_at']
|
||||||
}
|
}
|
||||||
return active_threads
|
return active_threads
|
||||||
@@ -185,7 +197,9 @@ class Topics(Model):
|
|||||||
SELECT
|
SELECT
|
||||||
threads.id, threads.title, threads.slug, threads.created_at, threads.is_locked, threads.is_stickied,
|
threads.id, threads.title, threads.slug, threads.created_at, threads.is_locked, threads.is_stickied,
|
||||||
users.username AS started_by,
|
users.username AS started_by,
|
||||||
u.username AS latest_post_username,
|
users.display_name AS started_by_display_name,
|
||||||
|
u.username AS latest_post_username,
|
||||||
|
u.display_name AS latest_post_display_name,
|
||||||
ph.content AS latest_post_content,
|
ph.content AS latest_post_content,
|
||||||
posts.created_at AS latest_post_created_at,
|
posts.created_at AS latest_post_created_at,
|
||||||
posts.id AS latest_post_id
|
posts.id AS latest_post_id
|
||||||
@@ -230,7 +244,13 @@ class Threads(Model):
|
|||||||
class Posts(Model):
|
class Posts(Model):
|
||||||
FULL_POSTS_QUERY = """
|
FULL_POSTS_QUERY = """
|
||||||
SELECT
|
SELECT
|
||||||
posts.id, posts.created_at, post_history.content, post_history.edited_at, users.username, users.status, avatars.file_path AS avatar_path, posts.thread_id, users.id AS user_id, post_history.original_markup, users.signature_rendered, threads.slug AS thread_slug, threads.is_locked AS thread_is_locked, threads.title AS thread_title
|
posts.id, posts.created_at,
|
||||||
|
post_history.content, post_history.edited_at,
|
||||||
|
users.username, users.display_name, users.status,
|
||||||
|
avatars.file_path AS avatar_path, posts.thread_id,
|
||||||
|
users.id AS user_id, post_history.original_markup,
|
||||||
|
users.signature_rendered, threads.slug AS thread_slug,
|
||||||
|
threads.is_locked AS thread_is_locked, threads.title AS thread_title
|
||||||
FROM
|
FROM
|
||||||
posts
|
posts
|
||||||
JOIN
|
JOIN
|
||||||
@@ -410,3 +430,7 @@ class MOTD(Model):
|
|||||||
q = 'SELECT id FROM motd'
|
q = 'SELECT id FROM motd'
|
||||||
res = db.query(q)
|
res = db.query(q)
|
||||||
return [MOTD.find({'id': i['id']}) for i in res]
|
return [MOTD.find({'id': i['id']}) for i in res]
|
||||||
|
|
||||||
|
|
||||||
|
class Mentions(Model):
|
||||||
|
table = 'mentions'
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ def babycode_preview():
|
|||||||
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
|
||||||
banned_tags = request.json.get('banned_tags', [])
|
banned_tags = request.json.get('banned_tags', [])
|
||||||
rendered = babycode_to_html(markup, banned_tags)
|
rendered = babycode_to_html(markup, banned_tags).result
|
||||||
return {'html': rendered}
|
return {'html': rendered}
|
||||||
|
|
||||||
|
|
||||||
@@ -213,3 +213,16 @@ def bookmark_thread(thread_id):
|
|||||||
return {'error': 'bad request'}, 400
|
return {'error': 'bad request'}, 400
|
||||||
|
|
||||||
return {'status': 'ok'}, 200
|
return {'status': 'ok'}, 200
|
||||||
|
|
||||||
|
@bp.get('/current-user')
|
||||||
|
def get_current_user_info():
|
||||||
|
if not is_logged_in():
|
||||||
|
return {'user': null}
|
||||||
|
|
||||||
|
user = get_active_user()
|
||||||
|
return {
|
||||||
|
'user': {
|
||||||
|
'username': user.username,
|
||||||
|
'display_name': user.display_name,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ def motd_editor_form():
|
|||||||
data = {
|
data = {
|
||||||
'title': title,
|
'title': title,
|
||||||
'body_original_markup': orig_body,
|
'body_original_markup': orig_body,
|
||||||
'body_rendered': babycode_to_html(orig_body, banned_tags=MOTD_BANNED_TAGS),
|
'body_rendered': babycode_to_html(orig_body, banned_tags=MOTD_BANNED_TAGS).result,
|
||||||
'format_version': BABYCODE_VERSION,
|
'format_version': BABYCODE_VERSION,
|
||||||
'edited_at': int(time.time()),
|
'edited_at': int(time.time()),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ from .users import login_required, get_active_user
|
|||||||
from ..lib.babycode import babycode_to_html, BABYCODE_VERSION
|
from ..lib.babycode import babycode_to_html, BABYCODE_VERSION
|
||||||
from ..constants import InfoboxKind
|
from ..constants import InfoboxKind
|
||||||
from ..db import db
|
from ..db import db
|
||||||
from ..models import Posts, PostHistory, Threads, Topics
|
from ..models import Posts, PostHistory, Threads, Topics, Mentions
|
||||||
|
|
||||||
bp = Blueprint("posts", __name__, url_prefix = "/post")
|
bp = Blueprint("posts", __name__, url_prefix = "/post")
|
||||||
|
|
||||||
@@ -21,13 +21,22 @@ def create_post(thread_id, user_id, content, markup_language="babycode"):
|
|||||||
|
|
||||||
revision = PostHistory.create({
|
revision = PostHistory.create({
|
||||||
"post_id": post.id,
|
"post_id": post.id,
|
||||||
"content": parsed_content,
|
"content": parsed_content.result,
|
||||||
"is_initial_revision": True,
|
"is_initial_revision": True,
|
||||||
"original_markup": content,
|
"original_markup": content,
|
||||||
"markup_language": markup_language,
|
"markup_language": markup_language,
|
||||||
"format_version": BABYCODE_VERSION,
|
"format_version": BABYCODE_VERSION,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
for mention in parsed_content.mentions:
|
||||||
|
Mentions.create({
|
||||||
|
'revision_id': revision.id,
|
||||||
|
'mentioned_user_id': mention['mentioned_user_id'],
|
||||||
|
'original_mention_text': mention['mention_text'],
|
||||||
|
'start_index': mention['start'],
|
||||||
|
'end_index': mention['end'],
|
||||||
|
})
|
||||||
|
|
||||||
post.update({"current_revision_id": revision.id})
|
post.update({"current_revision_id": revision.id})
|
||||||
return post
|
return post
|
||||||
|
|
||||||
@@ -38,13 +47,22 @@ def update_post(post_id, new_content, markup_language='babycode'):
|
|||||||
post = Posts.find({'id': post_id})
|
post = Posts.find({'id': post_id})
|
||||||
new_revision = PostHistory.create({
|
new_revision = PostHistory.create({
|
||||||
'post_id': post.id,
|
'post_id': post.id,
|
||||||
'content': parsed_content,
|
'content': parsed_content.result,
|
||||||
'is_initial_revision': False,
|
'is_initial_revision': False,
|
||||||
'original_markup': new_content,
|
'original_markup': new_content,
|
||||||
'markup_language': markup_language,
|
'markup_language': markup_language,
|
||||||
'format_version': BABYCODE_VERSION,
|
'format_version': BABYCODE_VERSION,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
for mention in parsed_content.mentions:
|
||||||
|
Mentions.create({
|
||||||
|
'revision_id': new_revision.id,
|
||||||
|
'mentioned_user_id': mention['mentioned_user_id'],
|
||||||
|
'original_mention_text': mention['mention_text'],
|
||||||
|
'start_index': mention['start'],
|
||||||
|
'end_index': mention['end'],
|
||||||
|
})
|
||||||
|
|
||||||
post.update({'current_revision_id': new_revision.id})
|
post.update({'current_revision_id': new_revision.id})
|
||||||
|
|
||||||
|
|
||||||
@@ -53,7 +71,8 @@ def update_post(post_id, new_content, markup_language='babycode'):
|
|||||||
def delete(post_id):
|
def delete(post_id):
|
||||||
post = Posts.find({'id': post_id})
|
post = Posts.find({'id': post_id})
|
||||||
if not post:
|
if not post:
|
||||||
return redirect(url_for('topics.all_topics'))
|
abort(404)
|
||||||
|
return
|
||||||
|
|
||||||
thread = Threads.find({'id': post.thread_id})
|
thread = Threads.find({'id': post.thread_id})
|
||||||
user = get_active_user()
|
user = get_active_user()
|
||||||
@@ -85,13 +104,15 @@ def delete(post_id):
|
|||||||
def edit(post_id):
|
def edit(post_id):
|
||||||
post = Posts.find({'id': post_id})
|
post = Posts.find({'id': post_id})
|
||||||
if not post:
|
if not post:
|
||||||
return redirect(url_for('topics.all_topics'))
|
abort(404)
|
||||||
|
return
|
||||||
|
|
||||||
user = get_active_user()
|
user = get_active_user()
|
||||||
q = f"{Posts.FULL_POSTS_QUERY} WHERE posts.id = ?"
|
q = f"{Posts.FULL_POSTS_QUERY} WHERE posts.id = ?"
|
||||||
editing_post = db.fetch_one(q, post_id)
|
editing_post = db.fetch_one(q, post_id)
|
||||||
if not editing_post:
|
if not editing_post:
|
||||||
return redirect(url_for('topics.all_topics'))
|
abort(404)
|
||||||
|
return
|
||||||
if editing_post['user_id'] != user.id:
|
if editing_post['user_id'] != user.id:
|
||||||
return redirect(url_for('topics.all_topics'))
|
return redirect(url_for('topics.all_topics'))
|
||||||
|
|
||||||
@@ -118,7 +139,8 @@ def edit_form(post_id):
|
|||||||
user = get_active_user()
|
user = get_active_user()
|
||||||
post = Posts.find({'id': post_id})
|
post = Posts.find({'id': post_id})
|
||||||
if not post:
|
if not post:
|
||||||
return redirect(url_for('topics.all_topics'))
|
abort(404)
|
||||||
|
return
|
||||||
if post.user_id != user.id:
|
if post.user_id != user.id:
|
||||||
return redirect(url_for('topics.all_topics'))
|
return redirect(url_for('topics.all_topics'))
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
from flask import (
|
from flask import (
|
||||||
Blueprint, render_template, request, redirect, url_for, flash
|
Blueprint, render_template, request, redirect, url_for, flash,
|
||||||
|
abort,
|
||||||
)
|
)
|
||||||
from .users import login_required, mod_only, get_active_user, is_logged_in
|
from .users import login_required, mod_only, get_active_user, is_logged_in
|
||||||
from ..db import db
|
from ..db import db
|
||||||
@@ -32,7 +33,8 @@ def thread(slug):
|
|||||||
POSTS_PER_PAGE = 10
|
POSTS_PER_PAGE = 10
|
||||||
thread = Threads.find({"slug": slug})
|
thread = Threads.find({"slug": slug})
|
||||||
if not thread:
|
if not thread:
|
||||||
return redirect(url_for('topics.all_topics'))
|
abort(404)
|
||||||
|
return
|
||||||
|
|
||||||
post_count = Posts.count({"thread_id": thread.id})
|
post_count = Posts.count({"thread_id": thread.id})
|
||||||
page_count = max(math.ceil(post_count / POSTS_PER_PAGE), 1)
|
page_count = max(math.ceil(post_count / POSTS_PER_PAGE), 1)
|
||||||
@@ -48,7 +50,6 @@ def thread(slug):
|
|||||||
page = math.ceil((post_position) / POSTS_PER_PAGE)
|
page = math.ceil((post_position) / POSTS_PER_PAGE)
|
||||||
else:
|
else:
|
||||||
page = max(1, min(page_count, int(request.args.get("page", default = 1))))
|
page = max(1, min(page_count, int(request.args.get("page", default = 1))))
|
||||||
|
|
||||||
posts = thread.get_posts(POSTS_PER_PAGE, (page - 1) * POSTS_PER_PAGE)
|
posts = thread.get_posts(POSTS_PER_PAGE, (page - 1) * POSTS_PER_PAGE)
|
||||||
topic = Topics.find({"id": thread.topic_id})
|
topic = Topics.find({"id": thread.topic_id})
|
||||||
other_topics = Topics.select()
|
other_topics = Topics.select()
|
||||||
@@ -87,7 +88,8 @@ def thread(slug):
|
|||||||
def reply(slug):
|
def reply(slug):
|
||||||
thread = Threads.find({"slug": slug})
|
thread = Threads.find({"slug": slug})
|
||||||
if not thread:
|
if not thread:
|
||||||
return redirect(url_for('topics.all_topics'))
|
abort(404)
|
||||||
|
return
|
||||||
user = get_active_user()
|
user = get_active_user()
|
||||||
if user.is_guest():
|
if user.is_guest():
|
||||||
return redirect(url_for('.thread', slug=slug))
|
return redirect(url_for('.thread', slug=slug))
|
||||||
@@ -149,7 +151,8 @@ def lock(slug):
|
|||||||
user = get_active_user()
|
user = get_active_user()
|
||||||
thread = Threads.find({'slug': slug})
|
thread = Threads.find({'slug': slug})
|
||||||
if not thread:
|
if not thread:
|
||||||
return redirect(url_for('topics.all_topics'))
|
abort(404)
|
||||||
|
return
|
||||||
if not ((thread.user_id == user.id) or user.is_mod()):
|
if not ((thread.user_id == user.id) or user.is_mod()):
|
||||||
return redirect(url_for('.thread', slug=slug))
|
return redirect(url_for('.thread', slug=slug))
|
||||||
target_op = request.form.get('target_op')
|
target_op = request.form.get('target_op')
|
||||||
@@ -166,7 +169,8 @@ def sticky(slug):
|
|||||||
user = get_active_user()
|
user = get_active_user()
|
||||||
thread = Threads.find({'slug': slug})
|
thread = Threads.find({'slug': slug})
|
||||||
if not thread:
|
if not thread:
|
||||||
return redirect(url_for('topics.all_topics'))
|
abort(404)
|
||||||
|
return
|
||||||
if not ((thread.user_id == user.id) or user.is_mod()):
|
if not ((thread.user_id == user.id) or user.is_mod()):
|
||||||
return redirect(url_for('.thread', slug=slug))
|
return redirect(url_for('.thread', slug=slug))
|
||||||
target_op = request.form.get('target_op')
|
target_op = request.form.get('target_op')
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
from flask import (
|
from flask import (
|
||||||
Blueprint, render_template, request, redirect, url_for, flash, session
|
Blueprint, render_template, request, redirect, url_for, flash, session,
|
||||||
|
abort,
|
||||||
)
|
)
|
||||||
from .users import login_required, mod_only, get_active_user, is_logged_in
|
from .users import login_required, mod_only, get_active_user, is_logged_in
|
||||||
from ..models import Users, Topics, Threads, Subscriptions
|
from ..models import Users, Topics, Threads, Subscriptions
|
||||||
@@ -50,7 +51,8 @@ def topic(slug):
|
|||||||
"slug": slug
|
"slug": slug
|
||||||
})
|
})
|
||||||
if not target_topic:
|
if not target_topic:
|
||||||
return redirect(url_for('.all_topics'))
|
abort(404)
|
||||||
|
return
|
||||||
|
|
||||||
threads_count = Threads.count({
|
threads_count = Threads.count({
|
||||||
"topic_id": target_topic.id
|
"topic_id": target_topic.id
|
||||||
@@ -88,7 +90,8 @@ def topic(slug):
|
|||||||
def edit(slug):
|
def edit(slug):
|
||||||
topic = Topics.find({"slug": slug})
|
topic = Topics.find({"slug": slug})
|
||||||
if not topic:
|
if not topic:
|
||||||
return redirect(url_for('.all_topics'))
|
abort(404)
|
||||||
|
return
|
||||||
return render_template("topics/edit.html", topic=topic)
|
return render_template("topics/edit.html", topic=topic)
|
||||||
|
|
||||||
|
|
||||||
@@ -98,7 +101,8 @@ def edit(slug):
|
|||||||
def edit_post(slug):
|
def edit_post(slug):
|
||||||
topic = Topics.find({"slug": slug})
|
topic = Topics.find({"slug": slug})
|
||||||
if not topic:
|
if not topic:
|
||||||
return redirect(url_for('.all_topics'))
|
abort(404)
|
||||||
|
return
|
||||||
|
|
||||||
topic.update({
|
topic.update({
|
||||||
"name": request.form.get('name', default = topic.name).strip(),
|
"name": request.form.get('name', default = topic.name).strip(),
|
||||||
@@ -115,7 +119,8 @@ def edit_post(slug):
|
|||||||
def delete(slug):
|
def delete(slug):
|
||||||
topic = Topics.find({"slug": slug})
|
topic = Topics.find({"slug": slug})
|
||||||
if not topic:
|
if not topic:
|
||||||
return redirect(url_for('.all_topics'))
|
abort(404)
|
||||||
|
return
|
||||||
|
|
||||||
topic.delete()
|
topic.delete()
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,15 @@
|
|||||||
from flask import (
|
from flask import (
|
||||||
Blueprint, render_template, request, redirect, url_for, flash, session, current_app
|
Blueprint, render_template, request, redirect, url_for, flash, session, current_app, abort
|
||||||
)
|
)
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
from ..db import db
|
from ..db import db
|
||||||
from ..lib.babycode import babycode_to_html, BABYCODE_VERSION
|
from ..lib.babycode import babycode_to_html, BABYCODE_VERSION
|
||||||
from ..models import Users, Sessions, Subscriptions, Avatars, PasswordResetLinks, InviteKeys, BookmarkCollections, BookmarkedThreads
|
from ..models import (
|
||||||
|
Users, Sessions, Subscriptions,
|
||||||
|
Avatars, PasswordResetLinks, InviteKeys,
|
||||||
|
BookmarkCollections, BookmarkedThreads,
|
||||||
|
Mentions, PostHistory,
|
||||||
|
)
|
||||||
from ..constants import InfoboxKind, PermissionLevel
|
from ..constants import InfoboxKind, PermissionLevel
|
||||||
from ..auth import digest, verify
|
from ..auth import digest, verify
|
||||||
from wand.image import Image
|
from wand.image import Image
|
||||||
@@ -68,6 +73,7 @@ def create_session(user_id):
|
|||||||
"expires_at": int(time.time()) + 31 * 24 * 60 * 60,
|
"expires_at": int(time.time()) + 31 * 24 * 60 * 60,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
def extend_session(user_id):
|
def extend_session(user_id):
|
||||||
session_obj = Sessions.find({'key': session['pyrom_session_key']})
|
session_obj = Sessions.find({'key': session['pyrom_session_key']})
|
||||||
if not session_obj:
|
if not session_obj:
|
||||||
@@ -90,6 +96,15 @@ def validate_username(username):
|
|||||||
return bool(re.fullmatch(pattern, username))
|
return bool(re.fullmatch(pattern, username))
|
||||||
|
|
||||||
|
|
||||||
|
def validate_display_name(display_name):
|
||||||
|
if not display_name:
|
||||||
|
return True
|
||||||
|
|
||||||
|
pattern = r'^[\w!#$%^*\(\)\-_=+\[\]\{\}\|;:,.?\s]{3,50}$'
|
||||||
|
display_name = display_name.replace('@', '_')
|
||||||
|
return bool(re.fullmatch(pattern, display_name))
|
||||||
|
|
||||||
|
|
||||||
def redirect_if_logged_in(*args, **kwargs):
|
def redirect_if_logged_in(*args, **kwargs):
|
||||||
def decorator(view_func):
|
def decorator(view_func):
|
||||||
@wraps(view_func)
|
@wraps(view_func)
|
||||||
@@ -112,6 +127,19 @@ def redirect_if_logged_in(*args, **kwargs):
|
|||||||
return decorator
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
|
def redirect_to_own(view_func):
|
||||||
|
@wraps(view_func)
|
||||||
|
def wrapper(username, *args, **kwargs):
|
||||||
|
user = get_active_user()
|
||||||
|
if username.lower() != user.username:
|
||||||
|
view_args = dict(request.view_args)
|
||||||
|
view_args.pop('username', None)
|
||||||
|
new_args = {**view_args, 'username': user.username}
|
||||||
|
return redirect(url_for(request.endpoint, **new_args))
|
||||||
|
return view_func(username, *args, **kwargs)
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
def login_required(view_func):
|
def login_required(view_func):
|
||||||
@wraps(view_func)
|
@wraps(view_func)
|
||||||
def wrapper(*args, **kwargs):
|
def wrapper(*args, **kwargs):
|
||||||
@@ -174,6 +202,53 @@ def get_prefers_theme():
|
|||||||
|
|
||||||
return session['theme']
|
return session['theme']
|
||||||
|
|
||||||
|
|
||||||
|
def anonymize_user(user_id):
|
||||||
|
deleted_user = Users.find({'username': 'deleteduser'})
|
||||||
|
|
||||||
|
from ..models import Threads, Posts
|
||||||
|
from ..lib.babycode import sanitize
|
||||||
|
threads = Threads.findall({'user_id': user_id})
|
||||||
|
posts = Posts.findall({'user_id': user_id})
|
||||||
|
|
||||||
|
revs_q = """SELECT DISTINCT m.revision_id FROM mentions m
|
||||||
|
WHERE m.mentioned_user_id = ?"""
|
||||||
|
|
||||||
|
mentioned_revs = db.query(revs_q, int(user_id))
|
||||||
|
with db.transaction():
|
||||||
|
for thread in threads:
|
||||||
|
thread.update({'user_id': int(deleted_user.id)})
|
||||||
|
for post in posts:
|
||||||
|
post.update({'user_id': int(deleted_user.id)})
|
||||||
|
|
||||||
|
revs = {}
|
||||||
|
for rev in mentioned_revs:
|
||||||
|
ph = PostHistory.find({'id': int(rev['revision_id'])})
|
||||||
|
ms = Mentions.findall({
|
||||||
|
'mentioned_user_id': int(user_id),
|
||||||
|
'revision_id': int(rev['revision_id'])
|
||||||
|
})
|
||||||
|
data = {
|
||||||
|
'text': sanitize(ph.original_markup),
|
||||||
|
'mentions': ms,
|
||||||
|
}
|
||||||
|
data['mentions'] = sorted(data['mentions'], key=lambda x: int(x.end_index), reverse=True)
|
||||||
|
revs[rev['revision_id']] = data
|
||||||
|
|
||||||
|
for rev_id, data in revs.items():
|
||||||
|
text = data['text']
|
||||||
|
for mention in data['mentions']:
|
||||||
|
text = text[:mention.start_index] + '@deleteduser' + text[mention.end_index:]
|
||||||
|
mention.delete()
|
||||||
|
|
||||||
|
res = babycode_to_html(text)
|
||||||
|
ph = PostHistory.find({'id': int(rev_id)})
|
||||||
|
ph.update({
|
||||||
|
'original_markup': text.unescape(),
|
||||||
|
'content': res.result,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
@bp.get("/log_in")
|
@bp.get("/log_in")
|
||||||
@redirect_if_logged_in(".page", username = lambda: get_active_user().username)
|
@redirect_if_logged_in(".page", username = lambda: get_active_user().username)
|
||||||
def log_in():
|
def log_in():
|
||||||
@@ -184,7 +259,7 @@ def log_in():
|
|||||||
@redirect_if_logged_in(".page", username = lambda: get_active_user().username)
|
@redirect_if_logged_in(".page", username = lambda: get_active_user().username)
|
||||||
def log_in_post():
|
def log_in_post():
|
||||||
target_user = Users.find({
|
target_user = Users.find({
|
||||||
"username": request.form['username']
|
"username": request.form['username'].lower()
|
||||||
})
|
})
|
||||||
if not target_user:
|
if not target_user:
|
||||||
flash("Incorrect username or password.", InfoboxKind.ERROR)
|
flash("Incorrect username or password.", InfoboxKind.ERROR)
|
||||||
@@ -239,7 +314,7 @@ def sign_up_post():
|
|||||||
flash("Invalid username.", InfoboxKind.ERROR)
|
flash("Invalid username.", InfoboxKind.ERROR)
|
||||||
return redirect(url_for("users.sign_up", key=key))
|
return redirect(url_for("users.sign_up", key=key))
|
||||||
|
|
||||||
user_exists = Users.count({"username": username}) > 0
|
user_exists = Users.count({"username": username.lower()}) > 0
|
||||||
if user_exists:
|
if user_exists:
|
||||||
flash(f"Username '{username}' is already taken.", InfoboxKind.ERROR)
|
flash(f"Username '{username}' is already taken.", InfoboxKind.ERROR)
|
||||||
return redirect(url_for("users.sign_up", key=key))
|
return redirect(url_for("users.sign_up", key=key))
|
||||||
@@ -254,8 +329,14 @@ def sign_up_post():
|
|||||||
|
|
||||||
hashed = digest(password)
|
hashed = digest(password)
|
||||||
|
|
||||||
|
if username.lower() != username:
|
||||||
|
display_name = username
|
||||||
|
else:
|
||||||
|
display_name = ''
|
||||||
|
|
||||||
new_user = Users.create({
|
new_user = Users.create({
|
||||||
"username": username,
|
"username": username,
|
||||||
|
'display_name': display_name,
|
||||||
"password_hash": hashed,
|
"password_hash": hashed,
|
||||||
"permission": PermissionLevel.GUEST.value,
|
"permission": PermissionLevel.GUEST.value,
|
||||||
})
|
})
|
||||||
@@ -279,22 +360,22 @@ def sign_up_post():
|
|||||||
|
|
||||||
@bp.get("/<username>")
|
@bp.get("/<username>")
|
||||||
def page(username):
|
def page(username):
|
||||||
target_user = Users.find({"username": username})
|
target_user = Users.find({"username": username.lower()})
|
||||||
|
if not target_user:
|
||||||
|
abort(404)
|
||||||
return render_template("users/user.html", target_user = target_user)
|
return render_template("users/user.html", target_user = target_user)
|
||||||
|
|
||||||
|
|
||||||
@bp.get("/<username>/settings")
|
@bp.get("/<username>/settings")
|
||||||
@login_required
|
@login_required
|
||||||
|
@redirect_to_own
|
||||||
def settings(username):
|
def settings(username):
|
||||||
target_user = Users.find({'username': username})
|
|
||||||
if target_user.id != get_active_user().id:
|
|
||||||
return redirect('.settings', username = get_active_user().username)
|
|
||||||
|
|
||||||
return render_template('users/settings.html')
|
return render_template('users/settings.html')
|
||||||
|
|
||||||
|
|
||||||
@bp.post('/<username>/settings')
|
@bp.post('/<username>/settings')
|
||||||
@login_required
|
@login_required
|
||||||
|
@redirect_to_own
|
||||||
def settings_form(username):
|
def settings_form(username):
|
||||||
# we silently ignore the passed username
|
# we silently ignore the passed username
|
||||||
# and grab the correct user from the session
|
# and grab the correct user from the session
|
||||||
@@ -311,10 +392,16 @@ def settings_form(username):
|
|||||||
status = request.form.get('status', default="")[:100]
|
status = request.form.get('status', default="")[:100]
|
||||||
original_sig = request.form.get('signature', default='').strip()
|
original_sig = request.form.get('signature', default='').strip()
|
||||||
if original_sig:
|
if original_sig:
|
||||||
rendered_sig = babycode_to_html(original_sig)
|
rendered_sig = babycode_to_html(original_sig).result
|
||||||
else:
|
else:
|
||||||
rendered_sig = ''
|
rendered_sig = ''
|
||||||
session['subscribe_by_default'] = request.form.get('subscribe_by_default', default='off') == 'on'
|
session['subscribe_by_default'] = request.form.get('subscribe_by_default', default='off') == 'on'
|
||||||
|
display_name = request.form.get('display_name', default='')
|
||||||
|
if not validate_display_name(display_name):
|
||||||
|
flash('Invalid display name.', InfoboxKind.ERROR)
|
||||||
|
return redirect('.settings', username=user.username)
|
||||||
|
|
||||||
|
old_dn = user.display_name
|
||||||
|
|
||||||
user.update({
|
user.update({
|
||||||
'status': status,
|
'status': status,
|
||||||
@@ -322,13 +409,29 @@ def settings_form(username):
|
|||||||
'signature_rendered': rendered_sig,
|
'signature_rendered': rendered_sig,
|
||||||
'signature_format_version': BABYCODE_VERSION,
|
'signature_format_version': BABYCODE_VERSION,
|
||||||
'signature_markup_language': 'babycode',
|
'signature_markup_language': 'babycode',
|
||||||
|
'display_name': display_name,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if old_dn != display_name:
|
||||||
|
# re-parse mentions
|
||||||
|
q = """SELECT DISTINCT m.revision_id FROM mentions m
|
||||||
|
JOIN post_history ph ON m.revision_id = ph.id
|
||||||
|
JOIN posts p ON p.current_revision_id = ph.id
|
||||||
|
WHERE m.mentioned_user_id = ?"""
|
||||||
|
mentions = db.query(q, int(user.id))
|
||||||
|
with db.transaction():
|
||||||
|
for mention in mentions:
|
||||||
|
rev = PostHistory.find({'id': int(mention['revision_id'])})
|
||||||
|
parsed_content = babycode_to_html(rev.original_markup).result
|
||||||
|
rev.update({'content': parsed_content})
|
||||||
|
|
||||||
flash('Settings updated.', InfoboxKind.INFO)
|
flash('Settings updated.', InfoboxKind.INFO)
|
||||||
return redirect(url_for('.settings', username=user.username))
|
return redirect(url_for('.settings', username=user.username))
|
||||||
|
|
||||||
|
|
||||||
@bp.post('/<username>/set_avatar')
|
@bp.post('/<username>/set_avatar')
|
||||||
@login_required
|
@login_required
|
||||||
|
@redirect_to_own
|
||||||
def set_avatar(username):
|
def set_avatar(username):
|
||||||
user = get_active_user()
|
user = get_active_user()
|
||||||
if user.is_guest():
|
if user.is_guest():
|
||||||
@@ -372,6 +475,7 @@ def set_avatar(username):
|
|||||||
|
|
||||||
@bp.post('/<username>/change_password')
|
@bp.post('/<username>/change_password')
|
||||||
@login_required
|
@login_required
|
||||||
|
@redirect_to_own
|
||||||
def change_password(username):
|
def change_password(username):
|
||||||
user = get_active_user()
|
user = get_active_user()
|
||||||
password = request.form.get('new_password')
|
password = request.form.get('new_password')
|
||||||
@@ -394,6 +498,7 @@ def change_password(username):
|
|||||||
|
|
||||||
@bp.post('/<username>/clear_avatar')
|
@bp.post('/<username>/clear_avatar')
|
||||||
@login_required
|
@login_required
|
||||||
|
@redirect_to_own
|
||||||
def clear_avatar(username):
|
def clear_avatar(username):
|
||||||
user = get_active_user()
|
user = get_active_user()
|
||||||
if user.is_default_avatar():
|
if user.is_default_avatar():
|
||||||
@@ -486,11 +591,9 @@ def guest_user(user_id):
|
|||||||
|
|
||||||
@bp.get("/<username>/inbox")
|
@bp.get("/<username>/inbox")
|
||||||
@login_required
|
@login_required
|
||||||
|
@redirect_to_own
|
||||||
def inbox(username):
|
def inbox(username):
|
||||||
user = get_active_user()
|
user = get_active_user()
|
||||||
if username != user.username:
|
|
||||||
return redirect(url_for(".inbox", username = user.username))
|
|
||||||
|
|
||||||
new_posts = []
|
new_posts = []
|
||||||
subscription = Subscriptions.find({"user_id": user.id})
|
subscription = Subscriptions.find({"user_id": user.id})
|
||||||
all_subscriptions = None
|
all_subscriptions = None
|
||||||
@@ -628,16 +731,14 @@ def reset_link_login_form(key):
|
|||||||
|
|
||||||
@bp.get('/<username>/invite-links/')
|
@bp.get('/<username>/invite-links/')
|
||||||
@login_required
|
@login_required
|
||||||
|
@redirect_to_own
|
||||||
def invite_links(username):
|
def invite_links(username):
|
||||||
target_user = Users.find({
|
target_user = Users.find({
|
||||||
'username': username
|
'username': username.lower()
|
||||||
})
|
})
|
||||||
if not target_user or not target_user.can_invite():
|
if not target_user or not target_user.can_invite():
|
||||||
return redirect(url_for('.page', username=username))
|
return redirect(url_for('.page', username=username))
|
||||||
|
|
||||||
if target_user.username != get_active_user().username:
|
|
||||||
return redirect(url_for('.invite_links', username=target_user.username))
|
|
||||||
|
|
||||||
invites = InviteKeys.findall({
|
invites = InviteKeys.findall({
|
||||||
'created_by': target_user.id
|
'created_by': target_user.id
|
||||||
})
|
})
|
||||||
@@ -647,15 +748,13 @@ def invite_links(username):
|
|||||||
|
|
||||||
@bp.post('/<username>/invite-links/create')
|
@bp.post('/<username>/invite-links/create')
|
||||||
@login_required
|
@login_required
|
||||||
|
@redirect_to_own
|
||||||
def create_invite_link(username):
|
def create_invite_link(username):
|
||||||
target_user = Users.find({
|
target_user = Users.find({
|
||||||
'username': username
|
'username': username.lower()
|
||||||
})
|
})
|
||||||
if not target_user or not target_user.can_invite():
|
if not target_user or not target_user.can_invite():
|
||||||
return redirect(url_for('.page', username=username))
|
return redirect(url_for('.page', username=username.lower()))
|
||||||
|
|
||||||
if target_user.username != get_active_user().username:
|
|
||||||
return redirect(url_for('.invite_links', username=target_user.username))
|
|
||||||
|
|
||||||
invite = InviteKeys.create({
|
invite = InviteKeys.create({
|
||||||
'created_by': target_user.id,
|
'created_by': target_user.id,
|
||||||
@@ -667,15 +766,13 @@ def create_invite_link(username):
|
|||||||
|
|
||||||
@bp.post('/<username>/invite-links/revoke')
|
@bp.post('/<username>/invite-links/revoke')
|
||||||
@login_required
|
@login_required
|
||||||
|
@redirect_to_own
|
||||||
def revoke_invite_link(username):
|
def revoke_invite_link(username):
|
||||||
target_user = Users.find({
|
target_user = Users.find({
|
||||||
'username': username
|
'username': username.lower()
|
||||||
})
|
})
|
||||||
if not target_user or not target_user.can_invite():
|
if not target_user or not target_user.can_invite():
|
||||||
return redirect(url_for('.page', username=username))
|
return redirect(url_for('.page', username=username.lower()))
|
||||||
|
|
||||||
if target_user.username != get_active_user().username:
|
|
||||||
return redirect(url_for('.invite_links', username=target_user.username))
|
|
||||||
|
|
||||||
invite = InviteKeys.find({
|
invite = InviteKeys.find({
|
||||||
'key': request.form.get('key'),
|
'key': request.form.get('key'),
|
||||||
@@ -694,10 +791,9 @@ def revoke_invite_link(username):
|
|||||||
|
|
||||||
@bp.get('/<username>/bookmarks')
|
@bp.get('/<username>/bookmarks')
|
||||||
@login_required
|
@login_required
|
||||||
|
@redirect_to_own
|
||||||
def bookmarks(username):
|
def bookmarks(username):
|
||||||
target_user = Users.find({'username': username})
|
target_user = get_active_user()
|
||||||
if not target_user or target_user.username != get_active_user().username:
|
|
||||||
return redirect(url_for('.bookmarks', username=get_active_user().username))
|
|
||||||
|
|
||||||
collections = target_user.get_bookmark_collections()
|
collections = target_user.get_bookmark_collections()
|
||||||
|
|
||||||
@@ -706,10 +802,40 @@ def bookmarks(username):
|
|||||||
|
|
||||||
@bp.get('/<username>/bookmarks/collections')
|
@bp.get('/<username>/bookmarks/collections')
|
||||||
@login_required
|
@login_required
|
||||||
|
@redirect_to_own
|
||||||
def bookmark_collections(username):
|
def bookmark_collections(username):
|
||||||
target_user = Users.find({'username': username})
|
target_user = get_active_user()
|
||||||
if not target_user or target_user.username != get_active_user().username:
|
|
||||||
return redirect(url_for('.bookmark_collections', username=get_active_user().username))
|
|
||||||
|
|
||||||
collections = target_user.get_bookmark_collections()
|
collections = target_user.get_bookmark_collections()
|
||||||
return render_template('users/bookmark_collections.html', collections=collections)
|
return render_template('users/bookmark_collections.html', collections=collections)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.get('/<username>/delete-account')
|
||||||
|
@login_required
|
||||||
|
@redirect_to_own
|
||||||
|
def delete_page(username):
|
||||||
|
target_user = get_active_user()
|
||||||
|
|
||||||
|
return render_template('users/delete_page.html')
|
||||||
|
|
||||||
|
|
||||||
|
@bp.post('/<username>/delete-account')
|
||||||
|
@login_required
|
||||||
|
@redirect_to_own
|
||||||
|
def delete_page_confirm(username):
|
||||||
|
target_user = get_active_user()
|
||||||
|
|
||||||
|
password = request.form.get('password', default='')
|
||||||
|
|
||||||
|
if not verify(target_user.password_hash, password):
|
||||||
|
flash('Incorrect password.', InfoboxKind.ERROR)
|
||||||
|
return redirect(url_for('.delete_page', username=username))
|
||||||
|
|
||||||
|
anonymize_user(target_user.id)
|
||||||
|
sessions = Sessions.findall({'user_id': int(target_user.id)})
|
||||||
|
for session_obj in sessions:
|
||||||
|
session_obj.delete()
|
||||||
|
|
||||||
|
session.clear()
|
||||||
|
target_user.delete()
|
||||||
|
return redirect(url_for('topics.all_topics'))
|
||||||
|
|||||||
@@ -132,6 +132,15 @@ SCHEMA = [
|
|||||||
"user_id" REFERENCES users(id) ON DELETE CASCADE
|
"user_id" REFERENCES users(id) ON DELETE CASCADE
|
||||||
)""",
|
)""",
|
||||||
|
|
||||||
|
"""CREATE TABLE IF NOT EXISTS "mentions" (
|
||||||
|
"id" INTEGER NOT NULL PRIMARY KEY,
|
||||||
|
"revision_id" REFERENCES post_history(id) ON DELETE CASCADE,
|
||||||
|
"mentioned_user_id" REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
"start_index" INTEGER NOT NULL,
|
||||||
|
"end_index" INTEGER NOT NULL,
|
||||||
|
"original_mention_text" TEXT NOT NULL
|
||||||
|
)""",
|
||||||
|
|
||||||
# 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)",
|
||||||
@@ -155,6 +164,9 @@ SCHEMA = [
|
|||||||
|
|
||||||
"CREATE INDEX IF NOT EXISTS idx_bookmarked_threads_collection ON bookmarked_threads(collection_id)",
|
"CREATE INDEX IF NOT EXISTS idx_bookmarked_threads_collection ON bookmarked_threads(collection_id)",
|
||||||
"CREATE INDEX IF NOT EXISTS idx_bookmarked_threads_thread ON bookmarked_threads(thread_id)",
|
"CREATE INDEX IF NOT EXISTS idx_bookmarked_threads_thread ON bookmarked_threads(thread_id)",
|
||||||
|
|
||||||
|
"CREATE INDEX IF NOT EXISTS idx_mentioned_user ON mentions(mentioned_user_id)",
|
||||||
|
"CREATE INDEX IF NOT EXISTS idx_mention_revision_id ON mentions(revision_id)",
|
||||||
]
|
]
|
||||||
|
|
||||||
def create():
|
def create():
|
||||||
|
|||||||
@@ -128,7 +128,7 @@
|
|||||||
<code class="inline-code">[img=https://forum.poto.cafe/avatars/default.webp]the Python logo with a cowboy hat[/img]</code>
|
<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 }}
|
{{ '[img=/static/avatars/default.webp]the Python logo with a cowboy hat[/img]' | babycode | safe }}
|
||||||
</p>
|
</p>
|
||||||
<p>Text inside the tag becomes the alt text. The attribute is the image URL. The text inside the tag will become the image's alt text.</p>
|
<p>The attribute is the image URL. The text inside the tag will become the image's alt text.</p>
|
||||||
<p>Images will always break up a paragraph and will get scaled down to a maximum of 400px. However, consecutive image tags will try to stay in one line, wrapping if necessary. Break the paragraph if you wish to keep images on their own paragraph.</p>
|
<p>Images will always break up a paragraph and will get scaled down to a maximum of 400px. However, consecutive image tags will try to stay in one line, wrapping if necessary. Break the paragraph if you wish to keep images on their own paragraph.</p>
|
||||||
<p>Multiple images attached to a post can be clicked to open a dialog to view them.</p>
|
<p>Multiple images attached to a post can be clicked to open a dialog to view them.</p>
|
||||||
</section>
|
</section>
|
||||||
@@ -157,13 +157,22 @@
|
|||||||
{{ list | babycode | safe }}
|
{{ list | babycode | safe }}
|
||||||
</section>
|
</section>
|
||||||
<section class="babycode-guide-section">
|
<section class="babycode-guide-section">
|
||||||
<h2 id="spoilers">Spoilers</h2>
|
<h2 id="spoilers">Spoilers</h2>
|
||||||
{% set spoiler = "[spoiler=Major Metal Gear Spoilers]Snake dies[/spoiler]" %}
|
{% set spoiler = "[spoiler=Major Metal Gear Spoilers]Snake dies[/spoiler]" %}
|
||||||
<p>You can make a section collapsible by using the <code class="inline-code">[spoiler]</code> tag:</p>
|
<p>You can make a section collapsible by using the <code class="inline-code">[spoiler]</code> tag:</p>
|
||||||
{{ ("[code]\n%s[/code]" % spoiler) | babycode | safe }}
|
{{ ("[code]\n%s[/code]" % spoiler) | babycode | safe }}
|
||||||
Will produce:
|
Will produce:
|
||||||
{{ spoiler | babycode | safe }}
|
{{ spoiler | babycode | safe }}
|
||||||
All other tags are supported inside spoilers.
|
All other tags are supported inside spoilers.
|
||||||
|
</section>
|
||||||
|
<section class="babycode-guide-section">
|
||||||
|
<h2 id="mentions">Mentioning users</h2>
|
||||||
|
<p>You can mention users by their username (<em>not</em> their display name) by using <code class="inline-code">@username</code>. A user's username is always shown below their avatar and display name on their posts and their user page.</p>
|
||||||
|
<p>A mention will show up on your post as a clickable box with the user's display name if they have one set or their username with an <code class="inline-code">@</code> symbol if they don't:</p>
|
||||||
|
<a class="mention" href="#mentions" title="@user-without-display-name">@user-without-display-name</a>
|
||||||
|
<a class="mention display" href="#mentions" title="@user-with-display-name">User with display name</a>
|
||||||
|
<a class="mention display me" href="#mentions" title="@your-username">Your display name</a>
|
||||||
|
<p>Mentioning a user does not notify them. It is simply a way to link to their profile in your posts.</p>
|
||||||
</section>
|
</section>
|
||||||
{% endset %}
|
{% endset %}
|
||||||
{{ sections | safe }}
|
{{ sections | safe }}
|
||||||
|
|||||||
@@ -28,5 +28,4 @@
|
|||||||
</footer>
|
</footer>
|
||||||
</bitty-6-0>
|
</bitty-6-0>
|
||||||
<script src="{{ "/static/js/ui.js" | cachebust }}"></script>
|
<script src="{{ "/static/js/ui.js" | cachebust }}"></script>
|
||||||
<script src="{{ "/static/js/date-fmt.js" | cachebust }}"></script>
|
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
8
app/templates/common/404.html
Normal file
8
app/templates/common/404.html
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
{% block title %}not found{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
<div class="darkbg settings-container">
|
||||||
|
<h1 class="thread-title">404 Not Found</h1>
|
||||||
|
<p>The requested URL does not exist.</p>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@@ -75,7 +75,7 @@
|
|||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|
||||||
{% macro timestamp(unix_ts) -%}
|
{% macro timestamp(unix_ts) -%}
|
||||||
<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 }}" data-init="convertTimestamps">{{ 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="", banned_tags=[]) %}
|
{% macro babycode_editor_component(ta_name, ta_placeholder="Post body", optional=False, prefill="", banned_tags=[]) %}
|
||||||
@@ -105,7 +105,7 @@
|
|||||||
<div>
|
<div>
|
||||||
<ul class="horizontal">
|
<ul class="horizontal">
|
||||||
{% for tag in banned_tags | unique %}
|
{% for tag in banned_tags | unique %}
|
||||||
<li><code class="inline-code">[{{ tag }}]</code></li>
|
<li><code class="inline-code">{{ tag }}</code></li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
@@ -121,7 +121,7 @@
|
|||||||
|
|
||||||
{% macro babycode_editor_form(ta_name, prefill = "", cancel_url="", endpoint="") %}
|
{% macro babycode_editor_form(ta_name, prefill = "", cancel_url="", endpoint="") %}
|
||||||
{% set save_button_text = "Post reply" if not cancel_url else "Save" %}
|
{% set save_button_text = "Post reply" if not cancel_url else "Save" %}
|
||||||
<form class="post-edit-form" method="post" action={{ endpoint }}>
|
<form class="post-edit-form" method="post" {%- if endpoint %}action={{ endpoint }}{% endif %}>
|
||||||
{{babycode_editor_component(ta_name, prefill = prefill)}}
|
{{babycode_editor_component(ta_name, prefill = prefill)}}
|
||||||
{% if not cancel_url %}
|
{% if not cancel_url %}
|
||||||
<span>
|
<span>
|
||||||
@@ -156,7 +156,8 @@
|
|||||||
<a href="{{ url_for("users.page", username=post['username']) }}" style="display: contents;">
|
<a href="{{ url_for("users.page", username=post['username']) }}" style="display: contents;">
|
||||||
<img src="{{ post['avatar_path'] }}" class="avatar">
|
<img src="{{ post['avatar_path'] }}" class="avatar">
|
||||||
</a>
|
</a>
|
||||||
<a href="{{ url_for("users.page", username=post['username']) }}" class="username-link">{{ post['username'] }}</a>
|
<a href="{{ url_for("users.page", username=post['username']) }}" class="username-link">{{ post['display_name'] or post['username'] }}</a>
|
||||||
|
<em><abbr title="Mention">@{{ post.username }}</abbr></em>
|
||||||
{% if post['status'] %}
|
{% if post['status'] %}
|
||||||
<em class="user-status">{{ post['status'] }}</em>
|
<em class="user-status">{{ post['status'] }}</em>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -205,7 +206,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if show_reply %}
|
{% if show_reply %}
|
||||||
{% set qtext = "[url=%s]%s said:[/url]" | format(post_permalink, post['username']) %}
|
{% set qtext = "@%s [url=%s]said:[/url]" | format(post['username'], post_permalink) %}
|
||||||
{% set reply_text = "%s\n[quote]\n%s\n[/quote]\n" | format(qtext, post['original_markup']) %}
|
{% set reply_text = "%s\n[quote]\n%s\n[/quote]\n" | format(qtext, post['original_markup']) %}
|
||||||
<button data-send="addQuote" value="{{ reply_text }}" class="reply-button">Quote</button>
|
<button data-send="addQuote" value="{{ reply_text }}" class="reply-button">Quote</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% else %}
|
{% else %}
|
||||||
{% with user = get_active_user() %}
|
{% with user = get_active_user() %}
|
||||||
Welcome, <a href="{{ url_for("users.page", username = user.username) }}">{{user.username}}</a>
|
Welcome, <a href="{{ url_for("users.page", username = user.username) }}">{{user.get_readable_name()}}</a>
|
||||||
<ul class="horizontal">
|
<ul class="horizontal">
|
||||||
<li><a href="{{ url_for("users.settings", username = user.username) }}">Settings</a></li>
|
<li><a href="{{ url_for("users.settings", username = user.username) }}">Settings</a></li>
|
||||||
<li><a href="{{ url_for("users.inbox", username = user.username) }}">Inbox</a></li>
|
<li><a href="{{ url_for("users.inbox", username = user.username) }}">Inbox</a></li>
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
<label for="title">Title</label>
|
<label for="title">Title</label>
|
||||||
<input name="title" id="title" type="text" required autocomplete="off" placeholder="Required" value="{{ current.title }}"><br>
|
<input name="title" id="title" type="text" required autocomplete="off" placeholder="Required" value="{{ current.title }}"><br>
|
||||||
<label for="body">Body</label>
|
<label for="body">Body</label>
|
||||||
{{ babycode_editor_component('body', ta_placeholder='MOTD body (required)', banned_tags=['img', 'spoiler'], prefill=current.body_original_markup) }}
|
{{ babycode_editor_component('body', ta_placeholder='MOTD body (required)', banned_tags=MOTD_BANNED_TAGS, prefill=current.body_original_markup) }}
|
||||||
<input type="submit" value="Save">
|
<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 ""}}>
|
<input class="critical" type="submit" formaction="{{ url_for('mod.motd_delete') }}" value="Delete MOTD" formnovalidate {{"disabled" if not current else ""}}>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -56,12 +56,12 @@
|
|||||||
</span>
|
</span>
|
||||||
•
|
•
|
||||||
<span>
|
<span>
|
||||||
Started by <a href="{{ url_for("users.page", username=thread['started_by']) }}">{{ thread['started_by'] }}</a> on {{ timestamp(thread['created_at']) }}
|
Started by <a href="{{ url_for("users.page", username=thread['started_by']) }}">{{ thread['started_by_display_name'] or thread['started_by'] }}</a> on {{ timestamp(thread['created_at']) }}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
<span>
|
<span>
|
||||||
Latest post by <a href="{{ url_for("users.page", username=thread['latest_post_username']) }}">{{ thread['latest_post_username'] }}</a>
|
Latest post by <a href="{{ url_for("users.page", username=thread['latest_post_username']) }}">{{ thread['latest_post_display_name'] or thread['latest_post_username'] }}</a>
|
||||||
on <a href="{{ url_for("threads.thread", slug=thread['slug'], after=thread['latest_post_id']) }}">on {{ timestamp(thread['latest_post_created_at']) }}</a>:
|
on <a href="{{ url_for("threads.thread", slug=thread['slug'], after=thread['latest_post_id']) }}">on {{ timestamp(thread['latest_post_created_at']) }}</a>:
|
||||||
</span>
|
</span>
|
||||||
<span class="thread-info-post-preview">
|
<span class="thread-info-post-preview">
|
||||||
|
|||||||
@@ -26,12 +26,12 @@
|
|||||||
{{ topic['description'] }}
|
{{ topic['description'] }}
|
||||||
{% if topic['latest_thread_username'] %}
|
{% if topic['latest_thread_username'] %}
|
||||||
<span>
|
<span>
|
||||||
Latest thread: <a href="{{ url_for("threads.thread", slug=topic['latest_thread_slug'])}}">{{topic['latest_thread_title']}}</a> by <a href="{{url_for("users.page", username=topic['latest_thread_username'])}}">{{topic['latest_thread_username']}}</a> on {{ timestamp(topic['latest_thread_created_at']) }}
|
Latest thread: <a href="{{ url_for("threads.thread", slug=topic['latest_thread_slug'])}}">{{topic['latest_thread_title']}}</a> by <a href="{{url_for("users.page", username=topic['latest_thread_username'])}}">{{topic['latest_thread_display_name'] or topic['latest_thread_username']}}</a> on {{ timestamp(topic['latest_thread_created_at']) }}
|
||||||
</span>
|
</span>
|
||||||
{% if topic['id'] in active_threads %}
|
{% if topic['id'] in active_threads %}
|
||||||
{% with thread=active_threads[topic['id']] %}
|
{% with thread=active_threads[topic['id']] %}
|
||||||
<span>
|
<span>
|
||||||
Latest post in: <a href="{{ url_for("threads.thread", slug=thread['thread_slug'])}}">{{ thread['thread_title'] }}</a> by <a href="{{ url_for("users.page", username=thread['username'])}}">{{ thread['username'] }}</a> at <a href="{{ get_post_url(thread.post_id, _anchor=true) }}">{{ timestamp(thread['post_created_at']) }}</a>
|
Latest post in: <a href="{{ url_for("threads.thread", slug=thread['thread_slug'])}}">{{ thread['thread_title'] }}</a> by <a href="{{ url_for("users.page", username=thread['username'])}}">{{ thread['display_name'] or thread['username'] }}</a> at <a href="{{ get_post_url(thread.post_id, _anchor=true) }}">{{ timestamp(thread['post_created_at']) }}</a>
|
||||||
</span>
|
</span>
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
15
app/templates/users/delete_page.html
Normal file
15
app/templates/users/delete_page.html
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
{% block title %}delete confirmation{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
<div class="darkbg login-container">
|
||||||
|
<h1>Confirm account deletion</h1>
|
||||||
|
<p>Are you sure you want to delete your account on {{ config.SITE_NAME }}? <strong>This action is irreversible.</strong> Your posts and threads will remain accessible to preserve history but will be de-personalized, showing up as authored by a system user. Posts that @mention you will also mention the system user instead.</p>
|
||||||
|
<p>If you wish for any and all content relating to you to be removed, you will have to ask {{ config.SITE_NAME }}'s administrators separately.</p>
|
||||||
|
<p>If you are sure, please confirm your current password below.</p>
|
||||||
|
<form method="post">
|
||||||
|
<label for="password">Confirm password</label>
|
||||||
|
<input type="password" id="password" name="password" required autocomplete="current-password">
|
||||||
|
<input class="critical" type="submit" value="Delete account">
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@@ -26,6 +26,8 @@
|
|||||||
<option value='activity' {{ 'selected' if session['sort_by'] == 'activity' else '' }}>Latest activity</option>
|
<option value='activity' {{ 'selected' if session['sort_by'] == 'activity' else '' }}>Latest activity</option>
|
||||||
<option value='thread' {{ 'selected' if session['sort_by'] == 'thread' else '' }}>Thread creation date</option>
|
<option value='thread' {{ 'selected' if session['sort_by'] == 'thread' else '' }}>Thread creation date</option>
|
||||||
</select>
|
</select>
|
||||||
|
<label for='display_name'>Display name</label>
|
||||||
|
<input type='text' id='display_name' name='display_name' value='{{ active_user.display_name }}' pattern="(?:[\w!#$%^*\(\)\-_=+\[\]\{\}\|;:,.?\s]{3,50})?" title='3-50 characters, no @, no <>, no &' placeholder='Optional. Will be shown in place of username.' autocomplete='off'></input>
|
||||||
<label for='status'>Status</label>
|
<label for='status'>Status</label>
|
||||||
<input type='text' id='status' name='status' value='{{ active_user.status }}' maxlength=100 placeholder='Will be shown under your name. Max 100 characters.'>
|
<input type='text' id='status' name='status' value='{{ active_user.status }}' maxlength=100 placeholder='Will be shown under your name. Max 100 characters.'>
|
||||||
<label for='babycode-content'>Signature</label>
|
<label for='babycode-content'>Signature</label>
|
||||||
@@ -41,5 +43,8 @@
|
|||||||
<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 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">
|
<input class="warn" type="submit" value="Change password">
|
||||||
</form>
|
</form>
|
||||||
|
<div>
|
||||||
|
<a class="linkbutton critical" href="{{ url_for('users.delete_page', username=active_user.username) }}">Delete account</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<div class="darkbg login-container">
|
<div class="darkbg login-container">
|
||||||
<h1>Sign up</h1>
|
<h1>Sign up</h1>
|
||||||
{% if inviter %}
|
{% if inviter %}
|
||||||
<p>You have been invited by <a href="{{ url_for('users.page', username=inviter.username) }}">{{ inviter.username }}</a> to join {{ config.SITE_NAME }}. Create an identity below.</p>
|
<p>You have been invited by <a href="{{ url_for('users.page', username=inviter.username) }}">{{ inviter.get_readable_name() }}</a> to join {{ config.SITE_NAME }}. Create an identity below.</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<form method="post">
|
<form method="post">
|
||||||
{% if key %}
|
{% if key %}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
{% from 'common/macros.html' import timestamp %}
|
{% from 'common/macros.html' import timestamp %}
|
||||||
{% extends 'base.html' %}
|
{% extends 'base.html' %}
|
||||||
{% block title %}{{ target_user.username }}'s profile{% endblock %}
|
{% block title %}{{ target_user.get_readable_name() }}'s profile{% endblock %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="darkbg">
|
<div class="darkbg">
|
||||||
<h1 class="thread-title"><i>{{ target_user.username }}</i>'s profile</h1>
|
<h1 class="thread-title"><i>{{ target_user.get_readable_name() }}</i>'s profile</h1>
|
||||||
{% if active_user.id == target_user.id %}
|
{% if active_user.id == target_user.id %}
|
||||||
<div class="user-actions">
|
<div class="user-actions">
|
||||||
<a class="linkbutton" href="{{ url_for("users.settings", username = active_user.username) }}">Settings</a>
|
<a class="linkbutton" href="{{ url_for("users.settings", username = active_user.username) }}">Settings</a>
|
||||||
@@ -45,7 +45,8 @@
|
|||||||
<div class="user-page-usercard">
|
<div class="user-page-usercard">
|
||||||
<div class="usercard-inner">
|
<div class="usercard-inner">
|
||||||
<img class="avatar" src="{{ target_user.get_avatar_url() }}">
|
<img class="avatar" src="{{ target_user.get_avatar_url() }}">
|
||||||
<strong class="big">{{ target_user.username }}</strong>
|
<strong class="big">{{ target_user.get_readable_name() }}</strong>
|
||||||
|
<em><abbr title="Mention">@{{ target_user.username }}</abbr></em>
|
||||||
{% if target_user.status %}
|
{% if target_user.status %}
|
||||||
<em class="user-status">{{ target_user.status }}</em>
|
<em class="user-status">{{ target_user.status }}</em>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -65,7 +66,7 @@
|
|||||||
<li>Latest started thread: <a href="{{ url_for("threads.thread", slug = stats.latest_thread_slug) }}">{{ stats.latest_thread_title }}</a>
|
<li>Latest started thread: <a href="{{ url_for("threads.thread", slug = stats.latest_thread_slug) }}">{{ stats.latest_thread_title }}</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if stats.inviter_username %}
|
{% if stats.inviter_username %}
|
||||||
<li>Invited by <a href="{{ url_for('users.page', username=stats.inviter_username) }}">{{ stats.inviter_username }}</a></li>
|
<li>Invited by <a href="{{ url_for('users.page', username=stats.inviter_username) }}">{{ stats.inviter_display_name or stats.inviter_username }}</a></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
|
|||||||
@@ -814,13 +814,15 @@ p {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.login-container > * {
|
.login-container > * {
|
||||||
width: 40%;
|
width: 70%;
|
||||||
margin: auto;
|
margin: auto;
|
||||||
|
max-width: 1000px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-container > * {
|
.settings-container > * {
|
||||||
width: 40%;
|
width: 70%;
|
||||||
margin: auto;
|
margin: auto;
|
||||||
|
max-width: 1000px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.avatar-form {
|
.avatar-form {
|
||||||
@@ -1409,3 +1411,24 @@ footer {
|
|||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
font-size: larger;
|
font-size: larger;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
a.mention, a.mention:visited {
|
||||||
|
display: inline-block;
|
||||||
|
color: white;
|
||||||
|
background-color: rgb(135.1928346457, 145.0974015748, 123.0025984252);
|
||||||
|
padding: 5px;
|
||||||
|
border-radius: 4px;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
a.mention.display, a.mention:visited.display {
|
||||||
|
text-decoration: underline;
|
||||||
|
text-decoration-style: dashed;
|
||||||
|
}
|
||||||
|
a.mention.me, a.mention:visited.me {
|
||||||
|
background-color: rgb(123.0025984252, 145.0974015748, 143.9545669291);
|
||||||
|
border: 1px dashed;
|
||||||
|
}
|
||||||
|
a.mention:hover, a.mention:visited:hover {
|
||||||
|
background-color: rgb(229.84, 231.92, 227.28);
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
|||||||
@@ -814,13 +814,15 @@ p {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.login-container > * {
|
.login-container > * {
|
||||||
width: 40%;
|
width: 70%;
|
||||||
margin: auto;
|
margin: auto;
|
||||||
|
max-width: 1000px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-container > * {
|
.settings-container > * {
|
||||||
width: 40%;
|
width: 70%;
|
||||||
margin: auto;
|
margin: auto;
|
||||||
|
max-width: 1000px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.avatar-form {
|
.avatar-form {
|
||||||
@@ -1410,6 +1412,27 @@ footer {
|
|||||||
font-size: larger;
|
font-size: larger;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
a.mention, a.mention:visited {
|
||||||
|
display: inline-block;
|
||||||
|
color: #e6e6e6;
|
||||||
|
background-color: rgb(96.95, 81.55, 96.95);
|
||||||
|
padding: 5px;
|
||||||
|
border-radius: 8px;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
a.mention.display, a.mention:visited.display {
|
||||||
|
text-decoration: underline;
|
||||||
|
text-decoration-style: dashed;
|
||||||
|
}
|
||||||
|
a.mention.me, a.mention:visited.me {
|
||||||
|
background-color: rgb(96.95, 89.25, 81.55);
|
||||||
|
border: 1px dashed;
|
||||||
|
}
|
||||||
|
a.mention:hover, a.mention:visited:hover {
|
||||||
|
background-color: #ae6bae;
|
||||||
|
color: #e6e6e6;
|
||||||
|
}
|
||||||
|
|
||||||
#topnav {
|
#topnav {
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
border: 10px solid rgb(40, 40, 40);
|
border: 10px solid rgb(40, 40, 40);
|
||||||
|
|||||||
@@ -814,13 +814,15 @@ p {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.login-container > * {
|
.login-container > * {
|
||||||
width: 60%;
|
width: 70%;
|
||||||
margin: auto;
|
margin: auto;
|
||||||
|
max-width: 1000px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-container > * {
|
.settings-container > * {
|
||||||
width: 60%;
|
width: 70%;
|
||||||
margin: auto;
|
margin: auto;
|
||||||
|
max-width: 1000px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.avatar-form {
|
.avatar-form {
|
||||||
@@ -1410,6 +1412,27 @@ footer {
|
|||||||
font-size: larger;
|
font-size: larger;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
a.mention, a.mention:visited {
|
||||||
|
display: inline-block;
|
||||||
|
color: white;
|
||||||
|
background-color: rgb(155.8907865169, 93.2211235955, 76.5092134831);
|
||||||
|
padding: 3px;
|
||||||
|
border-radius: 16px;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
a.mention.display, a.mention:visited.display {
|
||||||
|
text-decoration: underline;
|
||||||
|
text-decoration-style: dashed;
|
||||||
|
}
|
||||||
|
a.mention.me, a.mention:visited.me {
|
||||||
|
background-color: rgb(99.4880898876, 155.8907865169, 76.5092134831);
|
||||||
|
border: 1px dashed;
|
||||||
|
}
|
||||||
|
a.mention:hover, a.mention:visited:hover {
|
||||||
|
background-color: rgb(231.56, 212.36, 207.24);
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
|
||||||
#topnav {
|
#topnav {
|
||||||
border-top-left-radius: 16px;
|
border-top-left-radius: 16px;
|
||||||
border-top-right-radius: 16px;
|
border-top-right-radius: 16px;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
const bookmarkMenuHrefTemplate = '/hyperapi/bookmarks-dropdown';
|
const bookmarkMenuHrefTemplate = '/hyperapi/bookmarks-dropdown';
|
||||||
const previewEndpoint = '/api/babycode-preview';
|
const previewEndpoint = '/api/babycode-preview';
|
||||||
|
const userEndpoint = '/api/current-user';
|
||||||
|
|
||||||
const delay = ms => {return new Promise(resolve => setTimeout(resolve, ms))}
|
const delay = ms => {return new Promise(resolve => setTimeout(resolve, ms))}
|
||||||
|
|
||||||
@@ -141,7 +142,7 @@ export default class {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#previousMarkup = '';
|
#previousMarkup = null;
|
||||||
async babycodePreview(ev, el) {
|
async babycodePreview(ev, el) {
|
||||||
if (ev.sender.classList.contains('active')) {
|
if (ev.sender.classList.contains('active')) {
|
||||||
return;
|
return;
|
||||||
@@ -151,11 +152,17 @@ export default class {
|
|||||||
const previewContainer = el.querySelector('#babycode-preview-container');
|
const previewContainer = el.querySelector('#babycode-preview-container');
|
||||||
const ta = document.getElementById('babycode-content');
|
const ta = document.getElementById('babycode-content');
|
||||||
const markup = ta.value.trim();
|
const markup = ta.value.trim();
|
||||||
if (markup === '' || markup === this.#previousMarkup) {
|
if (markup === '') {
|
||||||
previewErrorsContainer.textContent = 'Type something!';
|
previewErrorsContainer.textContent = 'Type something!';
|
||||||
previewContainer.textContent = '';
|
previewContainer.textContent = '';
|
||||||
|
this.#previousMarkup = '';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (markup === this.#previousMarkup) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const bannedTags = JSON.parse(document.getElementById('babycode-banned-tags').value);
|
const bannedTags = JSON.parse(document.getElementById('babycode-banned-tags').value);
|
||||||
this.#previousMarkup = markup;
|
this.#previousMarkup = markup;
|
||||||
|
|
||||||
@@ -246,4 +253,27 @@ export default class {
|
|||||||
el.scrollIntoView();
|
el.scrollIntoView();
|
||||||
el.focus();
|
el.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
convertTimestamps(ev, el) {
|
||||||
|
const timestamp = el.getInt('utc');
|
||||||
|
if (!isNaN(timestamp)) {
|
||||||
|
const date = new Date(timestamp * 1000);
|
||||||
|
el.textContent = date.toLocaleString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#currentUsername = undefined;
|
||||||
|
async highlightMentions(ev, el) {
|
||||||
|
if (this.#currentUsername === undefined) {
|
||||||
|
const userInfo = await this.api.getJSON(userEndpoint);
|
||||||
|
if (!userInfo.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.#currentUsername = userInfo.value.user.username;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (el.getString('username') === this.#currentUsername) {
|
||||||
|
el.classList.add('me');
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
document.addEventListener("DOMContentLoaded", () => {
|
|
||||||
const timestampSpans = document.getElementsByClassName("timestamp");
|
|
||||||
for (let timestampSpan of timestampSpans) {
|
|
||||||
const timestamp = parseInt(timestampSpan.dataset.utc);
|
|
||||||
if (!isNaN(timestamp)) {
|
|
||||||
const date = new Date(timestamp * 1000);
|
|
||||||
timestampSpan.textContent = date.toLocaleString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
@@ -82,7 +82,6 @@
|
|||||||
quoteButton.textContent = "Quote fragment"
|
quoteButton.textContent = "Quote fragment"
|
||||||
quoteButton.className = "reduced"
|
quoteButton.className = "reduced"
|
||||||
quotePopover.appendChild(quoteButton);
|
quotePopover.appendChild(quoteButton);
|
||||||
|
|
||||||
document.body.appendChild(quotePopover);
|
document.body.appendChild(quotePopover);
|
||||||
return quoteButton;
|
return quoteButton;
|
||||||
}
|
}
|
||||||
@@ -98,7 +97,7 @@
|
|||||||
if (ta.value.trim() !== "") {
|
if (ta.value.trim() !== "") {
|
||||||
ta.value += "\n"
|
ta.value += "\n"
|
||||||
}
|
}
|
||||||
ta.value += `[url=${postPermalink}]${authorUsername} said:[/url]\n[quote]< :scissors: > ${document.getSelection().toString()} < :scissors: >[/quote]\n`;
|
ta.value += `@${authorUsername} [url=${postPermalink}]said:[/url]\n[quote]< :scissors: > ${document.getSelection().toString()} < :scissors: >[/quote]\n`;
|
||||||
ta.scrollIntoView()
|
ta.scrollIntoView()
|
||||||
ta.focus();
|
ta.focus();
|
||||||
|
|
||||||
|
|||||||
@@ -51,7 +51,8 @@ $BIGGER_PADDING: 30px !default;
|
|||||||
|
|
||||||
$PAGE_SIDE_MARGIN: 100px !default;
|
$PAGE_SIDE_MARGIN: 100px !default;
|
||||||
|
|
||||||
$SETTINGS_WIDTH: 40% !default;
|
$SETTINGS_WIDTH: 70% !default;
|
||||||
|
$SETTINGS_MAX_WIDTH: 1000px !default;
|
||||||
|
|
||||||
// **************
|
// **************
|
||||||
// BORDERS
|
// BORDERS
|
||||||
@@ -654,15 +655,19 @@ $pagebutton_min_width: $BIG_PADDING !default;
|
|||||||
}
|
}
|
||||||
|
|
||||||
$login_container_width: $SETTINGS_WIDTH !default;
|
$login_container_width: $SETTINGS_WIDTH !default;
|
||||||
|
$login_container_max_width: $SETTINGS_MAX_WIDTH !default;
|
||||||
.login-container > * {
|
.login-container > * {
|
||||||
width: $login_container_width;
|
width: $login_container_width;
|
||||||
margin: auto;
|
margin: auto;
|
||||||
|
max-width: $login_container_max_width;
|
||||||
}
|
}
|
||||||
|
|
||||||
$settings_container_width: $SETTINGS_WIDTH !default;
|
$settings_container_width: $SETTINGS_WIDTH !default;
|
||||||
|
$settings_container_max_width: $SETTINGS_MAX_WIDTH !default;
|
||||||
.settings-container > * {
|
.settings-container > * {
|
||||||
width: $settings_container_width;
|
width: $settings_container_width;
|
||||||
margin: auto;
|
margin: auto;
|
||||||
|
max-width: $settings_container_max_width
|
||||||
}
|
}
|
||||||
|
|
||||||
$avatar_form_padding: $BIG_PADDING $ZERO_PADDING !default;
|
$avatar_form_padding: $BIG_PADDING $ZERO_PADDING !default;
|
||||||
@@ -1369,3 +1374,36 @@ $motd_content_padding_right: 25% !default;
|
|||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
font-size: larger;
|
font-size: larger;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$mention_font_color: $DEFAULT_FONT_COLOR_INVERSE !default;
|
||||||
|
$mention_font_color_hover: $DEFAULT_FONT_COLOR !default;
|
||||||
|
$mention_background_color: $DARK_2 !default;
|
||||||
|
$mention_background_color_me: color.adjust($DARK_2, $hue: 90) !default;
|
||||||
|
$mention_background_color_hover: $LIGHT_2 !default;
|
||||||
|
$mention_border_me: 1px dashed;
|
||||||
|
$mention_padding: $SMALL_PADDING !default;
|
||||||
|
$mention_border_radius: $DEFAULT_BORDER_RADIUS !default;
|
||||||
|
a.mention, a.mention:visited {
|
||||||
|
display: inline-block;
|
||||||
|
color: $mention_font_color;
|
||||||
|
background-color: $mention_background_color;
|
||||||
|
padding: $mention_padding;
|
||||||
|
border-radius: $mention_border_radius;
|
||||||
|
text-decoration: none;
|
||||||
|
|
||||||
|
&.display {
|
||||||
|
text-decoration: underline;
|
||||||
|
text-decoration-style: dashed;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.me {
|
||||||
|
background-color: $mention_background_color_me;
|
||||||
|
border: $mention_border_me;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: $mention_background_color_hover;
|
||||||
|
color: $mention_font_color_hover;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|||||||
@@ -80,6 +80,8 @@ $br: 8px;
|
|||||||
$tab_button_active_color: #8a5584,
|
$tab_button_active_color: #8a5584,
|
||||||
|
|
||||||
$bookmarks_dropdown_background_color: $lightish_accent,
|
$bookmarks_dropdown_background_color: $lightish_accent,
|
||||||
|
|
||||||
|
$mention_font_color: $fc,
|
||||||
);
|
);
|
||||||
|
|
||||||
#topnav {
|
#topnav {
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ $br: 16px;
|
|||||||
$thread_locked_border: 1px solid black,
|
$thread_locked_border: 1px solid black,
|
||||||
$motd_border: 1px solid black,
|
$motd_border: 1px solid black,
|
||||||
|
|
||||||
$SETTINGS_WIDTH: 60%,
|
|
||||||
$PAGE_SIDE_MARGIN: 50px,
|
$PAGE_SIDE_MARGIN: 50px,
|
||||||
|
|
||||||
$link_color: black,
|
$link_color: black,
|
||||||
|
|||||||
Reference in New Issue
Block a user