#!/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 time 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#.*?)\]\n<\s*(?P.*?)> \| (?P.*)') priv_msg = re.compile(r'\(PM: (?P.*?)\)\n(?P.*)') subj_fmt = re.compile(r'IRC message (on|from) (?P.*)') notification_map = {} class Signaler: def __init__(self, loop): self.loop = loop def handler(self, *_): self.loop.quit() 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): key = '' if (m := subj_fmt.match(notification.props.summary)) is not None: key = m.group('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') if (m := re.match(chan_msg, message)) is not None: subject = 'IRC message on {}'.format(m.group('channel')) body = '<{}> {}'.format(m.group('nick'), m.group('msg')) key = m.group('channel') if (m := re.match(priv_msg, message)) is not None: 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) try: n.show() except GLib.GError as e: print("Failed to show notification: {}".format(e), file=sys.stderr) sys.exit(-1) 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, } ) attributes = { "user": user, "service": "mqtt", "host": host, } while (pw := Secret.password_lookup_sync(schema, attributes, None)) is None: time.sleep(5) return pw 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)