diff --git a/functions.py b/functions.py index 2ca5bca..5b8b1e7 100644 --- a/functions.py +++ b/functions.py @@ -1,13 +1,14 @@ -from flask import url_for, request +from flask import url_for, request, jsonify from markupsafe import Markup from bleach.sanitizer import Cleaner -from datetime import datetime +from datetime import datetime, timezone from pathlib import Path from mistune import HTMLRenderer, escape from PIL import Image import mistune import humanize import mysql.connector +import re import os import random import json @@ -40,6 +41,31 @@ def formatRelativeTime(date_str): 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") dbUser = os.environ.get("DB_USER") dbPass = os.environ.get("DB_PASS") @@ -95,6 +121,10 @@ def readPlainFile(file, split=False): else: return [] +def savePlainFile(file, contents): + with open(file, 'w') as file: + file.write(contents) + def getRandomWord(): items = readPlainFile(const.antiSpamFile, split=True) return random.choice(items) @@ -117,18 +147,203 @@ def parse_inline_button(inline, m, state): return m.end() def render_inline_button(renderer, text): - return f"" - + 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) +# 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[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":{emoji_name}:" + + 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 .json + pack_json_name = const.emojiPath / f"{pack_name.lower()}.json" + saveJSON(pack_info, pack_json_name) + + return processed_emojis + def renderMarkdown(text): plugins = [ 'strikethrough', - button + button, + emoji ] allowed_tags = [ 'p', @@ -143,11 +358,13 @@ def renderMarkdown(text): 'button', 'ol', 'li', - 'hr' + 'hr', + 'img' ] allowed_attrs = { 'a': 'href', - 'button': 'class' + 'button': 'class', + 'img': ['src', 'width', 'height', 'alt', 'class', 'loading', 'title'] } # hard_wrap=True means that newlines will be # converted into
tags @@ -187,10 +404,21 @@ def generateMetadata(question=None, answer=None): return metadata allowedFileExtensions = {'png', 'jpg', 'jpeg', 'webp', 'bmp', 'jxl'} +allowedArchiveExtensions = {'zip', 'tar', 'gz', 'bz2', 'xz'} def allowedFile(filename): 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): sizes = { 'apple-touch-icon.png': (180, 180), @@ -210,3 +438,16 @@ def generateFavicon(file_name): resized_img = img.resize(size) resized_img_absolute_path = const.faviconDir / filename 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 +"""