notify-broadcast 0.0.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,5 @@
1
+ from .dbussessionmanager import DBUSSessionManager
2
+
3
+ from .notifybroadcastargumentparser import NotifyBroadcastArgumentParser
4
+
5
+ from .notify_broadcast import notify_broadcast
@@ -0,0 +1,121 @@
1
+ """
2
+ This module implements the DBUSSessionManager class which finds all user DBUS sessions and maps their NotificationProxy to allow broadcast of
3
+ notifications to all users with a DBUS session
4
+ """
5
+ # Import System Libraries
6
+ import os
7
+ import psutil
8
+ import re
9
+ import logging
10
+
11
+ # Import dasbus library modules
12
+ from dasbus.connection import SystemMessageBus, AddressedMessageBus
13
+ from dasbus.client.proxy import InterfaceProxy
14
+
15
+
16
+ class DBUSSessionManager:
17
+ class DBUSSessionNotFound(Exception):
18
+ """ This exception is raised when we cannot find the DBUS session for the nominated user and session type """
19
+ pass
20
+
21
+ def __init__(self, log_level: str):
22
+ """
23
+ Initialise class and internal variables:
24
+ - Store UID of current user so we can change EUID during running
25
+ - Populate __users mapping uid to a DBUS InterfaceProxy that we can use to notify that user
26
+
27
+ :param log_level: Level to set for the internal class logger
28
+ """
29
+ self.__log = logging.getLogger('DBUSSessionManager')
30
+ if log_level is not None:
31
+ self.__log.setLevel(log_level)
32
+ else:
33
+ self.__log.debug('using default log level')
34
+ self.__log.info(f'Constructing Class')
35
+
36
+ self.__app_uid = os.geteuid()
37
+ self.__log.debug(f'Storing UID of user running application: {self.__app_uid}')
38
+
39
+ self.__users: dict[int, InterfaceProxy] = {}
40
+ self.__find_users()
41
+ self.__log.debug(f'User DBUS sessions: {self.__users}')
42
+
43
+ def __get_notify_proxy(self, uid: int, name: str) -> InterfaceProxy:
44
+ """
45
+ Return a DBUS Notification Proxy for the given user, running the graphical session from the provided process name
46
+
47
+ :param uid: User ID for a given session
48
+ :param name: Name of the program to search for
49
+
50
+ :return: The DBUS Notification Proxy to send notifications to the user via the attached session
51
+ """
52
+ try:
53
+ # 1) Find the process ID for program "name" owned by self.__uid
54
+ self.__log.debug(f'Searching for process "{name}"')
55
+ manager_pid = [p.pid for p in psutil.process_iter(['name', 'pid', 'uids']) if p.info['name'] == name and p.info['uids'].real == uid][0]
56
+ self.__log.debug(f'Found Process ID: {manager_pid}')
57
+
58
+ # 2) Search the environment for manager_pid, and extract the DBUS session address
59
+ self.__log.debug('Reading Process environment')
60
+ with open(f'/proc/{manager_pid}/environ', 'rb') as f:
61
+ env_bytes = f.read()
62
+
63
+ # Regex pattern to locate DBUS addresses inside environment blocks
64
+ self.__log.debug('Searching for DBUS Session Bus Address')
65
+ dbus_pattern = re.compile(b"DBUS_SESSION_BUS_ADDRESS=(unix:path=[^\\x00]+)")
66
+ dbus_match = dbus_pattern.search(env_bytes)
67
+
68
+ if not dbus_match:
69
+ raise DBUSSessionManager.DBUSSessionNotFound(f'No DBUS Session Address found in environment for process ID ({manager_pid})')
70
+
71
+ dbus_path = dbus_match.group(1).decode('utf-8')
72
+ self.__log.debug(f'DBUS Session Path: {dbus_path}')
73
+
74
+ # 3) Create and return Notifications Proxy for the nominated path
75
+ self.__log.debug(f'Creating Notification Proxy')
76
+ return AddressedMessageBus(dbus_path).get_proxy("org.freedesktop.Notifications", "/org/freedesktop/Notifications")
77
+
78
+ except IndexError:
79
+ raise DBUSSessionManager.DBUSSessionNotFound(f'Unable to locate process "{name}"')
80
+
81
+ except (IOError, PermissionError):
82
+ raise DBUSSessionManager.DBUSSessionNotFound(f'Unable to access environment variables for process ID ({manager_pid}) - Need to run as root or user with appropriate permissions')
83
+
84
+ def __find_users(self):
85
+ """
86
+ Find all users with a login session, locate if they have a DBUS session attached to it, then populate __users with that information
87
+ """
88
+ self.__log.info('Finding DBUS sessions for all users')
89
+ bus = SystemMessageBus()
90
+ login_bus = bus.get_proxy("org.freedesktop.login1", "/org/freedesktop/login1")
91
+
92
+ for session_id, uid, username, seat, path in login_bus.ListSessions():
93
+ # Get session details for this login
94
+ session_proxy = bus.get_proxy("org.freedesktop.login1", path)
95
+ if session_proxy.Type in ['wayland', 'x11']:
96
+ try:
97
+ self.__log.info(f'Login session {session_id}: User [{username}({uid})] on seat[{seat}] is a {session_proxy.Type} session')
98
+ self.__users[uid] = self.__get_notify_proxy(uid, f'kwin_{session_proxy.Type}')
99
+ except DBUSSessionManager.DBUSSessionNotFound as e:
100
+ self.__log.warning(e)
101
+ else:
102
+ self.__log.info(f'Non-graphical login session {session_id}: User [{username}({uid})]')
103
+
104
+ def broadcast_notification(self, notification: tuple, print_id: bool):
105
+ """
106
+ Broadcast a notification to all users using the DBUS Notification Proxies as listed in __users
107
+
108
+ :param notification: Tuple containing parameters to pass to the Notify() method of the Notification Proxy
109
+ :param print_id: Boolean indicating whether the message ID should be printed to screen
110
+ """
111
+ self.__log.info(f'Sending notification ({notification}) to all users')
112
+ for uid, notify_proxy in self.__users.items():
113
+ self.__log.debug(f'Changing UID to {uid} to send notification')
114
+ os.seteuid(uid)
115
+
116
+ self.__log.info(f'User({uid}): Sending Notification')
117
+ notify_id = notify_proxy.Notify(*notification)
118
+ if print_id: print(notify_id)
119
+
120
+ self.__log.debug(f'Reverting UID to {self.__app_uid}')
121
+ os.seteuid(self.__app_uid)
@@ -0,0 +1,28 @@
1
+ """
2
+ This module implements the notify-broadcast application. The application is installed as a script which runs the notify_broadcast()
3
+ function contained within this file
4
+ """
5
+ # Import System Libraries
6
+ import colorlog
7
+
8
+ # Import Package Modules
9
+ from notify_broadcast import NotifyBroadcastArgumentParser
10
+ from notify_broadcast import DBUSSessionManager
11
+
12
+
13
+ def notify_broadcast():
14
+ """
15
+ This is the main application which will be called directly when running the installed notify-broadcast application
16
+ """
17
+ # Create the command line argument parser and parse all arguments
18
+ parser = NotifyBroadcastArgumentParser()
19
+ parser.parse_args()
20
+
21
+ # Set the default logging format
22
+ colorlog.basicConfig(format='%(log_color)s[%(levelname)-8s] %(reset)s%(name)s.%(funcName)s() - %(log_color)s%(message)s%(reset)s',
23
+ log_colors={'DEBUG': 'cyan', 'INFO': 'green', 'WARNING': 'yellow', 'ERROR': 'red', 'CRITICAL': 'red,bg_white'}
24
+ )
25
+
26
+ # Create the DBUSSessionManager, then broadcast the notification to all users
27
+ dbus_manager = DBUSSessionManager(parser.log_level)
28
+ dbus_manager.broadcast_notification(parser.notification, parser.print_id)
@@ -0,0 +1,321 @@
1
+ """
2
+ This module implements the NotifyBroadcastArgumentParser class which provides the command-line argument parser for the notify-broadcast command
3
+ """
4
+ # Import System Libraries
5
+ import argparse
6
+ import logging
7
+ from gi.repository import GLib
8
+
9
+
10
+ class NotifyBroadcastArgumentParser(argparse.ArgumentParser):
11
+ """
12
+ Extends ArgumentParser to provide parameter parsing for the notify-broadcast command.
13
+
14
+ Installs arguments similar to notify-send
15
+ """
16
+ class NotifyHints(argparse.Action):
17
+ """
18
+ Process one or more command line options in form TYPE:NAME:VALUE and appends a dictionary value mapping NAME -> VALUE cast to the
19
+ specified TYPE
20
+
21
+ NOTE: This class is designed to be used for validating a formatted parameter in the context of command-line argument parsing.
22
+ When an instance of this class is called with a string, it checks if the string conforms to the appropriate format specification.
23
+ If the provided string is in error, it raises an error suitable for argument parsing utilities.
24
+ """
25
+ def __init__(self, option_strings, dest, nargs=None, **kwargs):
26
+ """Initialise argument to empty dictionary"""
27
+ kwargs.setdefault('default', {})
28
+ super().__init__(option_strings, dest, **kwargs)
29
+
30
+ def __call__(self, parser, namespace, values, option_string=None):
31
+ """
32
+ Parse command line option as TYPE:NAME:VALUE and append to dictionary mapping name-->value(cast to type)
33
+
34
+ :param parser: ArgParse instance, used to send errors back to the parser.
35
+ :param namespace: Current parsed parameters.
36
+ :param values: Current option being parsed.
37
+ :param option_string: Actual option (e.g. --hint)
38
+ """
39
+ # Get the pre-existing parameter dictionary
40
+ current_values = getattr(namespace, self.dest)
41
+
42
+ # Split parameter to TYPE:NAME:VALUE tuple and add to dictionary
43
+ try:
44
+ parts = values.split(':', 2)
45
+ if len(parts) != 3: raise ValueError(f'Parameter should contain three values in format TYPE:NAME:VALUE, only {len(parts)} values provided')
46
+
47
+ raw_type, name, raw_value = parts
48
+
49
+ match raw_type.lower():
50
+ case'int':
51
+ variant_val = GLib.Variant('i', int(raw_value))
52
+ case 'double':
53
+ variant_val = GLib.Variant('d', float(raw_value))
54
+ case 'boolean' | 'bool':
55
+ variant_val = GLib.Variant('b', raw_value.lower() in ('true', '1', 'yes'))
56
+ case 'byte':
57
+ variant_val = GLib.Variant('y', int(raw_value))
58
+ case _: # Default to string
59
+ variant_val = GLib.Variant('s', str(raw_value))
60
+
61
+ current_values[name] = variant_val
62
+
63
+ except ValueError as e:
64
+ parser.error(f'Invalid parameter "{option_string} {values}" is invalid: {e}')
65
+
66
+ # Save dictionary back to namespace
67
+ setattr(namespace, self.dest, current_values)
68
+
69
+ class NotifyUrgency(argparse.Action):
70
+ """
71
+ Process command line options as human-readable "urgency" values and map directly into hints directory as corresponding byte values
72
+ under the name "urgency"
73
+
74
+ NOTE: String values should be checked by argparse, so no errors should be raised here, merely convert string->byte and append to
75
+ dictionary
76
+ """
77
+ def __init__(self, option_strings, dest, **kwargs):
78
+ """Initialise argument to empty dictionary"""
79
+ kwargs.setdefault('default', {})
80
+ super().__init__(option_strings, dest, **kwargs)
81
+
82
+ def __call__(self, parser, namespace, values, option_string=None):
83
+ """
84
+ Parse command line option as low, normal, or critical, and append to dictionary with corresponding byte value under key name 'urgency'
85
+
86
+ :param parser: ArgParse instance, used to send errors back to the parser.
87
+ :param namespace: Current parsed parameters.
88
+ :param values: Current option being parsed.
89
+ :param option_string: Actual option (e.g. --urgency)
90
+ """
91
+ # Get the pre-existing parameter dictionary
92
+ current_values = getattr(namespace, self.dest)
93
+
94
+ urgency_mapping = {'low': 0, 'normal': 1, 'critical': 2}
95
+
96
+ # Inject the key directly into the dictionary as a D-Bus byte variant
97
+ current_values['urgency'] = GLib.Variant('y', urgency_mapping[values])
98
+
99
+ # Save dictionary back to namespace
100
+ setattr(namespace, self.dest, current_values)
101
+
102
+ class NotifyCategory(argparse.Action):
103
+ """
104
+ Process command line options as string "category" value and map directly into hints directory as corresponding string under the
105
+ name "category"
106
+
107
+ NOTE: String value provided already, so no errors should be raised here, merely append string to dictionary
108
+ """
109
+ def __init__(self, option_strings, dest, **kwargs):
110
+ """Initialise argument to empty dictionary"""
111
+ kwargs.setdefault('default', {})
112
+ super().__init__(option_strings, dest, **kwargs)
113
+
114
+ def __call__(self, parser, namespace, values, option_string=None):
115
+ """
116
+ Parse command line option as string, and append to dictionary with corresponding string value under key name 'category'
117
+
118
+ :param parser: ArgParse instance, used to send errors back to the parser.
119
+ :param namespace: Current parsed parameters.
120
+ :param values: Current option being parsed.
121
+ :param option_string: Actual option (e.g. --option)
122
+ """
123
+ # Get the pre-existing parameter dictionary
124
+ current_values = getattr(namespace, self.dest)
125
+
126
+ # Inject the key directly into the dictionary as a D-Bus string variant
127
+ current_values['category'] = GLib.Variant('s', str(values))
128
+
129
+ # Save dictionary back to namespace
130
+ setattr(namespace, self.dest, current_values)
131
+
132
+ class NotifyTransient(argparse.Action):
133
+ """
134
+ Process command line flag to activate transient feature. Map directory into hints dictionary as "transient"->True
135
+
136
+ NOTE: Simple parameter flag, so no errors should be raised here, merely append True to dictionary
137
+ """
138
+ def __init__(self, option_strings, dest, **kwargs):
139
+ """Initialise argument to empty dictionary"""
140
+ kwargs.setdefault('default', {})
141
+ super().__init__(option_strings, dest, **kwargs)
142
+
143
+ def __call__(self, parser, namespace, values, option_string=None):
144
+ """
145
+ Transient should be a flag on the command line, so this action is called to set flag to True. Append True value to dictionary
146
+ under key name 'transient'
147
+
148
+ :param parser: ArgParse instance, used to send errors back to the parser.
149
+ :param namespace: Current parsed parameters.
150
+ :param values: Current option being parsed.
151
+ :param option_string: Actual option (e.g. --option)
152
+ """
153
+ # Get the pre-existing parameter dictionary
154
+ current_values = getattr(namespace, self.dest)
155
+
156
+ # Inject the key directly into the dictionary as a D-Bus byte variant
157
+ current_values['transient'] = GLib.Variant('b', True)
158
+
159
+ # Save dictionary back to namespace
160
+ setattr(namespace, self.dest, current_values)
161
+
162
+ class NotifyAction(argparse.Action):
163
+ """
164
+ Process one or more command line options in form KEY=LABEL or KEY:LABEL and append them to a flat "actions" list
165
+
166
+ NOTE: This class is designed to be used for validating a formatted parameter in the context of command-line argument parsing.
167
+ When an instance of this class is called with a string, it checks if the string conforms to the appropriate format specification.
168
+ If the provided string is in error, it raises an error suitable for argument parsing utilities.
169
+ """
170
+ def __init__(self, option_strings, dest, nargs=None, **kwargs):
171
+ """Initialise argument to empty list"""
172
+ kwargs.setdefault('default', [])
173
+ super().__init__(option_strings, dest, **kwargs)
174
+
175
+ def __call__(self, parser, namespace, values, option_string=None):
176
+ """
177
+ Parse command line option as KEY=LABEL or KEY:LABEL and extend the existing list with two new entries [KEY, LABEL]
178
+
179
+ :param parser: ArgParse instance, used to send errors back to the parser.
180
+ :param namespace: Current parsed parameters.
181
+ :param values: Current option being parsed.
182
+ :param option_string: Actual option (e.g. --hint)
183
+ """
184
+ # Get the pre-existing parameter list
185
+ actions_list = getattr(namespace, self.dest)
186
+
187
+ # Allow splitting by either ':' or ',' to match common notify-send clones
188
+ delimiter = '=' if '=' in values else ':'
189
+
190
+ try:
191
+ parts = values.split(delimiter, 1)
192
+ if len(parts) != 2: raise ValueError(f'Must be in "KEY=LABEL" or "KEY:LABEL" format')
193
+
194
+ key, label = parts[0].strip(), parts[1].strip()
195
+ if not key: raise ValueError('Key cannot be an empty string')
196
+ if not label: raise ValueError('Label cannot be an empty string')
197
+
198
+ # D-Bus expects a flat list: [key1, label1, key2, label2]
199
+ actions_list.extend([key, label])
200
+
201
+ except Exception as e:
202
+ parser.error(f'Invalid parameter "{option_string} {values}" is invalid: {e}')
203
+
204
+ # Save dictionary back to namespace
205
+ setattr(namespace, self.dest, actions_list)
206
+
207
+ class SetGlobalLogLevel(argparse.Action):
208
+ """
209
+ Process the global-log-level parameter and set the default system log level as an action
210
+
211
+ NOTE: This class is designed to be used for validating a formatted parameter in the context of command-line argument parsing.
212
+ When an instance of this class is called with a string, it checks if the string conforms to the appropriate format specification.
213
+ If the provided string is in error, it raises an error suitable for argument parsing utilities.
214
+ """
215
+ def __call__(self, parser, namespace, values, option_string=None):
216
+ """
217
+ Parse command line option as DEBUG, INFO, WARNING, ERROR, or CRITICAL, then set the logging log-level to match
218
+
219
+ :param parser: ArgParse instance, used to send errors back to the parser.
220
+ :param namespace: Current parsed parameters.
221
+ :param values: Current option being parsed.
222
+ :param option_string: Actual option (e.g. --urgency)
223
+ """
224
+ logging.basicConfig(level=values)
225
+
226
+ # Save the value to the namespace for standard argparse behavior
227
+ setattr(namespace, self.dest, values)
228
+
229
+ # NotifyBroadcastArgumentParser member methods begin here
230
+ def __init__(self, *args, **kwargs):
231
+ """
232
+ Overloaded Constructor
233
+
234
+ Set default description and argparse parameters before calling superclass constructor
235
+
236
+ Then call add_arguments() to add the required command-line parameters
237
+ """
238
+ # Default options
239
+ kwargs.setdefault('description', 'Notify Broadcast\n\nSend a Broadcast DBUS notification to all users')
240
+ kwargs.setdefault('formatter_class', argparse.ArgumentDefaultsHelpFormatter)
241
+ kwargs.setdefault('allow_abbrev', False)
242
+ kwargs.setdefault('conflict_handler', 'resolve')
243
+
244
+ # Initialise the parent class
245
+ super().__init__(*args, **kwargs)
246
+
247
+ # Add parameter options
248
+ self.__add_arguments()
249
+
250
+ # Create variable to store generated notification from parsed arguments
251
+ self.__notification = None
252
+ self.__print_id = None
253
+ self.__log_level = None
254
+
255
+ def __add_arguments(self):
256
+ """ Add command line arguments to the argument parser """
257
+ self.add_argument('-a', '--app-name', type=str, default='', help='Specifies the app name for the notification')
258
+ self.add_argument('-i', '--icon', type=str, default='dialog-information', help='Specifies an icon filename or stock icon to display.')
259
+ self.add_argument('-t', '--expire-time', type=int, default=-1, help='The duration, in milliseconds, for the notification to appear on screen. Value of 0 means no expiry, while -1 uses the server default expiry.')
260
+ self.add_argument('-h', '--hint', action=NotifyBroadcastArgumentParser.NotifyHints, metavar='TYPE:NAME:VALUE', help='Notification hints to pass to server (e.g., int:urgency:2)')
261
+ self.add_argument('-c', '--category', action=NotifyBroadcastArgumentParser.NotifyCategory, metavar='TYPE', dest='hint', help='Specifies the notification category.')
262
+ self.add_argument('-u', '--urgency', action=NotifyBroadcastArgumentParser.NotifyUrgency, choices=['low', 'normal', 'critical'], dest='hint', help='Specifies the urgency level (low, normal, critical).')
263
+
264
+ self.add_argument('-A', '--action', action=NotifyBroadcastArgumentParser.NotifyAction, metavar='NAME=VALUE', help='Specifies the actions to display to the user. Implies --wait to wait for user input. May be set multiple times. The NAME of the action is output to stdout. If NAME is not specified, the numerical index of the option is used (starting with 1).')
265
+
266
+ self.add_argument('-r', '--replace-id', type=int, default=0, help='The ID of the notification to replace.')
267
+ self.add_argument('-p', '--print-id', action='store_true', help='Print the notification ID.')
268
+
269
+ self.add_argument('-e', '--transient', action=NotifyBroadcastArgumentParser.NotifyTransient, dest='hint', help='Show a transient notification. Transient notifications by-pass the server\'s persistence capability, if any. And so it won\'t be preserved until the user acknowledges it.')
270
+
271
+ self.add_argument('summary', type=str, help='exam configuration file to be used for student collection - toml format')
272
+ self.add_argument('body', type=str, help='exam solution file to be placed in collection directory')
273
+
274
+ self.add_argument("--log-level", choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"], help="Set the logging level for the core application.")
275
+
276
+ self.add_argument("--global-log-level", action=NotifyBroadcastArgumentParser.SetGlobalLogLevel, choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"], help="Set the global logging level (includes third-party libraries).")
277
+
278
+ def parse_args(self, args=None, namespace=None):
279
+ """
280
+ Overload parse_args method. Constructs the notification details (stored in __notification) from the parsed command line arguments.
281
+ - Call the base class method to parse the arguments and store in temporary variable
282
+ - Extract parameters into a tuple (ready to pass to Notify()) and store in __notification
283
+ - Extract other variables to make directly accessible via properties
284
+
285
+ :return: Return the parsed arguments Namespace as required by parse_args()
286
+ """
287
+ # Call base class method to parse arguments and store in internal variable
288
+ parsed = super().parse_args(args=args, namespace=namespace)
289
+
290
+ self.__notification = (parsed.app_name, parsed.replace_id, parsed.icon, parsed.summary, parsed.body, parsed.action, parsed.hint, parsed.expire_time)
291
+ self.__print_id = parsed.print_id
292
+ self.__log_level = parsed.log_level
293
+
294
+ return parsed
295
+
296
+ @property
297
+ def notification(self) -> tuple:
298
+ """
299
+ Retrieves the notification constructed from the command line arguments as a class property.
300
+
301
+ :return: The value stored in internal variable __notification
302
+ """
303
+ return self.__notification
304
+
305
+ @property
306
+ def print_id(self) -> bool:
307
+ """
308
+ Retrieves the print_id from the command line arguments as a class property.
309
+
310
+ :return: The value stored in internal variable __print_id
311
+ """
312
+ return self.__print_id
313
+
314
+ @property
315
+ def log_level(self) -> str:
316
+ """
317
+ Retrieves the log_level from the command line arguments as a class property.
318
+
319
+ :return: The value stored in internal variable __log_level
320
+ """
321
+ return self.__log_level
@@ -0,0 +1,170 @@
1
+ Metadata-Version: 2.4
2
+ Name: notify-broadcast
3
+ Version: 0.0.3
4
+ Summary: Broadcast version of notify-send to allow root processes to send a notification to all users with an active DBUS session
5
+ Author-email: Jason But <jbut@swin.edu.au>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/jason-but/notify-broadcast
8
+ Project-URL: Issues, https://github.com/jason-but/notify-broadcast/issues
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Operating System :: OS Independent
11
+ Requires-Python: >=3.11
12
+ Description-Content-Type: text/markdown
13
+ License-File: LICENSE
14
+ Requires-Dist: colorlog
15
+ Requires-Dist: dasbus
16
+ Requires-Dist: psutil
17
+ Requires-Dist: pygobject
18
+ Dynamic: license-file
19
+
20
+ # notify-broadcast
21
+
22
+ This program was developed because I run a number of **cron** jobs as root that I would like to be
23
+ able to send notifications to the GUI User
24
+
25
+ `notify-send` only functions if run by the same user running the graphical session.
26
+
27
+ I created `notify-broadcast` to effectively tack the same parameters as `notify-send` but that
28
+ could be executed by the `root` user.
29
+
30
+ The application searches for all active DBUS Notification sessions, and sends the notification to
31
+ all currently attached users.
32
+
33
+ ## Installation
34
+
35
+ When complete, should be:
36
+
37
+ ```console
38
+ pip install notify-broadcast
39
+ ```
40
+
41
+ Seek information elsewhere about installing in a virtual environment
42
+
43
+ The install should pull in all dependencies. At present these are:
44
+ - colorlog: https://github.com/borntyping/python-colorlog/
45
+ - dasbus: https://github.com/dasbus-project/dasbus
46
+ - psutil: https://github.com/giampaolo/psutil
47
+ - PyGObject: https://pygobject.gnome.org
48
+
49
+ ## Usage
50
+
51
+ ```console
52
+ # notify-broadcast --help
53
+ usage: notify-broadcast [--help] [-a APP_NAME] [-i ICON] [-t EXPIRE_TIME] [-h TYPE:NAME:VALUE] [-c TYPE] [-u {low,normal,critical}] [-A NAME=VALUE] [-r REPLACE_ID] [-p] [-e HINT] [--log-level {DEBUG,INFO,WARNING,ERROR,CRITICAL}]
54
+ [--global-log-level {DEBUG,INFO,WARNING,ERROR,CRITICAL}]
55
+ summary body
56
+
57
+ Notify Broadcast Send a Broadcast DBUS notification to all users
58
+
59
+ positional arguments:
60
+ summary exam configuration file to be used for student collection - toml format
61
+ body exam solution file to be placed in collection directory
62
+
63
+ options:
64
+ --help show this help message and exit
65
+ -a APP_NAME, --app-name APP_NAME
66
+ Specifies the app name for the notification (default: )
67
+ -i ICON, --icon ICON Specifies an icon filename or stock icon to display. (default: dialog-information)
68
+ -t EXPIRE_TIME, --expire-time EXPIRE_TIME
69
+ The duration, in milliseconds, for the notification to appear on screen. Value of 0 means no expiry, while -1 uses the server default expiry. (default: -1)
70
+ -h TYPE:NAME:VALUE, --hint TYPE:NAME:VALUE
71
+ Notification hints to pass to server (e.g., int:urgency:2) (default: {})
72
+ -c TYPE, --category TYPE
73
+ Specifies the notification category. (default: {})
74
+ -u {low,normal,critical}, --urgency {low,normal,critical}
75
+ Specifies the urgency level (low, normal, critical). (default: {})
76
+ -A NAME=VALUE, --action NAME=VALUE
77
+ Specifies the actions to display to the user. Implies --wait to wait for user input. May be set multiple times. The NAME of the action is output to stdout. If NAME is not specified, the numerical index of the
78
+ option is used (starting with 1). (default: [])
79
+ -r REPLACE_ID, --replace-id REPLACE_ID
80
+ The ID of the notification to replace. (default: 0)
81
+ -p, --print-id Print the notification ID. (default: False)
82
+ -e HINT, --transient HINT
83
+ Show a transient notification. Transient notifications by-pass the server's persistence capability, if any. And so it won't be preserved until the user acknowledges it. (default: {})
84
+ --log-level {DEBUG,INFO,WARNING,ERROR,CRITICAL}
85
+ Set the logging level for the core application. (default: None)
86
+ --global-log-level {DEBUG,INFO,WARNING,ERROR,CRITICAL}
87
+ Set the global logging level (includes third-party libraries). (default: None)
88
+ ```
89
+
90
+ **NOTE:** For more support on these parameters, see `notify-send` man pages
91
+
92
+ ## Examples
93
+
94
+ Some examples of how to use `notify-broadcast` are listed below. These are by no means exhaustive:
95
+
96
+ `notify-broadcast -a Daily-backup -t 0 -i dialog-information.png "Backup completed without error" ""`
97
+
98
+ Display a notification that the backup has completed without error:
99
+ - Display an information icon
100
+ - A timeout of 0 signifies that the notification will display until the user clears it
101
+ - Message contains a summary, but no body
102
+
103
+ `notify-broadcast -a Remote-rsync -t 6000 -i dialog-warning.png "Remote host not currently on the network" ""`
104
+
105
+ Display a notification that the remote host is not available:
106
+ - Display a warning icon
107
+ - A timeout of 6000 signifies that the notification will display for six seconds before clearing
108
+ - Message contains a summary, but no body
109
+
110
+ `notify-broadcast -a Daily-backup -t 0 -i dialog-error.png "Error running backup, please consult logs" ""`
111
+
112
+ Display a notification that the backup has completed without error:
113
+ - Display an error icon
114
+ - A timeout of 0 signifies that the notification will display until the user clears it
115
+ - Message contains a summary, but no body
116
+
117
+ `notify-broadcast -a "Disk Monitor" -h string:desktop-entry:org.kde.kinfocenter-i drive-harddisk "Disk" "SMART warning"`
118
+
119
+ Display a notification that a disk has encountered a SMART error:
120
+ - Display an disk icon
121
+ - No timeout signifies that the notification will display for the system default duration
122
+
123
+ ## Comments
124
+
125
+ I am aware of a number of potential shortcomings that may impact broader distribution, as well as
126
+ some points about why things were coded this way, details listed below
127
+
128
+ ### Finding DBUS Path via environment variables instead of `/run/user/{uid}/bus`
129
+
130
+ The code searches for the `DBUS_SESSION_BUS_ADDRESS` environment variable in a running program,
131
+ while many online examples suggest searching `/run/user/{uid}/bus`
132
+
133
+ My system (Gentoo) does not place the DBUS sockets in that location, so searching the environment
134
+ variables allows this to work regardless of the location of the socket.
135
+
136
+ ### Program is hard-coded to KDE
137
+
138
+ As per the previous point, to search the environment variables, it means finding a running
139
+ application that has the environment variables set. This means that we need to know the application
140
+ name to search for.
141
+
142
+ Hence, the program currently looks for running instances of `kwin_wayland` or `kwin_x11` depending
143
+ on the current session type.
144
+
145
+ This works for me as I use KDE, I don't like Gnome or other environments.
146
+
147
+ However, I realise this means that this will not work everywhere. Some chat online suggests looking
148
+ for `dbus-launch`, however the environment for this process does not appear to contain the
149
+ `DBUS_SESSION_BUS_ADDRESS` environment variable.
150
+
151
+ I would like to support alternate desktops, but I do not have the will to test and develop a
152
+ solution. I am happy to take comments/suggestions on how to detect across multiple platforms.
153
+
154
+ ### Some of the Options are Useless for Broadcast application
155
+
156
+ As a broadcast application, this really makes sense as a 1) send a message; and 2) do not wait for
157
+ replies scenario
158
+
159
+ 1. `--print-id` makes no sense if sending multiple notifications
160
+ 2. `--replace-id` make no sense if we are just blasting information to everyone
161
+ 3. `--action` display buttons to the user and return values to the program. This is generally useless for this application
162
+
163
+ ### Running as non-root
164
+
165
+ A non-root user will not be able to post notifications to other users in either case. The
166
+ application currently just prints warnings about being unable to access the environment and
167
+ does nothing else.
168
+
169
+ It might be better to abort early if the user does not have permissions, but a system could
170
+ allow multiple users the requisite permissions, so it is hard to manage this properly.
@@ -0,0 +1,10 @@
1
+ notify_broadcast/__init__.py,sha256=0z1DAuzakE1Z7fDamsUEWdSe2yHw0MwFmKhIbYujMlw,173
2
+ notify_broadcast/dbussessionmanager.py,sha256=sgE7xYrsQzCwVXtiQlcCs29QT4b-Owovq7YyNEucaN8,5836
3
+ notify_broadcast/notify_broadcast.py,sha256=3RNk9CiM0ZIkhtOAR950E4PV0egYips_Fmi51p6ICbc,1204
4
+ notify_broadcast/notifybroadcastargumentparser.py,sha256=Mdv-UTLpAmdES0fbCdL1g1bLu-HhVFaa0iJne70RYb8,16588
5
+ notify_broadcast-0.0.3.dist-info/licenses/LICENSE,sha256=ACwmltkrXIz5VsEQcrqljq-fat6ZXAMepjXGoe40KtE,1069
6
+ notify_broadcast-0.0.3.dist-info/METADATA,sha256=wX37Vffjq_fgoUj6TGxzULyTpuaiMmyLQVXGpv8tD2I,8103
7
+ notify_broadcast-0.0.3.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
8
+ notify_broadcast-0.0.3.dist-info/entry_points.txt,sha256=-i9jO-shNjuFIlwwah9vFse7brfjKEeA-rVxbTzFXBo,88
9
+ notify_broadcast-0.0.3.dist-info/top_level.txt,sha256=oKwdORu2uwiFsO_LlamRADQh75j4vPOhOxl28nl1XYg,17
10
+ notify_broadcast-0.0.3.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ notify-broadcast = notify_broadcast.notify_broadcast:notify_broadcast
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) [year] [fullname]
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ notify_broadcast