Compare commits

...

16 Commits

Author SHA1 Message Date
4e4f53b330 feat: support dynamic links in msg or link-field
Some checks failed
ci / docker (push) Failing after 8s
2025-06-05 21:45:16 +02:00
35f9fc2a99 feat: support 'message' & 'link' fields 2025-06-05 21:35:31 +02:00
53e6f32a18 add: header token auth 2025-06-05 20:59:01 +02:00
f2137e7e4c fix: deal with empty body or title
Some checks failed
ci / docker (push) Failing after 15s
2025-01-05 15:30:12 +01:00
c577802e63 fi: limit title and body length 2025-01-05 15:21:28 +01:00
2a74d9816f fix: honor 429 response 2025-01-05 15:19:53 +01:00
178ba5451d fix: make os header optional
Some checks failed
ci / docker (push) Failing after 47s
2024-11-23 23:22:14 +01:00
14612016af feat: add opensearch support 2024-11-23 23:15:31 +01:00
df1dfd8b0c change: status as json instead of string
Some checks failed
ci / docker (push) Failing after 4s
2024-11-14 22:02:00 +01:00
acf88ffa6e feat: downtime status information 2024-11-14 22:02:00 +01:00
7fa965a92c fix: change output to stderr
Some checks failed
ci / docker (push) Failing after 7s
2024-11-15 00:01:57 +01:00
e416149d35 fix: output users 2024-11-14 23:56:53 +01:00
181b3dae14 fix: add return view to endpoint 2024-11-14 23:54:39 +01:00
c10bdf1fb7 fix: output downtime after reading args 2024-11-14 23:53:24 +01:00
6783426e5f fix: add minutes default and cast 2024-11-14 23:50:15 +01:00
bc837169ff feat: implement downtime setting 2024-11-14 23:29:09 +01:00
2 changed files with 92 additions and 15 deletions

View File

