3

Running Ubuntu 24.04lts.

NOTE: I am trying to read the notification sent by he Discord app to my personal desktop notifications. When I notification from a specific sender, with a specific message, I want to run a command. For now just being able to print MY desktop notifications to terminal output will suffice.

I am trying to create a script that listens to the desktop notifications dbus and print the sender and message to the command line.... the final goal is to run a specific function when I get a specific notification.

I want to see these notifications on the command line. They are intended for me.... why I can't I easily access this info???

enter image description here

I tried using dbus-monitor --session which prints stuff out when I get a notification, but I don't see the actual notification. I had Gemini AI help me write some python code, this code runs, but nothing happens when I get a notification.

import asyncio
from dbus_next.aio import MessageBus
from dbus_next.constants import BusType, MessageType
from dbus_next import Message, Variant
import logging

# Set up basic logging
# Note: We are only using INFO level for connection status, 
# message processing is handled via print() for clarity.
logging.basicConfig(level=logging.INFO, format='%(levelname)s: %(message)s')

# --- DBus Interface Constants ---
# The D-Bus service name for notifications
NOTIF_SERVICE = 'org.freedesktop.Notifications'
# The D-Bus object path for notifications
NOTIF_PATH = '/org/freedesktop/Notifications'
# The D-Bus interface for notifications
NOTIF_INTERFACE = NOTIF_SERVICE

# ANSI escape codes for bold text
BOLD_START = '\033[1m'
BOLD_END = '\033[0m'

def message_handler(message):
    """
    Generic handler for D-Bus messages, focusing only on new notifications 
    (Notify method calls) and printing the sender, summary, and message body.
    """
    if message.interface == NOTIF_INTERFACE:
        
        # --- Handle New Notification (Method Call) ---
        if message.message_type == MessageType.METHOD_CALL and message.member == 'Notify':
            # Notify arguments: (app_name, replaces_id, app_icon, summary, body, actions, hints, expire_timeout)
            
            # Ensure we have enough arguments (at least app_name and body)
            if len(message.body) >= 8:
                app_name = message.body[0] # App Name (Sender)
                summary = message.body[3] # Notification Summary (Title)
                body_text = message.body[4] # Notification Body (Main message)
                
                # Combine summary and body for the message
                # If both exist, use "Summary: Body". If only one exists, use that.
                if summary and body_text:
                    message_content = f"{summary}: {body_text}"
                elif summary:
                    message_content = summary
                else:
                    message_content = body_text

                output = f"[{app_name}] {message_content}"
                
                # Apply bold formatting if the sender is Discord (case-insensitive check)
                if app_name.lower() == 'discord':
                    output = f"{BOLD_START}{output}{BOLD_END}"
                
                print(output)
            
        # Log other relevant messages (like replies to GetCapabilities)
        else:
            logging.debug(f"[D-Bus Message] Type: {message.message_type.name}, Member: {message.member}, Body: {message.body}")


async def notification_listener(bus):
    """
    Configures the bus to listen for all messages related to the 
    org.freedesktop.Notifications interface.
    """
    # 1. Add the generic message handler
    bus.add_message_handler(message_handler)

    # 2. Use AddMatch to filter messages directed to this interface
    # This is crucial for catching the 'Notify' method call.
    # The rule is updated to match on the specific method call ('Notify') rather than 
    # relying solely on the destination service, which is a more robust way to capture 
    # new notification requests.
    match_rule = f"type='method_call', interface='{NOTIF_INTERFACE}', member='Notify'"
    
    await bus.call(
        Message(
            destination='org.freedesktop.DBus',
            path='/org/freedesktop/DBus',
            interface='org.freedesktop.DBus',
            member='AddMatch',
            signature='s',
            body=[match_rule]
        )
    )

    logging.info("Listening for ALL D-Bus Messages on org.freedesktop.Notifications interface.")
    logging.info("To test, send a notification, e.g., 'notify-send Hello World'")
    
    # Keep the asyncio loop running indefinitely
    await asyncio.get_running_loop().create_future()


async def main():
    """
    The main entry point for the script.
    """
    try:
        logging.info(f"Attempting to connect to the D-Bus session bus...")
        bus = await MessageBus(bus_type=BusType.SESSION).connect()
        logging.info("Successfully connected to the D-Bus session bus.")
        
        # Attempt to call the GetCapabilities method to ensure the service is running
        reply = await bus.call(
            Message(
                destination=NOTIF_SERVICE,
                path=NOTIF_PATH,
                interface=NOTIF_INTERFACE,
                member='GetCapabilities',
                signature='',
                body=[]
            )
        )
        
        if reply.message_type == MessageType.METHOD_RETURN:
            caps = reply.body[0]
            logging.info(f"Notification service capabilities retrieved: {caps}")
            await notification_listener(bus)
        else:
            # This often means no notification daemon (like dunst or a desktop environment's service) is running.
            logging.error("Could not retrieve notification service capabilities. Is a notification daemon running?")
    except Exception as e:
        logging.error(f"Error during D-Bus setup or initial capability check:   {e}")

