mirror of
https://codeberg.org/catask-org/catask.git
synced 2025-04-20 13:53:42 -05:00
add ntfy support
This commit is contained in:
parent
91cd4b4c43
commit
b2eac9654b
5 changed files with 142 additions and 12 deletions
40
app.py
40
app.py
|
@ -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,20 +349,36 @@ 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():
|
||||||
from_who = request.form.get('from_who', cfg['anonName'])
|
try:
|
||||||
question = request.form.get('question', '')
|
app.logger.debug("[CatAsk/API/add_question] started question flow")
|
||||||
cw = request.form.get('cw', '')
|
from_who = request.form.get('from_who', cfg['anonName'])
|
||||||
|
question = request.form.get('question', '')
|
||||||
|
cw = request.form.get('cw', '')
|
||||||
|
|
||||||
if not question:
|
if not question:
|
||||||
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
|
||||||
|
|
|
@ -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",
|
||||||
|
|
43
functions.py
43
functions.py
|
@ -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:
|
||||||
|
|
|
@ -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>
|
||||||
|
|
60
templates/admin/categories/notifications.html
Normal file
60
templates/admin/categories/notifications.html
Normal 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 & 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 %}
|
Loading…
Add table
Reference in a new issue