123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411 |
- // SPDX-FileCopyrightText: GSConnect Developers https://github.com/GSConnect
- //
- // SPDX-License-Identifier: GPL-2.0-or-later
- 'use strict';
- const Gio = imports.gi.Gio;
- const GLib = imports.gi.GLib;
- const GjsPrivate = imports.gi.GjsPrivate;
- const GObject = imports.gi.GObject;
- const DBus = imports.service.utils.dbus;
- const _nodeInfo = Gio.DBusNodeInfo.new_for_xml(`
- <node>
- <interface name="org.freedesktop.Notifications">
- <method name="Notify">
- <arg name="appName" type="s" direction="in"/>
- <arg name="replacesId" type="u" direction="in"/>
- <arg name="iconName" type="s" direction="in"/>
- <arg name="summary" type="s" direction="in"/>
- <arg name="body" type="s" direction="in"/>
- <arg name="actions" type="as" direction="in"/>
- <arg name="hints" type="a{sv}" direction="in"/>
- <arg name="timeout" type="i" direction="in"/>
- </method>
- </interface>
- <interface name="org.gtk.Notifications">
- <method name="AddNotification">
- <arg type="s" direction="in"/>
- <arg type="s" direction="in"/>
- <arg type="a{sv}" direction="in"/>
- </method>
- <method name="RemoveNotification">
- <arg type="s" direction="in"/>
- <arg type="s" direction="in"/>
- </method>
- </interface>
- </node>
- `);
- const FDO_IFACE = _nodeInfo.lookup_interface('org.freedesktop.Notifications');
- const FDO_MATCH = "interface='org.freedesktop.Notifications',member='Notify',type='method_call'";
- const GTK_IFACE = _nodeInfo.lookup_interface('org.gtk.Notifications');
- const GTK_MATCH = "interface='org.gtk.Notifications',member='AddNotification',type='method_call'";
- /**
- * A class for snooping Freedesktop (libnotify) and Gtk (GNotification)
- * notifications and forwarding them to supporting devices.
- */
- const Listener = GObject.registerClass({
- GTypeName: 'GSConnectNotificationListener',
- Signals: {
- 'notification-added': {
- flags: GObject.SignalFlags.RUN_LAST,
- param_types: [GLib.Variant.$gtype],
- },
- },
- }, class Listener extends GObject.Object {
- _init() {
- super._init();
- // Respect desktop notification settings
- this._settings = new Gio.Settings({
- schema_id: 'org.gnome.desktop.notifications',
- });
- // Watch for new application policies
- this._settingsId = this._settings.connect(
- 'changed::application-children',
- this._onSettingsChanged.bind(this)
- );
- // Cache for appName->desktop-id lookups
- this._names = {};
- // Asynchronous setup
- this._init_async();
- }
- get applications() {
- if (this._applications === undefined)
- this._onSettingsChanged();
- return this._applications;
- }
- /**
- * Update application notification settings
- */
- _onSettingsChanged() {
- this._applications = {};
- for (const app of this._settings.get_strv('application-children')) {
- const appSettings = new Gio.Settings({
- schema_id: 'org.gnome.desktop.notifications.application',
- path: `/org/gnome/desktop/notifications/application/${app}/`,
- });
- const appInfo = Gio.DesktopAppInfo.new(
- appSettings.get_string('application-id')
- );
- if (appInfo !== null)
- this._applications[appInfo.get_name()] = appSettings;
- }
- }
- async _listNames() {
- const reply = await this._session.call(
- 'org.freedesktop.DBus',
- '/org/freedesktop/DBus',
- 'org.freedesktop.DBus',
- 'ListNames',
- null,
- null,
- Gio.DBusCallFlags.NONE,
- -1,
- null);
- return reply.deepUnpack()[0];
- }
- async _getNameOwner(name) {
- const reply = await this._session.call(
- 'org.freedesktop.DBus',
- '/org/freedesktop/DBus',
- 'org.freedesktop.DBus',
- 'GetNameOwner',
- new GLib.Variant('(s)', [name]),
- null,
- Gio.DBusCallFlags.NONE,
- -1,
- null);
- return reply.deepUnpack()[0];
- }
- /**
- * Try and find a well-known name for @sender on the session bus
- *
- * @param {string} sender - A DBus unique name (eg. :1.2282)
- * @param {string} appName - @appName passed to Notify() (Optional)
- * @return {string} A well-known name or %null
- */
- async _getAppId(sender, appName) {
- try {
- // Get a list of well-known names, ignoring @sender
- const names = await this._listNames();
- names.splice(names.indexOf(sender), 1);
- // Make a short list for substring matches (fractal/org.gnome.Fractal)
- const appLower = appName.toLowerCase();
- const shortList = names.filter(name => {
- return name.toLowerCase().includes(appLower);
- });
- // Run the short list first
- for (const name of shortList) {
- const nameOwner = await this._getNameOwner(name);
- if (nameOwner === sender)
- return name;
- names.splice(names.indexOf(name), 1);
- }
- // Run the full list
- for (const name of names) {
- const nameOwner = await this._getNameOwner(name);
- if (nameOwner === sender)
- return name;
- }
- return null;
- } catch (e) {
- debug(e);
- return null;
- }
- }
- /**
- * Try and find the application name for @sender
- *
- * @param {string} sender - A DBus unique name
- * @param {string} [appName] - `appName` supplied by Notify()
- * @return {string} A well-known name or %null
- */
- async _getAppName(sender, appName = null) {
- // Check the cache first
- if (appName && this._names.hasOwnProperty(appName))
- return this._names[appName];
- try {
- const appId = await this._getAppId(sender, appName);
- const appInfo = Gio.DesktopAppInfo.new(`${appId}.desktop`);
- this._names[appName] = appInfo.get_name();
- appName = appInfo.get_name();
- } catch (e) {
- // Silence errors
- }
- return appName;
- }
- /**
- * Callback for AddNotification()/Notify()
- *
- * @param {DBus.Interface} iface - The DBus interface
- * @param {string} name - The DBus method name
- * @param {GLib.Variant} parameters - The method parameters
- * @param {Gio.DBusMethodInvocation} invocation - The method invocation info
- */
- async _onHandleMethodCall(iface, name, parameters, invocation) {
- try {
- // Check if notifications are disabled in desktop settings
- if (!this._settings.get_boolean('show-banners'))
- return;
- parameters = parameters.full_unpack();
- // GNotification
- if (name === 'AddNotification') {
- this.AddNotification(...parameters);
- // libnotify
- } else if (name === 'Notify') {
- const message = invocation.get_message();
- const destination = message.get_destination();
- // Deduplicate notifications; only accept messages
- // directed to the notification bus, or its owner.
- if (destination !== 'org.freedesktop.Notifications') {
- if (this._fdoNameOwner === undefined) {
- this._fdoNameOwner = await this._getNameOwner(
- 'org.freedesktop.Notifications');
- }
- if (this._fdoNameOwner !== destination)
- return;
- }
- // Try to brute-force an application name using DBus
- if (!this.applications.hasOwnProperty(parameters[0])) {
- const sender = message.get_sender();
- parameters[0] = await this._getAppName(sender, parameters[0]);
- }
- this.Notify(...parameters);
- }
- } catch (e) {
- debug(e);
- }
- }
- /**
- * Export interfaces for proxying notifications and become a monitor
- *
- * @return {Promise} A promise for the operation
- */
- _monitorConnection() {
- // libnotify Interface
- this._fdoNotifications = new GjsPrivate.DBusImplementation({
- g_interface_info: FDO_IFACE,
- });
- this._fdoMethodCallId = this._fdoNotifications.connect(
- 'handle-method-call', this._onHandleMethodCall.bind(this));
- this._fdoNotifications.export(this._monitor,
- '/org/freedesktop/Notifications');
- this._fdoNameOwnerChangedId = this._session.signal_subscribe(
- 'org.freedesktop.DBus',
- 'org.freedesktop.DBus',
- 'NameOwnerChanged',
- '/org/freedesktop/DBus',
- 'org.freedesktop.Notifications',
- Gio.DBusSignalFlags.MATCH_ARG0_NAMESPACE,
- this._onFdoNameOwnerChanged.bind(this)
- );
- // GNotification Interface
- this._gtkNotifications = new GjsPrivate.DBusImplementation({
- g_interface_info: GTK_IFACE,
- });
- this._gtkMethodCallId = this._gtkNotifications.connect(
- 'handle-method-call', this._onHandleMethodCall.bind(this));
- this._gtkNotifications.export(this._monitor, '/org/gtk/Notifications');
- // Become a monitor for Fdo & Gtk notifications
- return this._monitor.call(
- 'org.freedesktop.DBus',
- '/org/freedesktop/DBus',
- 'org.freedesktop.DBus.Monitoring',
- 'BecomeMonitor',
- new GLib.Variant('(asu)', [[FDO_MATCH, GTK_MATCH], 0]),
- null,
- Gio.DBusCallFlags.NONE,
- -1,
- null);
- }
- async _init_async() {
- try {
- this._session = Gio.DBus.session;
- this._monitor = await DBus.newConnection();
- await this._monitorConnection();
- } catch (e) {
- const service = Gio.Application.get_default();
- if (service !== null)
- service.notify_error(e);
- else
- logError(e);
- }
- }
- _onFdoNameOwnerChanged(connection, sender, object, iface, signal, parameters) {
- this._fdoNameOwner = parameters.deepUnpack()[2];
- }
- _sendNotification(notif) {
- // Check if this application is disabled in desktop settings
- const appSettings = this.applications[notif.appName];
- if (appSettings && !appSettings.get_boolean('enable'))
- return;
- // Send the notification to each supporting device
- // TODO: avoid the overhead of the GAction framework with a signal?
- const variant = GLib.Variant.full_pack(notif);
- this.emit('notification-added', variant);
- }
- Notify(appName, replacesId, iconName, summary, body, actions, hints, timeout) {
- // Ignore notifications without an appName
- if (!appName)
- return;
- this._sendNotification({
- appName: appName,
- id: `fdo|null|${replacesId}`,
- title: summary,
- text: body,
- ticker: `${summary}: ${body}`,
- isClearable: (replacesId !== 0),
- icon: iconName,
- });
- }
- AddNotification(application, id, notification) {
- // Ignore our own notifications or we'll cause a notification loop
- if (application === 'org.gnome.Shell.Extensions.GSConnect')
- return;
- const appInfo = Gio.DesktopAppInfo.new(`${application}.desktop`);
- // Try to get an icon for the notification
- if (!notification.hasOwnProperty('icon'))
- notification.icon = appInfo.get_icon() || undefined;
- this._sendNotification({
- appName: appInfo.get_name(),
- id: `gtk|${application}|${id}`,
- title: notification.title,
- text: notification.body,
- ticker: `${notification.title}: ${notification.body}`,
- isClearable: true,
- icon: notification.icon,
- });
- }
- destroy() {
- try {
- if (this._fdoNotifications) {
- this._fdoNotifications.disconnect(this._fdoMethodCallId);
- this._fdoNotifications.unexport();
- this._session.signal_unsubscribe(this._fdoNameOwnerChangedId);
- }
- if (this._gtkNotifications) {
- this._gtkNotifications.disconnect(this._gtkMethodCallId);
- this._gtkNotifications.unexport();
- }
- if (this._settings) {
- this._settings.disconnect(this._settingsId);
- this._settings.run_dispose();
- }
- // TODO: Gio.IOErrorEnum: The connection is closed
- // this._monitor.close_sync(null);
- GObject.signal_handlers_destroy(this);
- } catch (e) {
- debug(e);
- }
- }
- });
- /**
- * The service class for this component
- */
- var Component = Listener;
|