Compare commits

..

6 Commits

Author SHA1 Message Date
e81a69cffd fix: use message as link source fallback
All checks were successful
ci / docker (push) Successful in 1m9s
2026-02-28 23:54:06 +01:00
792b162025 fix: correctly import re 2026-02-28 23:50:47 +01:00
4b2d97fa87 fix: allow link parameter in dq 2026-02-28 23:44:37 +01:00
b2292943cd fix: remove README references to signal 2026-02-28 23:31:09 +01:00
69161135ec fix: set new app name & make db configurable 2026-02-28 23:19:37 +01:00
f08f6a2953 remove: signal support 2026-02-28 23:14:41 +01:00
3 changed files with 15 additions and 147 deletions

View File

@@ -1,93 +1,5 @@
# HTTP->Signal Gateway Notification Service # HTTP -> Notification Service
Simplistic server to listing for HTTP queries, specifically from Icinga or Grafana and send out Signal-Messages. Simplistic server to listing for HTTP queries, specifically from Icinga or Grafana and send out Alert-Messages.
# Signal Cli Setup
You need `glibc>=2.29`, check this first with `ldd --version` (for Debian this means bullseye or later).
Clone the following repositories
https://github.com/AsamK/signal-cli
https://github.com/signalapp/libsignal-client/
https://github.com/signalapp/zkgroup
Install the prerequisites (potentially non-exaustive list):
apt install gradle
https://www.rust-lang.org/tools/install (as current user)
Go to signal-cli project-root:
./gradlew build
./gradlew installDist
Go to libsignal-client project-root, change to java-directory and make sure to remove android from the build options, otherwise this will take ages:
cd java
sed -i "s/, ':android'//" settings.gradle
./build_jni.sh desktop
Go to zkgroup project-root and build it:
make libzkgroup
You need to make the build libraries available for java, either copy them to the java-library path (make sure they are readable for all users) or add them to the *LD\_LIBRARY\_PATH* enviroment variable whenever you intend to use the signal-cli binary.
To get the default java-library-path execute:
java -XshowSettings:properties 2>&1 | grep java.library
Usually on linux that's `/usr/java/packages/lib/`, though this directory might not exist yet, so:
sudo mkdir -p /usr/java/packages/lib/
sudo cp libsignal-client/target/release/libsignal_jni.so /usr/java/packages/lib/
sudo cp zkgroup/target/release/libzkgroup.so /usr/java/packages/lib/
sudo chmod a+rX /usr/java/packages/lib/
Or:
LD_LIBRARY_PATH=LD_LIBRARY_PATH:~/libsignal-client/target/release/:~/path/to/...
Now go to signal-cli project-root, we will have to make some preparations. First prepare your phone number, if you use a number which does not support SMS, use the `--voice`-switch to receive a call instead. Your full phone number means your number, including your country code (including a leading `+`), your area code (without any leading zeros).
You also need a captcha-token, for this open a browser tab first. Then open the developer console, then *make sure to have 'persist-logs' on*, and only *after* that navigate to:
https://signalcaptchas.org/registration/generate.html
You may or may not actually have to solve a chaptcha, in the console, after you the check succeeded,you will likely get a popup to open signal, ignore that and look into the dev-console, there should be something along the lines of:
Navigated to: signalchaptcha://very_very_loooooooooooong_token
Copy everything after `signalchaptcha://` and use it as the token for the `--captcha`-argument. Be advised, the token isn't valid very long:
cd build/install/signal-cli/bin/signal-cli
signal-cli -u FULL_PHONE_NUMBER register --voice --captcha 'TOKEN'
You will now get a SMS/call with the verification-code, which you can use with:
signal-cli -u FULL_PHONE_NUMBER verify CODE
You should consider setting a pin directly after, for help with this and other options use:
signal-cli -h
You should use `signal-cli receive` regulary, otherwise your account will be flagged inactive and potentially deleted. You may ommit the `-u` option if you only have registered one account with this user on this machine. Data (including private keys) are saved to `~/.local/share/signal-cli/`.
# Server Setup
Add the target number(s) (one per line) to signal\_targets.txt, then set the a enviroment variable `SIGNAL_API_PASS`, which must be used withing a basic authentication during access to the gateway. Finally execute the server:
usage: interface.py [-h] [--interface INTERFACE]
[--port PORT]
[--signal-cli-bin SIGNAL_CLI_BIN]
optional arguments:
-h, --help show this help message and exit
--interface INTERFACE
Interface on which to listen (default: localhost)
--port PORT Port on which to listen (default: 5000)
--signal-cli-bin SIGNAL_CLI_BIN
Path to signal-cli binary if no in $PATH (default: None)
`SIGNAL_CLI_BIN` can also be set as an environment variable, which will overwrite any command line option.
# HTTP Request # HTTP Request
The HTTP request must be a *POST*-request, with *Content-Type: application/json* and a json-field containing the key *"message"* with the value being the message you want to send. The HTTP request must be a *POST*-request, with *Content-Type: application/json* and a json-field containing the key *"message"* with the value being the message you want to send.
@@ -99,7 +11,7 @@ The following locations are supported:
# Example (curl) # Example (curl)
curl -u nobody:SIGNAL_API_PASS -X POST -H "Content-Type: application/json" --data '{"message":"hallo world"}' localhost:5000/send-all curl -u nobody:API_PASS -X POST -H "Content-Type: application/json" --data '{"message":"hello world"}' localhost:5000/send-all
# Additional Packages Required # Additional Packages Required

