mirror of
https://codeberg.org/catask-org/catask.git
synced 2025-04-20 13:53:42 -05:00
207 lines
5.7 KiB
Python
207 lines
5.7 KiB
Python
from flask import url_for, request
|
|
from markupsafe import Markup
|
|
from bleach.sanitizer import Cleaner
|
|
from datetime import datetime
|
|
from pathlib import Path
|
|
from mistune import HTMLRenderer, escape
|
|
from PIL import Image
|
|
import mistune
|
|
import humanize
|
|
import mysql.connector
|
|
import os
|
|
import random
|
|
import json
|
|
import constants as const
|
|
|
|
# load json file
|
|
def loadJSON(file_path):
|
|
# open the file
|
|
path = Path.cwd() / file_path
|
|
with open(path, 'r', encoding="utf-8") as file:
|
|
# return loaded file
|
|
return json.load(file)
|
|
|
|
# save json file
|
|
def saveJSON(dict, file_path):
|
|
# open the file
|
|
path = Path.cwd() / file_path
|
|
with open(path, 'w', encoding="utf-8") as file:
|
|
# dump the contents
|
|
json.dump(dict, file, indent=4)
|
|
|
|
cfg = loadJSON(const.configFile)
|
|
|
|
def formatRelativeTime(date_str):
|
|
date_format = "%Y-%m-%d %H:%M:%S"
|
|
past_date = datetime.strptime(date_str, date_format)
|
|
|
|
now = datetime.now()
|
|
time_difference = now - past_date
|
|
|
|
return humanize.naturaltime(time_difference)
|
|
|
|
dbHost = os.environ.get("DB_HOST")
|
|
dbUser = os.environ.get("DB_USER")
|
|
dbPass = os.environ.get("DB_PASS")
|
|
dbName = os.environ.get("DB_NAME")
|
|
dbPort = os.environ.get("DB_PORT")
|
|
if not dbPort:
|
|
dbPort = 3306
|
|
|
|
def createDatabase(cursor, dbName):
|
|
try:
|
|
cursor.execute("CREATE DATABASE {} DEFAULT CHARACTER SET 'utf8'".format(dbName))
|
|
print(f"Database {dbName} created successfully")
|
|
except mysql.connector.Error as error:
|
|
print("Failed to create database:", error)
|
|
exit(1)
|
|
|
|
def connectToDb():
|
|
conn = mysql.connector.connect(
|
|
host=dbHost,
|
|
user=dbUser,
|
|
password=dbPass,
|
|
database=dbName,
|
|
port=dbPort,
|
|
autocommit=True
|
|
)
|
|
return conn
|
|
|
|
def getQuestion(question_id: int):
|
|
conn = connectToDb()
|
|
cursor = conn.cursor(dictionary=True)
|
|
cursor.execute("SELECT * FROM questions WHERE id=%s", (question_id,))
|
|
question = cursor.fetchone()
|
|
cursor.close()
|
|
conn.close()
|
|
return question
|
|
|
|
def getAnswer(question_id: int):
|
|
conn = connectToDb()
|
|
cursor = conn.cursor(dictionary=True)
|
|
cursor.execute("SELECT * FROM answers WHERE question_id=%s", (question_id,))
|
|
answer = cursor.fetchone()
|
|
cursor.close()
|
|
conn.close()
|
|
return answer
|
|
|
|
def readPlainFile(file, split=False):
|
|
if os.path.exists(file):
|
|
with open(file, 'r', encoding="utf-8") as file:
|
|
if split == False:
|
|
return file.read()
|
|
if split == True:
|
|
return file.read().splitlines()
|
|
else:
|
|
return []
|
|
|
|
def getRandomWord():
|
|
items = readPlainFile(const.antiSpamFile, split=True)
|
|
return random.choice(items)
|
|
|
|
def trimContent(var, trim):
|
|
if int(trim) > 0:
|
|
trimmed = var[:trim] + '…' if len(var) >= trim else var
|
|
trimmed = trimmed.rstrip()
|
|
return trimmed
|
|
else:
|
|
return var
|
|
|
|
# mistune plugin
|
|
inlineBtnPattern = r'\[btn\](?P<button_text>.+?)\[/btn\]'
|
|
|
|
def parse_inline_button(inline, m, state):
|
|
text = m.group("button_text")
|
|
state.append_token({"type": "inline_button", "raw": text})
|
|
return m.end()
|
|
|
|
def render_inline_button(renderer, text):
|
|
return f"<button class='btn btn-outline-secondary'>{text}</button>"
|
|
|
|
|
|
def button(md):
|
|
md.inline.register('inline_button', inlineBtnPattern, parse_inline_button, before='link')
|
|
if md.renderer and md.renderer.NAME == 'html':
|
|
md.renderer.register('inline_button', render_inline_button)
|
|
|
|
def renderMarkdown(text):
|
|
plugins = [
|
|
'strikethrough',
|
|
button
|
|
]
|
|
allowed_tags = [
|
|
'p',
|
|
'em',
|
|
'b',
|
|
'strong',
|
|
'i',
|
|
'br',
|
|
's',
|
|
'del',
|
|
'a',
|
|
'button'
|
|
]
|
|
allowed_attrs = {
|
|
'a': 'href'
|
|
}
|
|
# hard_wrap=True means that newlines will be
|
|
# converted into <br> tags
|
|
#
|
|
# yes, markdown usually lets you make line breaks only
|
|
# with 2 spaces or <br> tag, but hard_wrap is enabled to keep
|
|
# sanity of whoever will use this software
|
|
# (after all, not everyone knows markdown syntax)
|
|
md = mistune.create_markdown(
|
|
escape=False,
|
|
plugins=plugins,
|
|
hard_wrap=True
|
|
)
|
|
html = md(text)
|
|
cleaner = Cleaner(tags=allowed_tags, attributes=allowed_attrs)
|
|
clean_html = cleaner.clean(html)
|
|
return Markup(html)
|
|
|
|
def generateMetadata(question=None, answer=None):
|
|
metadata = {
|
|
'title': cfg['instance']['title'],
|
|
'description': cfg['instance']['description'],
|
|
'url': cfg['instance']['fullBaseUrl'],
|
|
'image': cfg['instance']['image']
|
|
}
|
|
|
|
# if question is specified, generate metadata for that question
|
|
if question and answer:
|
|
metadata.update({
|
|
'title': trimContent(question['content'], 150) + " | " + cfg['instance']['title'],
|
|
'description': trimContent(answer['content'], 150),
|
|
'url': cfg['instance']['fullBaseUrl'] + url_for('viewQuestion', question_id=question['id']),
|
|
'image': cfg['instance']['image']
|
|
})
|
|
|
|
# return 'metadata' dictionary
|
|
return metadata
|
|
|
|
allowedFileExtensions = {'png', 'jpg', 'jpeg', 'webp', 'bmp', 'jxl'}
|
|
|
|
def allowedFile(filename):
|
|
return '.' in filename and filename.rsplit('.', 1)[1].lower() in allowedFileExtensions
|
|
|
|
def generateFavicon(file_name):
|
|
sizes = {
|
|
'apple-touch-icon.png': (180, 180),
|
|
'android-chrome-192x192.png': (192, 192),
|
|
'android-chrome-512x512.png': (512, 512),
|
|
'favicon-32x32.png': (32, 32),
|
|
'favicon-16x16.png': (16, 16),
|
|
'favicon.ico': (16, 16)
|
|
}
|
|
|
|
img = Image.open(const.faviconDir / file_name)
|
|
|
|
if not os.path.exists(const.faviconDir):
|
|
os.makedirs(const.faviconDir)
|
|
|
|
for filename, size in sizes.items():
|
|
resized_img = img.resize(size)
|
|
resized_img_absolute_path = const.faviconDir / filename
|
|
resized_img.save(resized_img_absolute_path)
|