@@ -27,7 +27,7 @@ def debug_send(uuid, data, fail_it=False):
def email_send(dispatch_uuid, email_address, message, smtp_target, def email_send(dispatch_uuid, email_address, message, smtp_target,
smtp_target_port, smtp_user, smtp_pass): smtp_target_port, smtp_user, smtp_pass):
'''Send message via email''' '''Send message via email'''
if not email_address: if not email_address:
print("Missing E-Mail Address for STMP send", file=sys.stderr) print("Missing E-Mail Address for STMP send", file=sys.stderr)
report_failed_dispatch(dispatch_uuid, "Missing email-field in dispatch infor") report_failed_dispatch(dispatch_uuid, "Missing email-field in dispatch infor")
@@ -54,15 +54,32 @@ def ntfy_api_get_topic(ntfy_api_server, ntfy_api_token, username):
print(r.text) print(r.text)
return r.json().get("topic") return r.json().get("topic")
def ntfy_send(dispatch_uuid, user_topic, title, message, ntfy_push_target, ntfy_user, ntfy_pass): def ntfy_send(dispatch_uuid, user_topic, title, message, link,
ntfy_push_target, ntfy_user, ntfy_pass):
'''Send message via NTFY topic''' '''Send message via NTFY topic'''
# check message for links #
if not link:
pattern = r"https:\/\/[^\s]+"
match = re.search(pattern, text)
if match:
link = match.group(0)
# limit message length and title #
title = title or ""
message = message or ""
message = message[:1024]
title = title[:512]
if not user_topic: if not user_topic:
report_failed_dispatch(dispatch_uuid, "No user topic") report_failed_dispatch(dispatch_uuid, "No user topic")
return return
try: try:
# build message # # build message #
payload = { payload = {
"topic" : user_topic, "topic" : user_topic,
@@ -71,13 +88,16 @@ def ntfy_send(dispatch_uuid, user_topic, title, message, ntfy_push_target, ntfy_
#"tags" : [], #"tags" : [],
"priority" : 4, "priority" : 4,
#"attach" : None, #"attach" : None,
"click" : "https://vid.pr0gramm.com/2022/11/06/ed66c8c5a9cd1a3b.mp4", "click" : link,
#"actions" : [] #"actions" : []
} }
# send # # send #
r = requests.post(ntfy_push_target, auth=(ntfy_user, ntfy_pass), json=payload) r = requests.post(ntfy_push_target, auth=(ntfy_user, ntfy_pass), json=payload)
print(r.status_code, r.text, payload) print(r.status_code, r.text, payload)
if r.status_code == 429: # rate-limit
time.sleep(60)
r.raise_for_status() r.raise_for_status()
# talk to dispatch # # talk to dispatch #
@@ -131,7 +151,7 @@ if __name__ == "__main__":
parser.add_argument('--loop', default=True, action=argparse.BooleanOptionalAction) parser.add_argument('--loop', default=True, action=argparse.BooleanOptionalAction)
args = parser.parse_args() args = parser.parse_args()
dispatch_server = args.dispatch_server or os.environ.get("DISPATCH_SERVER") dispatch_server = args.dispatch_server or os.environ.get("DISPATCH_SERVER")
@@ -157,7 +177,7 @@ if __name__ == "__main__":
while args.loop or first_run: while args.loop or first_run:
# request dispatches # # request dispatches #
response = requests.get(dispatch_server + response = requests.get(dispatch_server +
"/get-dispatch?method=all&timeout=0&dispatch-access-token={}".format(DISPATCH_ACCESS_TOKEN)) "/get-dispatch?method=all&timeout=0&dispatch-access-token={}".format(DISPATCH_ACCESS_TOKEN))
# check status # # check status #
@@ -181,6 +201,7 @@ if __name__ == "__main__":
method = entry["method"] method = entry["method"]
message = entry["message"] message = entry["message"]
title = entry.get("title") title = entry.get("title")
link = entry.get("link")
# method dependent fields # # method dependent fields #
phone = entry.get("phone") phone = entry.get("phone")
@@ -191,7 +212,7 @@ if __name__ == "__main__":
pass pass
elif method == "ntfy": elif method == "ntfy":
user_topic = ntfy_api_get_topic(ntfy_api_server, ntfy_api_token, user) user_topic = ntfy_api_get_topic(ntfy_api_server, ntfy_api_token, user)
ntfy_send(dispatch_uuid, user_topic, title, message, ntfy_send(dispatch_uuid, user_topic, title, message, link,
ntfy_push_target, ntfy_user, ntfy_pass) ntfy_push_target, ntfy_user, ntfy_pass)
elif method == "email": elif method == "email":
email_send(dispatch_uuid, email_address, message, smtp_target, email_send(dispatch_uuid, email_address, message, smtp_target,
@@ -207,6 +228,6 @@ if __name__ == "__main__":
# wait a moment # # wait a moment #
if args.loop: if args.loop:
time.sleep(5) time.sleep(5)
# handle non-loop runs # # handle non-loop runs #
first_run = False first_run = False

View File

@@ -20,7 +20,7 @@ import sqlalchemy
from flask_sqlalchemy import SQLAlchemy from flask_sqlalchemy import SQLAlchemy
from sqlalchemy.sql.expression import func from sqlalchemy.sql.expression import func
OPENSEARCH_HEADER_SEPERATOR = ","
HOST = "icinga.atlantishq.de" HOST = "icinga.atlantishq.de"
app = flask.Flask("Signal Notification Gateway") app = flask.Flask("Signal Notification Gateway")
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///sqlite.db" app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///sqlite.db"
@@ -84,6 +84,7 @@ class DispatchObject(db.Model):
title = Column(String) title = Column(String)
message = Column(String, primary_key=True) message = Column(String, primary_key=True)
method = Column(String) method = Column(String)
link = Column(String)
dispatch_secret = Column(String) dispatch_secret = Column(String)
dispatch_error = Column(String) dispatch_error = Column(String)
@@ -98,6 +99,7 @@ class DispatchObject(db.Model):
"email" : self.email, "email" : self.email,
"title" : _apply_substitution(self.title), "title" : _apply_substitution(self.title),
"message" : _apply_substitution(self.message), "message" : _apply_substitution(self.message),
"link" : self.link,
"uuid" : self.dispatch_secret, "uuid" : self.dispatch_secret,
"method" : self.method, "method" : self.method,
"error" : self.dispatch_error, "error" : self.dispatch_error,
@@ -169,6 +171,31 @@ def webhooks():
db.session.commit() db.session.commit()
return ("", 204) return ("", 204)
@app.route('/downtime', methods=["GET", "DELETE","POST"])
def downtime():
# check static access token #
token = flask.request.args.get("token")
if token != app.config["SETTINGS_ACCESS_TOKEN"]:
return ("SETTINGS_ACCESS_TOKEN incorrect. Refusing to access downtime settings", 401)
if flask.request.method == "DELETE":
app.config["DOWNTIME"] = datetime.datetime.now()
return ('Downtime successfully disabled', 200)
elif flask.request.method == "POST":
minutes = int(flask.request.args.get("minutes") or 5)
app.config["DOWNTIME"] = datetime.datetime.now() + datetime.timedelta(minutes=minutes)
return ('Downtime set to {}'.format(app.config["DOWNTIME"].isoformat(), 204))
elif flask.request.method == "GET":
dt = app.config["DOWNTIME"]
if dt < datetime.datetime.now():
return flask.jsonify({"title" : "No Downtime set at the moment", "message" : ""})
else:
delta = int((dt - datetime.datetime.now()).total_seconds()/60)
return flask.jsonify({"title" : "Downtime set for {}m until {}".format(delta, dt.isoformat()),
"message" : ""})
@app.route('/settings', methods=["GET", "POST"]) @app.route('/settings', methods=["GET", "POST"])
def settings(): def settings():
@@ -332,16 +359,42 @@ def smart_send_to_clients(path=None):
- supported struct of type "ICINGA|ZABBIX|GENERIC" (see docs) in field "data" - supported struct of type "ICINGA|ZABBIX|GENERIC" (see docs) in field "data"
''' '''
instructions = flask.request.json if flask.request.headers.get("opensearch"):
users = instructions.get("users") instructions = {}
groups = instructions.get("groups") users = flask.request.headers.get("opensearch-users")
message = instructions.get("msg") groups = flask.request.headers.get("opensearch-groups")
title = instructions.get("title")
method = instructions.get("method") if groups and OPENSEARCH_HEADER_SEPERATOR in groups:
groups = groups.split(OPENSEARCH_HEADER_SEPERATOR)
if users and OPENSEARCH_HEADER_SEPERATOR in users:
users = users.split(OPENSEARCH_HEADER_SEPERATOR)
message = flask.request.get_data(as_text=True)
title = "Opensearch Alert"
method = None
else:
instructions = flask.request.json
users = instructions.get("users")
groups = instructions.get("groups")
message = instructions.get("msg") or instructions.get("message")
title = instructions.get("title")
method = instructions.get("method")
link = instructions.get("link")
if app.config["DOWNTIME"] > datetime.datetime.now():
print("Ignoring because of Downtime:", title, message, users, file=sys.stderr)
print("Downtime until", app.config["DOWNTIME"].isoformat(), file=sys.stderr)
return ("Ignored because of Downtime", 200)
# authenticated by access token or webhook path # # authenticated by access token or webhook path #
dispatch_acces_token = flask.request.args.get("dispatch-access-token") or "" dispatch_acces_token = flask.request.args.get("dispatch-access-token") or ""
if not dispatch_acces_token:
dispatch_acces_token = flask.request.headers.get("Dispatcher-Token") or ""
print(path) print(path)
if path: if path:
webhook_path = db.session.query(WebHookPaths).filter(WebHookPaths.path==path).first() webhook_path = db.session.query(WebHookPaths).filter(WebHookPaths.path==path).first()
@@ -371,7 +424,7 @@ def smart_send_to_clients(path=None):
else: else:
persons = ldaptools.select_targets(users, groups, app.config["LDAP_ARGS"]) persons = ldaptools.select_targets(users, groups, app.config["LDAP_ARGS"])
dispatch_secrets = save_in_dispatch_queue(persons, title, message, method) dispatch_secrets = save_in_dispatch_queue(persons, title, message, method, link)
return flask.jsonify(dispatch_secrets) return flask.jsonify(dispatch_secrets)
@@ -395,6 +448,7 @@ def save_in_dispatch_queue(persons, title, message, method):
timestamp=datetime.datetime.now().timestamp(), timestamp=datetime.datetime.now().timestamp(),
dispatch_secret=dispatch_secret, dispatch_secret=dispatch_secret,
title=title, title=title,
link=link,
message=message) message=message)
db.session.merge(obj) db.session.merge(obj)
@@ -433,6 +487,8 @@ def create_app():
print("Loaded subs:", substitution_config_file, app.config["SUBSTITUTIONS"], file=sys.stderr) print("Loaded subs:", substitution_config_file, app.config["SUBSTITUTIONS"], file=sys.stderr)
# set small downtime #
app.config["DOWNTIME"] = datetime.datetime.now() + datetime.timedelta(minutes=1)
if __name__ == "__main__": if __name__ == "__main__":