View File

@@ -6,6 +6,7 @@ import argparse
import subprocess import subprocess
import os import os
import requests import requests
import re
import smtphelper import smtphelper
import json import json
@@ -61,7 +62,7 @@ def ntfy_send(dispatch_uuid, user_topic, title, message, link,
# check message for links # # check message for links #
if not link: if not link:
pattern = r"https:\/\/[^\s]+" pattern = r"https:\/\/[^\s]+"
match = re.search(pattern, text) match = re.search(pattern, message)
if match: if match:
link = match.group(0) link = match.group(0)

View File

@@ -22,8 +22,8 @@ from sqlalchemy.sql.expression import func
OPENSEARCH_HEADER_SEPERATOR = "," OPENSEARCH_HEADER_SEPERATOR = ","
HOST = "icinga.atlantishq.de" HOST = "icinga.atlantishq.de"
app = flask.Flask("Signal Notification Gateway") app = flask.Flask("Atlantis Notification Gateway & Dispatcher")
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///sqlite.db" app.config["SQLALCHEMY_DATABASE_URI"] = os.environ.get("DB_URL") or "sqlite:///sqlite.db"
db = SQLAlchemy(app) db = SQLAlchemy(app)
BAD_DISPATCH_ACCESS_TOKEN = "Invalid or missing dispatch-access-token parameter in URL" BAD_DISPATCH_ACCESS_TOKEN = "Invalid or missing dispatch-access-token parameter in URL"
@@ -50,15 +50,13 @@ class UserSettings(db.Model):
__tablename__ = "user_settings" __tablename__ = "user_settings"
username = Column(String, primary_key=True) username = Column(String, primary_key=True)
signal_priority = Column(Integer) signal_priority = Column(Integer) # legacy, no longer used
email_priority = Column(Integer) email_priority = Column(Integer)
ntfy_priority = Column(Integer) ntfy_priority = Column(Integer)
def get_highest_prio_method(self): def get_highest_prio_method(self):
if self.signal_priority >= max(self.email_priority, self.ntfy_priority): if self.email_priority >= self.ntfy_priority:
return "signal"
elif self.email_priority >= max(self.signal_priority, self.ntfy_priority):
return "email" return "email"
else: else:
return "ntfy" return "ntfy"
@@ -66,7 +64,6 @@ class UserSettings(db.Model):
def serizalize(self): def serizalize(self):
return { return {
"username" : self.username, "username" : self.username,
"signal_priority" : self.signal_priority,
"email_priority" : self.email_priority, "email_priority" : self.email_priority,
"ntfy_priority" : self.ntfy_priority, "ntfy_priority" : self.ntfy_priority,
} }
@@ -114,9 +111,7 @@ class DispatchObject(db.Model):
user_settings = db.session.query(UserSettings).filter( user_settings = db.session.query(UserSettings).filter(
UserSettings.username == ret["username"]).first() UserSettings.username == ret["username"]).first()
if not user_settings and self.phone: if not user_settings and self.email:
ret["method"] = "signal"
elif not user_settings and self.email:
ret["method"] = "email" ret["method"] = "email"
elif user_settings: elif user_settings:
ret["method"] = user_settings.get_highest_prio_method() ret["method"] = user_settings.get_highest_prio_method()
@@ -210,7 +205,7 @@ def settings():
if flask.request.method == "POST": if flask.request.method == "POST":
posted = UserSettings(username=user, posted = UserSettings(username=user,
signal_priority=flask.request.json.get("signal_priority") or 0, signal_priority=-1,
email_priority=flask.request.json.get("email_priority") or 0, email_priority=flask.request.json.get("email_priority") or 0,
ntfy_priority=flask.request.json.get("ntfy_priority") or 0) ntfy_priority=flask.request.json.get("ntfy_priority") or 0)
db.session.merge(posted) db.session.merge(posted)
@@ -220,7 +215,7 @@ def settings():
if flask.request.method == "GET": if flask.request.method == "GET":
user_settings = db.session.query(UserSettings).filter(UserSettings.username==user).first() user_settings = db.session.query(UserSettings).filter(UserSettings.username==user).first()
if not user_settings: if not user_settings:
posted = UserSettings(username=user, signal_priority=5, email_priority=7, ntfy_priority=3) posted = UserSettings(username=user, signal_priority=-1, email_priority=7, ntfy_priority=3)
db.session.merge(posted) db.session.merge(posted)
db.session.commit() db.session.commit()
user_settings = posted user_settings = posted
@@ -240,7 +235,7 @@ def get_dispatch():
return (BAD_DISPATCH_ACCESS_TOKEN, 401) return (BAD_DISPATCH_ACCESS_TOKEN, 401)
if not method: if not method:
return (500, "Missing Dispatch Target (signal|email|phone|ntfy|all|any)") return (500, "Missing Dispatch Target (email|phone|ntfy|all|any)")
# prevent message floods # # prevent message floods #
timeout_cutoff = datetime.datetime.now() - datetime.timedelta(seconds=timeout) timeout_cutoff = datetime.datetime.now() - datetime.timedelta(seconds=timeout)
@@ -262,45 +257,7 @@ def get_dispatch():
else: else:
dispatch_objects = lines_timeout.all() dispatch_objects = lines_timeout.all()
# TODO THIS IS THE NEW MASTER PART
if method and method != "signal":
debug = [ d.serialize() for d in dispatch_objects]
if debug:
print(debug)
return flask.jsonify([ d.serialize() for d in dispatch_objects]) return flask.jsonify([ d.serialize() for d in dispatch_objects])
else:
# TODO THIS PART WILL BE REMOVED ##
# accumulate messages by person #
dispatch_by_person = dict()
dispatch_secrets = []
for dobj in dispatch_objects:
if dobj.username not in dispatch_by_person:
dispatch_by_person.update({ dobj.username : dobj.message })
dispatch_secrets.append(dobj.dispatch_secret)
else:
dispatch_by_person[dobj.username] += "\n{}".format(dobj.message)
dispatch_secrets.append(dobj.dispatch_secret)
# legacy hack #
if method == "any":
method = "signal"
response = [ { "person" : tupel[0].decode("utf-8"),
"message" : tupel[1],
"method" : method,
"uids" : dispatch_secrets
} for tupel in dispatch_by_person.items() ]
# add phone numbers and emails #
for obj in response:
for person in dispatch_objects:
if obj["person"] == person.username.decode("utf-8"):
if person.email:
obj.update({ "email" : person.email.decode("utf-8") })
if person.phone:
obj.update({ "phone" : person.phone.decode("utf-8") })
return flask.jsonify(response)
@app.route('/report-dispatch-failed', methods=["POST"]) @app.route('/report-dispatch-failed', methods=["POST"])
def reject_dispatch(): def reject_dispatch():
@@ -428,7 +385,7 @@ def smart_send_to_clients(path=None):
return flask.jsonify(dispatch_secrets) return flask.jsonify(dispatch_secrets)
def save_in_dispatch_queue(persons, title, message, method): def save_in_dispatch_queue(persons, title, message, method, link=""):
dispatch_secrets = [] dispatch_secrets = []
@@ -497,8 +454,6 @@ if __name__ == "__main__":
parser.add_argument('--interface', default="localhost", help='Interface on which to listen') parser.add_argument('--interface', default="localhost", help='Interface on which to listen')
parser.add_argument('--port', default="5000", help='Port on which to listen') parser.add_argument('--port', default="5000", help='Port on which to listen')
parser.add_argument("--signal-cli-bin", default=None, type=str,
help="Path to signal-cli binary if no in $PATH")
parser.add_argument('--ldap-server') parser.add_argument('--ldap-server')
parser.add_argument('--ldap-base-dn') parser.add_argument('--ldap-base-dn')