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"
"
+
+ 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
+"""