mirror of
https://codeberg.org/catask-org/catask.git
synced 2025-04-20 13:53:42 -05:00
rendering of custom emojis, etc
This commit is contained in:
parent
b24e3ced2d
commit
ee40d1b73f
1 changed files with 248 additions and 7 deletions
255
functions.py
255
functions.py
|
@ -1,13 +1,14 @@
|
||||||
from flask import url_for, request
|
from flask import url_for, request, jsonify
|
||||||
from markupsafe import Markup
|
from markupsafe import Markup
|
||||||
from bleach.sanitizer import Cleaner
|
from bleach.sanitizer import Cleaner
|
||||||
from datetime import datetime
|
from datetime import datetime, timezone
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from mistune import HTMLRenderer, escape
|
from mistune import HTMLRenderer, escape
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
import mistune
|
import mistune
|
||||||
import humanize
|
import humanize
|
||||||
import mysql.connector
|
import mysql.connector
|
||||||
|
import re
|
||||||
import os
|
import os
|
||||||
import random
|
import random
|
||||||
import json
|
import json
|
||||||
|
@ -40,6 +41,31 @@ def formatRelativeTime(date_str):
|
||||||
|
|
||||||
return humanize.naturaltime(time_difference)
|
return humanize.naturaltime(time_difference)
|
||||||
|
|
||||||
|
def formatRelativeTime2(date_str):
|
||||||
|
date_format = "%Y-%m-%dT%H:%M:%SZ"
|
||||||
|
|
||||||
|
past_date = None
|
||||||
|
try:
|
||||||
|
if date_str:
|
||||||
|
past_date = datetime.strptime(date_str, date_format)
|
||||||
|
else:
|
||||||
|
pass
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if past_date is None:
|
||||||
|
return ''
|
||||||
|
# raise ValueError("Date string does not match any supported format.")
|
||||||
|
|
||||||
|
if past_date.tzinfo is None:
|
||||||
|
past_date = past_date.replace(tzinfo=timezone.utc)
|
||||||
|
|
||||||
|
now = datetime.now(timezone.utc)
|
||||||
|
|
||||||
|
time_difference = now - past_date
|
||||||
|
|
||||||
|
return humanize.naturaltime(time_difference)
|
||||||
|
|
||||||
dbHost = os.environ.get("DB_HOST")
|
dbHost = os.environ.get("DB_HOST")
|
||||||
dbUser = os.environ.get("DB_USER")
|
dbUser = os.environ.get("DB_USER")
|
||||||
dbPass = os.environ.get("DB_PASS")
|
dbPass = os.environ.get("DB_PASS")
|
||||||
|
@ -95,6 +121,10 @@ def readPlainFile(file, split=False):
|
||||||
else:
|
else:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
def savePlainFile(file, contents):
|
||||||
|
with open(file, 'w') as file:
|
||||||
|
file.write(contents)
|
||||||
|
|
||||||
def getRandomWord():
|
def getRandomWord():
|
||||||
items = readPlainFile(const.antiSpamFile, split=True)
|
items = readPlainFile(const.antiSpamFile, split=True)
|
||||||
return random.choice(items)
|
return random.choice(items)
|
||||||
|
@ -117,18 +147,203 @@ def parse_inline_button(inline, m, state):
|
||||||
return m.end()
|
return m.end()
|
||||||
|
|
||||||
def render_inline_button(renderer, text):
|
def render_inline_button(renderer, text):
|
||||||
return f"<button class='btn btn-outline-secondary'>{text}</button>"
|
return f"<button class='btn btn-secondary' type='button'>{text}</button>"
|
||||||
|
|
||||||
|
|
||||||
def button(md):
|
def button(md):
|
||||||
md.inline.register('inline_button', inlineBtnPattern, parse_inline_button, before='link')
|
md.inline.register('inline_button', inlineBtnPattern, parse_inline_button, before='link')
|
||||||
if md.renderer and md.renderer.NAME == 'html':
|
if md.renderer and md.renderer.NAME == 'html':
|
||||||
md.renderer.register('inline_button', render_inline_button)
|
md.renderer.register('inline_button', render_inline_button)
|
||||||
|
|
||||||
|
# Base directory where emoji packs are stored
|
||||||
|
EMOJI_BASE_PATH = Path.cwd() / 'static' / 'emojis'
|
||||||
|
|
||||||
|
emoji_cache = {}
|
||||||
|
|
||||||
|
def find_emoji_path(emoji_name):
|
||||||
|
head, sep, tail = emoji_name.partition('_')
|
||||||
|
if any(Path(EMOJI_BASE_PATH).glob(f'{head}.json')):
|
||||||
|
for json_file in Path(EMOJI_BASE_PATH).glob('*.json'):
|
||||||
|
print("\n[CatAsk/functions/find_emoji_path] Using JSON meta file\n")
|
||||||
|
pack_data = loadJSON(json_file)
|
||||||
|
emojis = pack_data.get('emojis', [])
|
||||||
|
|
||||||
|
for emoji in emojis:
|
||||||
|
if emoji['name'] == emoji_name:
|
||||||
|
rel_dir = json_file.stem
|
||||||
|
emoji_path = os.path.join('static/emojis', rel_dir, emoji['file_name'])
|
||||||
|
emoji_cache[emoji_name] = emoji_path
|
||||||
|
return emoji_path
|
||||||
|
|
||||||
|
else:
|
||||||
|
for address, dirs, files in os.walk(EMOJI_BASE_PATH):
|
||||||
|
print("\n[CatAsk/functions/find_emoji_path] Falling back to scanning directories\n")
|
||||||
|
if f"{emoji_name}.png" in files:
|
||||||
|
rel_dir = os.path.relpath(address, EMOJI_BASE_PATH)
|
||||||
|
emoji_path = os.path.join("static/emojis", rel_dir, f"{emoji_name}.png")
|
||||||
|
emoji_cache[emoji_name] = emoji_path
|
||||||
|
return emoji_path
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
emojiPattern = r':(?P<emoji_name>[a-zA-Z0-9_]+):'
|
||||||
|
|
||||||
|
def parse_emoji(inline, m, state):
|
||||||
|
emoji_name = m.group("emoji_name")
|
||||||
|
state.append_token({"type": "emoji", "raw": emoji_name})
|
||||||
|
return m.end()
|
||||||
|
|
||||||
|
def render_emoji(renderer, emoji_name):
|
||||||
|
emoji_path = find_emoji_path(emoji_name)
|
||||||
|
|
||||||
|
if emoji_path:
|
||||||
|
absolute_emoji_path = url_for('static', filename=emoji_path.replace('static/', ''))
|
||||||
|
return f"<img src='{absolute_emoji_path}' alt=':{emoji_name}:' title=':{emoji_name}:' class='emoji' loading='lazy' width='28' height='28' />"
|
||||||
|
|
||||||
|
return f":{emoji_name}:"
|
||||||
|
|
||||||
|
def emoji(md):
|
||||||
|
md.inline.register('emoji', emojiPattern, parse_emoji, before='link')
|
||||||
|
|
||||||
|
if md.renderer and md.renderer.NAME == 'html':
|
||||||
|
md.renderer.register('emoji', render_emoji)
|
||||||
|
|
||||||
|
def listEmojis():
|
||||||
|
emojis = []
|
||||||
|
emoji_base_path = Path.cwd() / 'static' / 'emojis'
|
||||||
|
|
||||||
|
# Iterate over files that are directly in the emoji base path (not in subdirectories)
|
||||||
|
for file in emoji_base_path.iterdir():
|
||||||
|
# Only include files, not directories
|
||||||
|
if file.is_file() and file.suffix in {'.png', '.jpg', '.jpeg', '.webp'}:
|
||||||
|
# Get the relative path and name for the emoji
|
||||||
|
relative_path = os.path.relpath(file, emoji_base_path)
|
||||||
|
emojis.append({
|
||||||
|
'name': file.stem, # Get the file name without the extension
|
||||||
|
'image': os.path.join('static/emojis', relative_path), # Full relative path for image
|
||||||
|
'relative_path': relative_path
|
||||||
|
})
|
||||||
|
|
||||||
|
return emojis
|
||||||
|
|
||||||
|
"""
|
||||||
|
def listEmojiPacks():
|
||||||
|
emoji_packs = []
|
||||||
|
emoji_base_path = Path.cwd() / 'static' / 'emojis'
|
||||||
|
|
||||||
|
for pack_dir in emoji_base_path.iterdir():
|
||||||
|
if pack_dir.is_dir():
|
||||||
|
relative_path = os.path.relpath(pack_dir, emoji_base_path)
|
||||||
|
json_file_path = const.emojiPath / f"{pack_dir.name}.json"
|
||||||
|
if json_file_path.exists():
|
||||||
|
pack_data = loadJSON(json_file_path)
|
||||||
|
pack_info = {
|
||||||
|
'name': pack_data['name'],
|
||||||
|
'exportedAt': pack_data['exportedAt'],
|
||||||
|
# 'website': pack_data['website'],
|
||||||
|
'emojis': pack_data['emojis']
|
||||||
|
}
|
||||||
|
print(f"pack info: {pack_info}")
|
||||||
|
print(f"pack name: {pack_info['name']}")
|
||||||
|
return pack_info
|
||||||
|
else:
|
||||||
|
preview_image = None
|
||||||
|
for file in pack_dir.iterdir():
|
||||||
|
if file.suffix in {'.png', '.jpg', '.jpeg', '.webp'}:
|
||||||
|
preview_image = os.path.join('static/emojis', relative_path, file.name)
|
||||||
|
break
|
||||||
|
|
||||||
|
emoji_packs.append({
|
||||||
|
'name': pack_dir.name,
|
||||||
|
'preview_image': preview_image,
|
||||||
|
'relative_path': f'static/emojis/{relative_path}'
|
||||||
|
})
|
||||||
|
|
||||||
|
return emoji_packs
|
||||||
|
"""
|
||||||
|
|
||||||
|
def listEmojiPacks():
|
||||||
|
emoji_packs = []
|
||||||
|
emoji_base_path = const.emojiPath
|
||||||
|
|
||||||
|
# Iterate through all directories in the emoji base path
|
||||||
|
for pack_dir in emoji_base_path.iterdir():
|
||||||
|
if pack_dir.is_dir():
|
||||||
|
relative_path = os.path.relpath(pack_dir, emoji_base_path)
|
||||||
|
|
||||||
|
# Check if a meta.json file exists in the directory
|
||||||
|
meta_json_path = const.emojiPath / f"{pack_dir}.json"
|
||||||
|
if meta_json_path.exists():
|
||||||
|
print(f"[CatAsk/functions/listEmojiPacks] Using meta.json file ({meta_json_path})")
|
||||||
|
# Load data from the meta.json file
|
||||||
|
pack_data = loadJSON(meta_json_path)
|
||||||
|
|
||||||
|
emoji_packs.append({
|
||||||
|
'name': pack_data.get('name', pack_dir.name).capitalize(),
|
||||||
|
'exportedAt': pack_data.get('exportedAt', 'Unknown'),
|
||||||
|
'preview_image': pack_data.get('preview_image', ''),
|
||||||
|
'website': pack_data.get('website', ''),
|
||||||
|
'relative_path': f'static/emojis/{relative_path}',
|
||||||
|
'emojis': pack_data.get('emojis', [])
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
print(f"[CatAsk/functions/listEmojiPacks] Falling back to directory scan ({pack_dir})")
|
||||||
|
# If no meta.json is found, fall back to directory scan
|
||||||
|
preview_image = None
|
||||||
|
# Find the first image in the directory for preview
|
||||||
|
for file in pack_dir.iterdir():
|
||||||
|
if file.suffix in {'.png', '.jpg', '.jpeg', '.webp'}:
|
||||||
|
preview_image = os.path.join('static/emojis', relative_path, file.name)
|
||||||
|
break
|
||||||
|
|
||||||
|
# Append pack info without meta.json
|
||||||
|
emoji_packs.append({
|
||||||
|
'name': pack_dir.name.capitalize(),
|
||||||
|
'preview_image': preview_image,
|
||||||
|
'relative_path': f'static/emojis/{relative_path}'
|
||||||
|
})
|
||||||
|
|
||||||
|
return emoji_packs
|
||||||
|
|
||||||
|
|
||||||
|
def processEmojis(meta_json_path):
|
||||||
|
emoji_metadata = loadJSON(meta_json_path)
|
||||||
|
emojis = emoji_metadata.get('emojis', [])
|
||||||
|
pack_name = emoji_metadata['emojis'][0]['emoji']['category'].capitalize()
|
||||||
|
exported_at = emoji_metadata.get('exportedAt', 'Unknown')
|
||||||
|
website = emoji_metadata.get('host', '')
|
||||||
|
preview_image = os.path.join('static/emojis', pack_name.lower(), emoji_metadata['emojis'][0]['fileName'])
|
||||||
|
relative_path = os.path.join('static/emojis', pack_name.lower())
|
||||||
|
|
||||||
|
processed_emojis = []
|
||||||
|
for emoji in emojis:
|
||||||
|
emoji_info = {
|
||||||
|
'name': emoji['emoji']['name'],
|
||||||
|
'file_name': emoji['fileName'],
|
||||||
|
}
|
||||||
|
processed_emojis.append(emoji_info)
|
||||||
|
print(f"[CatAsk/API/upload_emoji_pack] Processed emoji: {emoji_info['name']}\t(File: {emoji_info['file_name']})")
|
||||||
|
|
||||||
|
# Create the pack info structure
|
||||||
|
pack_info = {
|
||||||
|
'name': pack_name,
|
||||||
|
'exportedAt': exported_at,
|
||||||
|
'preview_image': preview_image,
|
||||||
|
'relative_path': relative_path,
|
||||||
|
'website': website,
|
||||||
|
'emojis': processed_emojis
|
||||||
|
}
|
||||||
|
|
||||||
|
# Save the combined pack info to <pack_name>.json
|
||||||
|
pack_json_name = const.emojiPath / f"{pack_name.lower()}.json"
|
||||||
|
saveJSON(pack_info, pack_json_name)
|
||||||
|
|
||||||
|
return processed_emojis
|
||||||
|
|
||||||
def renderMarkdown(text):
|
def renderMarkdown(text):
|
||||||
plugins = [
|
plugins = [
|
||||||
'strikethrough',
|
'strikethrough',
|
||||||
button
|
button,
|
||||||
|
emoji
|
||||||
]
|
]
|
||||||
allowed_tags = [
|
allowed_tags = [
|
||||||
'p',
|
'p',
|
||||||
|
@ -143,11 +358,13 @@ def renderMarkdown(text):
|
||||||
'button',
|
'button',
|
||||||
'ol',
|
'ol',
|
||||||
'li',
|
'li',
|
||||||
'hr'
|
'hr',
|
||||||
|
'img'
|
||||||
]
|
]
|
||||||
allowed_attrs = {
|
allowed_attrs = {
|
||||||
'a': 'href',
|
'a': 'href',
|
||||||
'button': 'class'
|
'button': 'class',
|
||||||
|
'img': ['src', 'width', 'height', 'alt', 'class', 'loading', 'title']
|
||||||
}
|
}
|
||||||
# hard_wrap=True means that newlines will be
|
# hard_wrap=True means that newlines will be
|
||||||
# converted into <br> tags
|
# converted into <br> tags
|
||||||
|
@ -187,10 +404,21 @@ def generateMetadata(question=None, answer=None):
|
||||||
return metadata
|
return metadata
|
||||||
|
|
||||||
allowedFileExtensions = {'png', 'jpg', 'jpeg', 'webp', 'bmp', 'jxl'}
|
allowedFileExtensions = {'png', 'jpg', 'jpeg', 'webp', 'bmp', 'jxl'}
|
||||||
|
allowedArchiveExtensions = {'zip', 'tar', 'gz', 'bz2', 'xz'}
|
||||||
|
|
||||||
def allowedFile(filename):
|
def allowedFile(filename):
|
||||||
return '.' in filename and filename.rsplit('.', 1)[1].lower() in allowedFileExtensions
|
return '.' in filename and filename.rsplit('.', 1)[1].lower() in allowedFileExtensions
|
||||||
|
|
||||||
|
def allowedArchive(filename):
|
||||||
|
return '.' in filename and filename.rsplit('.', 1)[1].lower() in allowedArchiveExtensions
|
||||||
|
|
||||||
|
def stripArchExtension(filename):
|
||||||
|
if filename.endswith(('.tar.gz', '.tar.bz2', '.tar.xz')):
|
||||||
|
filename = filename.rsplit('.', 2)[0]
|
||||||
|
else:
|
||||||
|
filename = filename.rsplit('.', 1)[0]
|
||||||
|
return filename
|
||||||
|
|
||||||
def generateFavicon(file_name):
|
def generateFavicon(file_name):
|
||||||
sizes = {
|
sizes = {
|
||||||
'apple-touch-icon.png': (180, 180),
|
'apple-touch-icon.png': (180, 180),
|
||||||
|
@ -210,3 +438,16 @@ def generateFavicon(file_name):
|
||||||
resized_img = img.resize(size)
|
resized_img = img.resize(size)
|
||||||
resized_img_absolute_path = const.faviconDir / filename
|
resized_img_absolute_path = const.faviconDir / filename
|
||||||
resized_img.save(resized_img_absolute_path)
|
resized_img.save(resized_img_absolute_path)
|
||||||
|
|
||||||
|
# reserved for 1.7.0 or later
|
||||||
|
"""
|
||||||
|
def getUserIp():
|
||||||
|
if request.environ.get('HTTP_X_FORWARDED_FOR') is None:
|
||||||
|
return request.environ['REMOTE_ADDR']
|
||||||
|
else:
|
||||||
|
return request.environ['HTTP_X_FORWARDED_FOR']
|
||||||
|
|
||||||
|
def isIpBlacklisted(user_ip):
|
||||||
|
blacklist = readPlainFile(const.ipBlacklistFile, split=True)
|
||||||
|
return user_ip in blacklist
|
||||||
|
"""
|
||||||
|
|
Loading…
Add table
Reference in a new issue