add ntfy support

This commit is contained in:
mst 2024-12-16 16:45:41 +03:00
parent 91cd4b4c43
commit b2eac9654b
5 changed files with 142 additions and 12 deletions

26
app.py
View file

@ -5,6 +5,7 @@ from mysql.connector import errorcode
from functools import wraps from functools import wraps
from werkzeug.utils import secure_filename from werkzeug.utils import secure_filename
from pathlib import Path from pathlib import Path
import threading
import requests import requests
import secrets import secrets
import shutil import shutil
@ -232,6 +233,11 @@ def information():
def accessibility(): def accessibility():
return render_template('admin/categories/accessibility.html') return render_template('admin/categories/accessibility.html')
@admin_bp.route('/notifications/', methods=['GET', 'POST'])
@loginRequired
def notifications():
return render_template('admin/categories/notifications.html')
@admin_bp.route('/general/', methods=['GET', 'POST']) @admin_bp.route('/general/', methods=['GET', 'POST'])
@loginRequired @loginRequired
def general(): def general():
@ -343,10 +349,20 @@ def pwaManifest():
"background_color": "" "background_color": ""
}) })
# wip, scheduled for 1.8.0 release
# @api_bp.route('/hyper/widget/', methods=['GET'])
# def widget():
# func_val = func.getAllQuestions()
# combined = func_val[0]
# metadata = func_val[1]
# return render_template('widget.html', combined=combined, urllib=urllib, trimContent=func.trimContent, metadata=metadata, getRandomWord=func.getRandomWord, formatRelativeTime=func.formatRelativeTime)
# -- question routes -- # -- question routes --
@api_bp.route('/add_question/', methods=['POST']) @api_bp.route('/add_question/', methods=['POST'])
def addQuestion(): def addQuestion():
try:
app.logger.debug("[CatAsk/API/add_question] started question flow")
from_who = request.form.get('from_who', cfg['anonName']) from_who = request.form.get('from_who', cfg['anonName'])
question = request.form.get('question', '') question = request.form.get('question', '')
cw = request.form.get('cw', '') cw = request.form.get('cw', '')
@ -355,8 +371,14 @@ def addQuestion():
abort(400, "Question field must not be empty") abort(400, "Question field must not be empty")
if len(question) > int(cfg['charLimit']) or len(from_who) > int(cfg['charLimit']): if len(question) > int(cfg['charLimit']) or len(from_who) > int(cfg['charLimit']):
abort(400, "Question exceeds the character limit") abort(400, "Question exceeds the character limit")
return_val = func.addQuestion(from_who, question, cw)
return func.addQuestion(from_who, question, cw) app.logger.debug("[CatAsk/API/add_question] finished question flow")
return return_val[0]
finally:
if cfg['ntfy']['enabled']:
# cw, return_val, from_who, question
ntfy_thread = threading.Thread(target=func.ntfySend, name=f"ntfy thread for {return_val[2]}", args=(cw, return_val, from_who, question,))
ntfy_thread.start()
@api_bp.route('/delete_question/', methods=['DELETE']) @api_bp.route('/delete_question/', methods=['DELETE'])
@loginRequired @loginRequired

View file

@ -41,6 +41,13 @@
"apikey": "" "apikey": ""
} }
}, },
"ntfy": {
"enabled": false,
"host": "https://ntfy.sh",
"user": "",
"pass": "",
"topic": ""
},
"trimContentAfter": "150", "trimContentAfter": "150",
"charLimit": "512", "charLimit": "512",
"anonName": "Anonymous", "anonName": "Anonymous",

View file

@ -1,10 +1,11 @@
from flask import url_for, request, jsonify, Flask from flask import url_for, request, jsonify, Flask, abort
from markupsafe import Markup from markupsafe import Markup
from bleach.sanitizer import Cleaner from bleach.sanitizer import Cleaner
from datetime import datetime, timezone 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 base64
import time import time
import zipfile import zipfile
import shutil import shutil
@ -191,11 +192,12 @@ def addQuestion(from_who, question, cw, noAntispam=False):
app.logger.debug("[CatAsk/API/add_question] INSERT'ing new question into database") app.logger.debug("[CatAsk/API/add_question] INSERT'ing new question into database")
cursor.execute("INSERT INTO questions (from_who, content, answered, cw) VALUES (%s, %s, %s, %s)", (from_who, question, False, cw)) cursor.execute("INSERT INTO questions (from_who, content, answered, cw) VALUES (%s, %s, %s, %s) RETURNING id", (from_who, question, False, cw))
question_id = cursor.fetchone()[0]
cursor.close() cursor.close()
conn.close() conn.close()
return {'message': 'Question asked successfully!'}, 201 return {'message': 'Question asked successfully!'}, 201, question_id
def getAnswer(question_id: int): def getAnswer(question_id: int):
conn = connectToDb() conn = connectToDb()
@ -229,6 +231,41 @@ def addAnswer(question_id, answer, cw):
return jsonify({'message': 'Answer added successfully!'}), 201 return jsonify({'message': 'Answer added successfully!'}), 201
def ntfySend(cw, return_val, from_who, question):
app.logger.debug("[CatAsk/functions/ntfySend] started ntfy flow")
ntfy_cw = f" [CW: {cw}]" if cw else ""
ntfy_host = cfg['ntfy']['host']
ntfy_topic = cfg['ntfy']['topic']
question_id = return_val[2]
# doesn't work otherwise
from_who = from_who if from_who else cfg['anonName']
if cfg['ntfy']['user'] and cfg['ntfy']['pass']:
ntfy_user = cfg['ntfy']['user']
ntfy_pass = cfg['ntfy']['pass']
ascii_auth = f"{ntfy_user}:{ntfy_pass}".encode('ascii')
b64_auth = base64.b64encode(ascii_auth)
# there's probably a better way to do this without duplicated code
headers={
"Authorization": f"Basic {b64_auth.decode('ascii')}",
"Title": f"New question from {from_who}{ntfy_cw}",
"Actions": f"view, View question, {cfg['instance']['fullBaseUrl']}/inbox/#question-{question_id}",
"Tags": "question"
}
else:
headers={
"Title": f"New question from {from_who}{ntfy_cw}",
"Actions": f"view, View question, {cfg['instance']['fullBaseUrl']}/inbox/#question-{question_id}",
"Tags": "question"
}
r = requests.put(
f"{ntfy_host}/{ntfy_topic}".encode('utf-8'),
data=trimContent(question, int(cfg['trimContentAfter'])),
headers=headers
)
app.logger.debug("[CatAsk/functions/ntfySend] finished ntfy flow")
def readPlainFile(file, split=False): def readPlainFile(file, split=False):
if os.path.exists(file): if os.path.exists(file):
with open(file, 'r', encoding="utf-8") as file: with open(file, 'r', encoding="utf-8") as file:

