notify: add app.py
This commit is contained in:
parent
0b08076702
commit
925041a5d8
1 changed files with 288 additions and 0 deletions
288
machines/gerd/services/notify/app.py
Normal file
288
machines/gerd/services/notify/app.py
Normal file
|
@ -0,0 +1,288 @@
|
|||
#!/usr/bin/env nix-shell
|
||||
#!nix-shell --pure -i python3 -p "python3.withPackages (ps: with ps; [ flask apprise mnemonic wtforms jq ])"
|
||||
from typing import Any
|
||||
import apprise
|
||||
from flask import Flask, request
|
||||
from mnemonic import Mnemonic
|
||||
import sqlite3
|
||||
import os
|
||||
import jq
|
||||
import json
|
||||
|
||||
ENV_PREFIX = "NOTIFIER_"
|
||||
|
||||
|
||||
def getenv(key: str, default: Any = None) -> Any:
|
||||
v = os.getenv(ENV_PREFIX + key, default)
|
||||
if not v:
|
||||
exit(f"{ENV_PREFIX+key} must be specified!")
|
||||
|
||||
return v
|
||||
|
||||
|
||||
CONFIG_URL = getenv("URL", "http://127.0.0.1")
|
||||
CONFIG_PORT = int(getenv("PORT", 8080))
|
||||
|
||||
CONFIG_DATABASE_PATH = getenv("DATABASE_PATH", "notifications.db")
|
||||
|
||||
CONFIG_MATRIX_BOT_TOKEN = getenv("MATRIX_BOT_TOKEN")
|
||||
CONFIG_MATRIX_HOST = getenv("MATRIX_HOST")
|
||||
|
||||
CONFIG_PROXY_AUTH_USERNAME_HEADER = getenv("PROXY_AUTH_USERNAME_HEADER", "Remote-User")
|
||||
|
||||
CONFIG_MAIL_USERNAME = getenv("MAIL_USERNAME")
|
||||
CONFIG_MAIL_PASSWORD = getenv("MAIL_PASSWORD")
|
||||
CONFIG_MAIL_DOMAIN = getenv("MAIL_DOMAIN")
|
||||
CONFIG_MAIL_HOST = getenv("MAIL_HOST")
|
||||
CONFIG_MAIL_PORT = int(getenv("MAIL_PORT"))
|
||||
CONFIG_MAIL_MODE = getenv("MAIL_MODE", "ssl")
|
||||
|
||||
|
||||
script_example = r"""#!/usr/bin/env bash
|
||||
BODY="$1"
|
||||
TITLE=${2:-Notification}
|
||||
JQ_EXPR=${3:-.}
|
||||
TYPE=${4:-matrix}
|
||||
TOKEN="$(cat ~/.config/notify/token)"
|
||||
# TOKEN="$(cat /run/agenix/notify-token)"
|
||||
URL="||URL||/notify"
|
||||
|
||||
# get stdin if needed
|
||||
if [ "$BODY" = "-" ]; then
|
||||
BODY="$(cat -)"
|
||||
fi
|
||||
|
||||
# make request
|
||||
curl -H "Authorization: Bearer $TOKEN" "$URL" \
|
||||
--get \
|
||||
--data-urlencode "title=$TITLE" \
|
||||
--data-urlencode "body=$BODY" \
|
||||
--data-urlencode "jq=$JQ_EXPR" \
|
||||
--data-urlencode "type=$TYPE"
|
||||
""".replace(
|
||||
"||URL||", "https://notify.fricloud.dk"
|
||||
)
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
|
||||
def get_db():
|
||||
con = sqlite3.connect(CONFIG_DATABASE_PATH)
|
||||
cur = con.cursor()
|
||||
cur.execute(
|
||||
"CREATE TABLE IF NOT EXISTS default_room(username TEXT PRIMARY KEY, room_id TEXT NOT NULL)"
|
||||
)
|
||||
cur.execute(
|
||||
"CREATE TABLE IF NOT EXISTS tokens(username TEXT PRIMARY KEY, token TEXT NOT NULL)"
|
||||
)
|
||||
|
||||
return con
|
||||
|
||||
|
||||
@app.route("/", methods=["GET", "POST"])
|
||||
def index():
|
||||
username = request.headers.get(CONFIG_PROXY_AUTH_USERNAME_HEADER)
|
||||
if not username:
|
||||
return ("Not authenticated", 401)
|
||||
|
||||
# handle post stuff
|
||||
if request.method == "POST":
|
||||
action = request.form.get("action", "").lower()
|
||||
print("Action", action)
|
||||
if "token" in action:
|
||||
generate_token_for_user(username)
|
||||
elif "room id" in action:
|
||||
roomid = request.form.get("room_id")
|
||||
if not roomid:
|
||||
return ("Room Id cannot be empty", 400)
|
||||
set_user_default_matrix_room(username, roomid)
|
||||
else:
|
||||
return ("Unknown action", 400)
|
||||
|
||||
con = get_db()
|
||||
cur = con.cursor()
|
||||
res = cur.execute(
|
||||
"SELECT token FROM tokens WHERE username = ?", (username,)
|
||||
).fetchone()
|
||||
|
||||
token: str = ""
|
||||
if res:
|
||||
token = res[0]
|
||||
|
||||
res = cur.execute(
|
||||
"SELECT room_id FROM default_room WHERE username = ?", (username,)
|
||||
).fetchone()
|
||||
|
||||
room_id: str = ""
|
||||
if res:
|
||||
room_id = res[0]
|
||||
|
||||
tmpl = f"""<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||
<title>Notification Service</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="container my-5">
|
||||
<h1>Hello {username}!</h1>
|
||||
<div class="col-lg-8 px-0">
|
||||
<form method="post">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Default Matrix Room ID</label>
|
||||
<input type="text" name="room_id" value="{room_id}" placeholder="!yREJWHUMJhGROiHbtu:fricloud.dk" class="form-control">
|
||||
</div>
|
||||
<input type="submit" class="btn btn-primary" name="action" value="Set Default Room ID">
|
||||
<hr>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Token</label>
|
||||
<input type="text" value="{token}" placeholder="token-not-generated" readonly class="form-control" >
|
||||
</div>
|
||||
<input type="submit" class="btn btn-primary" name="action" value="Generate Token">
|
||||
</form>
|
||||
<hr>
|
||||
<pre><code>
|
||||
{script_example}
|
||||
</code></pre>
|
||||
<!--
|
||||
<p class="fs-5">You've successfully loaded up the Bootstrap starter example. It includes <a href="https://getbootstrap.com/">Bootstrap 5</a> via the <a href="https://www.jsdelivr.com/package/npm/bootstrap">jsDelivr CDN</a> and includes an additional CSS and JS file for your own code.</p>
|
||||
<p>Feel free to download or copy-and-paste any parts of this example.</p>
|
||||
|
||||
<hr class="col-1 my-4">
|
||||
|
||||
<a href="https://getbootstrap.com" class="btn btn-primary">Read the Bootstrap docs</a>
|
||||
<a href="https://github.com/twbs/examples" class="btn btn-secondary">View on GitHub</a>
|
||||
-->
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
return tmpl
|
||||
|
||||
|
||||
@app.route("/notify", methods=["GET", "POST"])
|
||||
def send_notification():
|
||||
# default to this
|
||||
ntype = request.args.get("type", "matrix")
|
||||
title = request.args.get("title", "Notification")
|
||||
body = request.args.get("body", request.get_data().decode("utf-8"))
|
||||
if not body:
|
||||
body = " "
|
||||
print("BODY", body)
|
||||
token = request.authorization.token or request.authorization.password
|
||||
if not token:
|
||||
return (
|
||||
"No token found, please either specify with Authorization: Bearer <token> or HTTPBasic",
|
||||
401,
|
||||
)
|
||||
|
||||
if (
|
||||
request.method == "POST"
|
||||
and "json" in request.headers.get("content-type", "")
|
||||
or is_json(body)
|
||||
):
|
||||
try:
|
||||
body = jq.compile(request.args.get("jq", ".")).input_text(body).first()
|
||||
except Exception:
|
||||
return ("Unable to compile JQ, please ensure it is correct", 400)
|
||||
|
||||
con = get_db()
|
||||
cur = con.cursor()
|
||||
print(token)
|
||||
res = cur.execute(
|
||||
"SELECT username FROM tokens WHERE token = ?", (token,)
|
||||
).fetchone()
|
||||
|
||||
if not res:
|
||||
return ("Access denied - invalid token", 401)
|
||||
|
||||
username = res[0]
|
||||
|
||||
if ntype not in ["matrix", "mail"]:
|
||||
return ("Invalid type, only matrix or mail allowed", 400)
|
||||
|
||||
print(ntype, username, token)
|
||||
|
||||
apobj = apprise.Apprise()
|
||||
if ntype == "matrix":
|
||||
# try to get a room_id
|
||||
room_id = request.args.get("room_id")
|
||||
if not room_id:
|
||||
res = cur.execute(
|
||||
"SELECT room_id FROM default_room WHERE username = ?", (username,)
|
||||
).fetchone()
|
||||
if res:
|
||||
room_id = res[0]
|
||||
|
||||
print(room_id)
|
||||
|
||||
if not room_id:
|
||||
return ("No room_id specified, and no default saved", 400)
|
||||
|
||||
apobj.add(f"matrixs://{CONFIG_MATRIX_BOT_TOKEN}@{CONFIG_MATRIX_HOST}/{room_id}")
|
||||
else:
|
||||
apobj.add(
|
||||
f"mailto://{CONFIG_MAIL_USERNAME}:{CONFIG_MAIL_PASSWORD}@{CONFIG_MAIL_HOST}:{CONFIG_MAIL_PORT}?mode={CONFIG_MAIL_MODE}&from={CONFIG_MAIL_USERNAME}@{CONFIG_MAIL_DOMAIN}&to={username}@{CONFIG_MAIL_DOMAIN}"
|
||||
)
|
||||
|
||||
sent_notification = apobj.notify(
|
||||
title=title,
|
||||
body=body,
|
||||
)
|
||||
|
||||
if not send_notification:
|
||||
return ("Unable to send notification", 500)
|
||||
return ("Notification sent!", 200)
|
||||
|
||||
|
||||
def generate_token_for_user(username: str):
|
||||
token = generate_token()
|
||||
con = get_db()
|
||||
cur = con.cursor()
|
||||
t = cur.execute(
|
||||
"INSERT INTO tokens (username, token) VALUES (?, ?) ON CONFLICT (username) DO UPDATE SET token = ?",
|
||||
(
|
||||
username,
|
||||
token,
|
||||
token,
|
||||
),
|
||||
)
|
||||
con.commit()
|
||||
|
||||
|
||||
def set_user_default_matrix_room(username: str, roomid: str):
|
||||
con = get_db()
|
||||
cur = con.cursor()
|
||||
t = cur.execute(
|
||||
"INSERT INTO default_room (username, room_id) VALUES (?, ?) ON CONFLICT (username) DO UPDATE SET room_id = ?",
|
||||
(
|
||||
username,
|
||||
roomid,
|
||||
roomid,
|
||||
),
|
||||
)
|
||||
con.commit()
|
||||
|
||||
|
||||
def generate_token(num_words: int = 5) -> str:
|
||||
mnemo = Mnemonic("english")
|
||||
words = mnemo.generate(strength=256)
|
||||
return "-".join(words.split(" ")[0:num_words])
|
||||
|
||||
|
||||
def is_json(potential_json: str) -> bool:
|
||||
try:
|
||||
json.loads(potential_json)
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(host="127.0.0.1", port=CONFIG_PORT, debug=True)
|
Loading…
Add table
Add a link
Reference in a new issue