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): trim = int(trim) if 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.+?)\[/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"" 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', 'ol', 'li', 'hr' ] allowed_attrs = { 'a': 'href', 'button': 'class' } # hard_wrap=True means that newlines will be # converted into
tags # # yes, markdown usually lets you make line breaks only # with 2 spaces or
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=True, plugins=plugins, hard_wrap=True ) html = md(text) cleaner = Cleaner(tags=allowed_tags, attributes=allowed_attrs) clean_html = cleaner.clean(html) return Markup(clean_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)