View file

@ -45,6 +45,10 @@
<i class="bi bi-universal-access fs-5 scale-child"></i> <i class="bi bi-universal-access fs-5 scale-child"></i>
<span class="sidebar-btn-text ms-2 ps-1">Accessibility</span> <span class="sidebar-btn-text ms-2 ps-1">Accessibility</span>
</a> </a>
<a class="ps-3 btn btn-outline-secondary my-1 fs-6 scale-parent {{ notif_link }} d-flex align-items-center" href="{{ url_for('admin.notifications') }}">
<i class="bi bi-bell fs-5 scale-child"></i>
<span class="sidebar-btn-text ms-2 ps-1">Notifications</span>
</a>
<a class="ps-3 btn btn-outline-secondary my-1 fs-6 scale-parent {{ custom_link }} d-flex align-items-center" href="{{ url_for('admin.customize') }}"> <a class="ps-3 btn btn-outline-secondary my-1 fs-6 scale-parent {{ custom_link }} d-flex align-items-center" href="{{ url_for('admin.customize') }}">
<i class="bi bi-columns-gap fs-5 scale-child"></i> <i class="bi bi-columns-gap fs-5 scale-child"></i>
<span class="sidebar-btn-text ms-2 ps-1">Customize</span> <span class="sidebar-btn-text ms-2 ps-1">Customize</span>

View file

@ -0,0 +1,60 @@
{% extends 'admin/base.html' %}
{% block _title %}Notifications{% endblock %}
{% set notif_link = 'active' %}
{% block _content %}
<form hx-post="{{ url_for('api.updateConfig') }}" hx-target="#response-container" hx-swap="none" hx-disabled-elt="#saveConfig">
<h2 id="general" class="mb-22 fw-normal">Notifications</h2>
<p class="fs-5 h3 text-body-secondary mb-3">Configure notifications for new questions using <a href="https://ntfy.sh/" target="_blank">ntfy</a></p>
<div class="form-check form-switch mb-3">
<input
class="form-check-input"
type="checkbox"
name="_ntfy.enabled"
id="_ntfy.enabled"
value="{{ cfg.ntfy.enabled }}"
role="switch"
{% if cfg.ntfy.enabled %}checked{% endif %}>
<input type="hidden" id="ntfy.enabled" name="ntfy.enabled" value="{{ cfg.ntfy.enabled }}">
<label for="_ntfy.enabled" class="form-check-label">Enabled</label>
</div>
<p class="form-label">Server &amp; Topic</p>
<div class="input-group mb-4">
<input type="text" id="ntfy.host" name="ntfy.host" value="{{ cfg.ntfy.host }}" class="form-control" aria-label="Server">
<span class="input-group-text">/</span>
<input type="text" id="ntfy.topic" name="ntfy.topic" value="{{ cfg.ntfy.topic }}" class="form-control" aria-label="Topic">
</div>
<h3 class="fw-light mb-2">Credentials (optional)</h3>
<p class="text-body-secondary mb-3">Set credentials if the topic is protected</p>
<div class="form-group mb-3 mt-2">
<label class="form-label" for="ntfy.user">Username</label>
<input type="text" id="ntfy.user" name="ntfy.user" value="{{ cfg.ntfy.user }}" class="form-control">
<p class="form-text">
Topic user
</p>
</div>
<div class="form-group mb-3">
<label class="form-label" for="ntfy.pass">Password</label>
<input type="password" id="ntfy.pass" name="ntfy.pass" value="{{ cfg.ntfy.pass }}" class="form-control">
<p class="form-text">
Topic password
</p>
</div>
<div class="form-group">
<button type="submit" class="btn btn-primary mt-3" id="saveConfig">
<span class="spinner-border spinner-border-sm htmx-indicator" aria-hidden="true"></span>
<span class="visually-hidden" role="status">Loading...</span>
Save
</button>
</div>
</form>
{% endblock %}
{% block _scripts %}
<script>
// fix handling checkboxes
document.querySelectorAll('.form-check-input[type=checkbox]').forEach(function(checkbox) {
checkbox.addEventListener('change', function() {
checkbox.nextElementSibling.value = this.checked ? 'True' : 'False';
});
});
</script>
{% endblock %}