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

40
app.py
View file

@ -5,6 +5,7 @@ from mysql.connector import errorcode
from functools import wraps
from werkzeug.utils import secure_filename
from pathlib import Path
import threading
import requests
import secrets
import shutil
@ -232,6 +233,11 @@ def information():
def accessibility():
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'])
@loginRequired
def general():
@ -343,20 +349,36 @@ def pwaManifest():
"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 --
@api_bp.route('/add_question/', methods=['POST'])
def addQuestion():
from_who = request.form.get('from_who', cfg['anonName'])
question = request.form.get('question', '')
cw = request.form.get('cw', '')
try:
app.logger.debug("[CatAsk/API/add_question] started question flow")
from_who = request.form.get('from_who', cfg['anonName'])
question = request.form.get('question', '')
cw = request.form.get('cw', '')
if not question:
abort(400, "Question field must not be empty")
if len(question) > int(cfg['charLimit']) or len(from_who) > int(cfg['charLimit']):
abort(400, "Question exceeds the character limit")
return func.addQuestion(from_who, question, cw)
if not question:
abort(400, "Question field must not be empty")
if len(question) > int(cfg['charLimit']) or len(from_who) > int(cfg['charLimit']):
abort(400, "Question exceeds the character limit")
return_val = 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'])
@loginRequired

View file

@ -41,6 +41,13 @@
"apikey": ""
}
},
"ntfy": {
"enabled": false,
"host": "https://ntfy.sh",
"user": "",
"pass": "",
"topic": ""
},
"trimContentAfter": "150",
"charLimit": "512",
"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 bleach.sanitizer import Cleaner
from datetime import datetime, timezone
from pathlib import Path
from mistune import HTMLRenderer, escape
from PIL import Image
import base64
import time
import zipfile
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")
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()
conn.close()
return {'message': 'Question asked successfully!'}, 201
return {'message': 'Question asked successfully!'}, 201, question_id
def getAnswer(question_id: int):
conn = connectToDb()
@ -229,6 +231,41 @@ def addAnswer(question_id, answer, cw):
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):
if os.path.exists(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>
<span class="sidebar-btn-text ms-2 ps-1">Accessibility</span>
</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') }}">
<i class="bi bi-columns-gap fs-5 scale-child"></i>
<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 %}