diff options
-rw-r--r-- | config | 5 | ||||
-rwxr-xr-x | mqtt-notify.py | 177 | ||||
-rw-r--r-- | mqtt-notify.service | 11 |
3 files changed, 193 insertions, 0 deletions
@@ -0,0 +1,5 @@ +[DEFAULT] +broker = example.com +port = 8883 +topic = irssi/# +user = myuser diff --git a/mqtt-notify.py b/mqtt-notify.py new file mode 100755 index 0000000..36fc56b --- /dev/null +++ b/mqtt-notify.py @@ -0,0 +1,177 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# SPDX-License-Identifier: GPL-3.0-or-later + +# Relevant API docs: +# https://pypi.org/project/paho-mqtt/ +# https://lazka.github.io/pgi-docs/#Notify-0.7 +# https://lazka.github.io/pgi-docs/#Secret-1 +# https://lazka.github.io/pgi-docs/#GLib-2.0 +# https://dbus.freedesktop.org/doc/dbus-python/dbus.mainloop.html + +import argparse +import configparser +import re +import signal +import sys +import paho.mqtt.client as mqtt +import gi +gi.require_version('Notify', '0.7') +gi.require_version('Secret', '1') +from gi.repository import GLib, Notify, Secret + +from dbus.mainloop.glib import DBusGMainLoop +DBusGMainLoop(set_as_default=True) + +chan_msg = re.compile(r'\[(?P<channel>#.*?)\]\n<\s*(?P<nick>.*?)> \| (?P<msg>.*)') +priv_msg = re.compile(r'\(PM: (?P<nick>.*?)\)\n(?P<msg>.*)') +subj_fmt = re.compile(r'IRC message (on|from) (?P<key>.*)') + +notification_map = {} + +class Signaler: + def __init__(self, loop): + self.loop = loop + + def handler(self, *_): + self.loop.quit() + +class Match: + def __init__(self, matchstring): + self.matchstring = matchstring + + def match(self, regexp): + self.rematch = re.match(regexp, self.matchstring) + return bool(self.rematch) + + def group(self, i): + return self.rematch.group(i) + +def on_connect(client, userdata, flags, rc): + print("Connected") + + # Subscribing in on_connect() means that if we lose the connection and + # reconnect then subscriptions will be renewed. + client.subscribe(userdata) + +def on_close(notification): + m = subj_fmt.match(notification.props.summary) + if m: + key = m.group('key') + else: + key = '' + + if key in notification_map: + for i in notification_map[key]: + if i is not notification: + i.close() + del notification_map[key] + +def on_message(client, userdata, msg): + icon = '/usr/share/icons/HighContrast/scalable/apps-extra/internet-group-chat.svg' + message = msg.payload.decode('utf-8') + + m = Match(message) + if m.match(chan_msg): + subject = 'IRC message on {}'.format(m.group('channel')) + body = '<{}> {}'.format(m.group('nick'), m.group('msg')) + key = m.group('channel') + elif m.match(priv_msg): + subject = 'IRC message from {}'.format(m.group('nick')) + body = '<{}> {}'.format(m.group('nick'), m.group('msg')) + key = m.group('nick') + else: + subject = 'IRC' + body = message + key = '' + + if key not in notification_map or len(notification_map[key]) == 1: + n = Notify.Notification.new(subject, body, icon) + n.set_category('im.received') + n.connect('closed', on_close) + + if key not in notification_map: + notification_map[key] = [n] + else: + notification_map[key].append(n) + else: + n = notification_map[key][1] + n.update(subject, body, icon) + + n.show() + +def on_disconnect(client, userdata, rc): + print("Disconnected") + +def password(user, host): + # Insert password with secret-tool(1). E.g., + # secret-tool store --label="mqtts://example.com" user myuser service mqtt host example.com + + schema = Secret.Schema.new("org.freedesktop.Secret.Generic", + Secret.SchemaFlags.NONE, + { + "user": Secret.SchemaAttributeType.STRING, + "service": Secret.SchemaAttributeType.STRING, + "host": Secret.SchemaAttributeType.STRING, + } + ) + + return Secret.password_lookup_sync(schema, + { + "user": user, + "service": "mqtt", + "host": host, + }, + None) + +def config(filename): + try: + with open(filename) as file: + config = configparser.ConfigParser() + config.read_file(file) + + cfg = config[configparser.DEFAULTSECT] + broker = cfg['broker'] + topic = cfg['topic'] + port = int(cfg['port']) + user = cfg['user'] + + return user, broker, port, topic + except: + print("Failed to parse {}".format(filename), file=sys.stderr) + sys.exit(-1) + +def main(argv): + loop = GLib.MainLoop() + + do = Signaler(loop) + signal.signal(signal.SIGINT, do.handler) + signal.signal(signal.SIGTERM, do.handler) + + parser = argparse.ArgumentParser() + parser.add_argument('-c', '--config', help='configuration file', + type=argparse.FileType('r'), required=True) + args = parser.parse_args() + + user, broker, port, topic = config(args.config.name) + + Notify.init('MQTT to Notify bridge') + client = mqtt.Client(userdata=topic) + + client.tls_set() + client.username_pw_set(user, password(user, broker)) + client.on_connect = on_connect + client.on_message = on_message + client.on_disconnect = on_disconnect + client.connect_async(broker, port, 60) + + client.loop_start() + + loop.run() + + client.loop_stop() + client.disconnect() + Notify.uninit() + +if __name__ == '__main__': + main(sys.argv) diff --git a/mqtt-notify.service b/mqtt-notify.service new file mode 100644 index 0000000..6a5c7f5 --- /dev/null +++ b/mqtt-notify.service @@ -0,0 +1,11 @@ +[Unit] +Description=MQTT to libnotify IRC notifications +After=network-online.target nss-lookup.target +Wants=network-online.target nss-lookup.target + +[Service] +Type=simple +ExecStart=%h/bin/mqtt-notify.py -c %E/%p/config + +[Install] +WantedBy=default.target |