if __name__ == "__main__":
    try:
        # Run the main coroutine
        asyncio.run(main())
    except KeyboardInterrupt:
        logging.info("Notification listener stopped by user.")
    except Exception as e:
        logging.error(f"An unexpected error occurred: {e}")

It seems like this should be a simple task.... I would rather use a bash script if I could, but python would be fine too.

I did find [this post][1] which had a python script that I had to be update to work with py3. This script runs but nothing happens when I get a notification or send one via `notify-send`

import gi
gi.require_version("Gtk", "3.0") # or "4.0" depending on your target GTK version
from gi.repository import Gtk
import dbus
from dbus.mainloop.glib import DBusGMainLoop

def filter_cb(bus, message):
    # the NameAcquired message comes through before match string gets applied
    if message.get_member() != "Notify":
        return
    args = message.get_args_list()
    # args are
    # (app_name, notification_id, icon, summary, body, actions, hints, timeout)
    print("Notification from app '%s'" % args[0])
    print("Summary: %s" % args[3])
    print("Body: %s", args[4])


DBusGMainLoop(set_as_default=True)
bus = dbus.SessionBus()
bus.add_match_string(
    "type='method_call',interface='org.freedesktop.Notifications',member='Notify'")
bus.add_message_filter(filter_cb)
Gtk.main()

Thanks.

1
  • Regarding below answer, I want to read the messages that are directed to me to the command line. Specifically my discord notifications. Commented Dec 4 at 17:11

2 Answers 2

4

After some tinkering the solution was rather simple. I was using dbus-monitor --session, which for some reason didn't include my messages. Simply running dbus-monitor without any arguments prints all dbus information including desktop notifications. So I was close, but processing an excessve ammount of data.... then I found the right command:

dbus-monitor --session "interface='org.freedesktop.Notifications',member='Notify'"

With the above command, it is possible to to get just desktop notifications and do something with it. In my example am looking for a notification that says "@[Game Update]".

#!/bin/bash
while IFS= read -r line; do
  if grep -q '@\[Game Update\]' <<< $line; then 
      echo $line
      #add fancy command here 
  fi
done < <(dbus-monitor --session "interface='org.freedesktop.Notifications',member='Notify'")

Super easy actually.

Sign up to request clarification or add additional context in comments.

2 Comments

define a regex: regex='@\[Game Update]' and change the conditional to if [[ "$line" =~ ${regex} ]] (in this case do not wrap ${regex} in double quotes); this eliminates the repeated calls to the grep binary; if you want to keep the grep call then consider piping the dbus-monitor output to a single grep call (eg, done < <(dbus-monitor ... | grep ...) which reduces the number of lines processed by the while/read to just the desired line(s)
when I tried to use grep before the loop, I would get no output.... like grep needs to finish before it prints maybe? I am going to implement the regex... that seems way more efficient
-1

Simply doing AddMatch() is not enough to make D-Bus send you messages that were not addressed to you. The method_call message that invokes Notify() is unicast – sent directly to a specific bus peer – and only that bus peer will receive it. The purpose of AddMatch() is mainly to filter broadcast messages, i.e. signals.

There are two ways to snoop on messages meant for other services:

  1. Monitor mode. In current versions, dbus-monitor issues a special BecomeMonitor() method call (to the bus daemon itself) to switch its connection from "normal" to "monitoring" mode:

    destination = org.freedesktop.DBus
    
    path = /org/freedesktop/DBus
    interface = org.freedesktop.DBus.Monitoring
    member = BecomeMonitor
    

    You can specify a filter as parameter for the call. See the D-Bus specification for more details.

    Note that once the connection is in monitor mode, it cannot make any further calls. So depending on what your script is planned to do upon matching a notification, it may need two separate connections.

  2. Match rule with eavesdrop=true. In old versions, dbus-monitor used to use plain AddMatch() with this additional parameter. I think this feature still exists, but it was a bit wonky and was formally deprecated 8 years ago.

    See the D-Bus specification for more details.

5 Comments

I am looking to simply read my desktop notifications that I get from Discord app and print them to the terminal. The messages are 100% meant for me. Perhaps my question was unclear. I have updated and added a screenshot for clarity. Thanks.
The messages aren't meant for "you". They're meant for the notification daemon process. The popup windows that the notification daemon process might be meant for you, but D-Bus does not care about that; it cares about D-Bus message delivery to the notification daemon.
So can I read these messages from a script or not? I am sorry, I am still totally confused.
I even provided two ways for doing so.
sorry.... I don't see any solution in your answer. I have added the answer I finally came up with.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.