2024-07-08 22:46:35 +02:00

226 lines
6.4 KiB
JavaScript
Executable File

#!/usr/bin/env -S gjs -m
// SPDX-FileCopyrightText: GSConnect Developers https://github.com/GSConnect
//
// SPDX-License-Identifier: GPL-2.0-or-later
import Gio from 'gi://Gio?version=2.0';
import GLib from 'gi://GLib?version=2.0';
import GObject from 'gi://GObject?version=2.0';
import system from 'system';
// Retain compatibility with GLib < 2.80, which lacks GioUnix
let GioUnix;
try {
GioUnix = (await import('gi://GioUnix?version=2.0')).default;
} catch (e) {
GioUnix = {
InputStream: Gio.UnixInputStream,
OutputStream: Gio.UnixOutputStream,
};
}
const NativeMessagingHost = GObject.registerClass({
GTypeName: 'GSConnectNativeMessagingHost',
}, class NativeMessagingHost extends Gio.Application {
_init() {
super._init({
application_id: 'org.gnome.Shell.Extensions.GSConnect.NativeMessagingHost',
flags: Gio.ApplicationFlags.NON_UNIQUE,
});
}
get devices() {
if (this._devices === undefined)
this._devices = {};
return this._devices;
}
vfunc_activate() {
super.vfunc_activate();
}
vfunc_startup() {
super.vfunc_startup();
this.hold();
// IO Channels
this._stdin = new Gio.DataInputStream({
base_stream: new GioUnix.InputStream({fd: 0}),
byte_order: Gio.DataStreamByteOrder.HOST_ENDIAN,
});
this._stdout = new Gio.DataOutputStream({
base_stream: new GioUnix.OutputStream({fd: 1}),
byte_order: Gio.DataStreamByteOrder.HOST_ENDIAN,
});
const source = this._stdin.base_stream.create_source(null);
source.set_callback(this.receive.bind(this));
source.attach(null);
// Device Manager
try {
this._manager = Gio.DBusObjectManagerClient.new_for_bus_sync(
Gio.BusType.SESSION,
Gio.DBusObjectManagerClientFlags.DO_NOT_AUTO_START,
'org.gnome.Shell.Extensions.GSConnect',
'/org/gnome/Shell/Extensions/GSConnect',
null,
null
);
} catch (e) {
logError(e);
this.quit();
}
// Add currently managed devices
for (const object of this._manager.get_objects()) {
for (const iface of object.get_interfaces())
this._onInterfaceAdded(this._manager, object, iface);
}
// Watch for new and removed devices
this._manager.connect(
'interface-added',
this._onInterfaceAdded.bind(this)
);
this._manager.connect(
'object-removed',
this._onObjectRemoved.bind(this)
);
// Watch for device property changes
this._manager.connect(
'interface-proxy-properties-changed',
this.sendDeviceList.bind(this)
);
// Watch for service restarts
this._manager.connect(
'notify::name-owner',
this.sendDeviceList.bind(this)
);
this.send({
type: 'connected',
data: (this._manager.name_owner !== null),
});
}
receive() {
try {
// Read the message
const length = this._stdin.read_int32(null);
const bytes = this._stdin.read_bytes(length, null).toArray();
const message = JSON.parse(new TextDecoder().decode(bytes));
// A request for a list of devices
if (message.type === 'devices') {
this.sendDeviceList();
// A request to invoke an action
} else if (message.type === 'share') {
let actionName;
const device = this.devices[message.data.device];
if (device) {
if (message.data.action === 'share')
actionName = 'shareUri';
else if (message.data.action === 'telephony')
actionName = 'shareSms';
device.actions.activate_action(
actionName,
new GLib.Variant('s', message.data.url)
);
}
}
return GLib.SOURCE_CONTINUE;
} catch (e) {
this.quit();
}
}
send(message) {
try {
const data = JSON.stringify(message);
this._stdout.put_int32(data.length, null);
this._stdout.put_string(data, null);
} catch (e) {
this.quit();
}
}
sendDeviceList() {
// Inform the WebExtension we're disconnected from the service
if (this._manager && this._manager.name_owner === null)
return this.send({type: 'connected', data: false});
// Collect all the devices with supported actions
const available = [];
for (const device of Object.values(this.devices)) {
const share = device.actions.get_action_enabled('shareUri');
const telephony = device.actions.get_action_enabled('shareSms');
if (share || telephony) {
available.push({
id: device.g_object_path,
name: device.name,
type: device.type,
share: share,
telephony: telephony,
});
}
}
this.send({type: 'devices', data: available});
}
_proxyGetter(name) {
try {
return this.get_cached_property(name).unpack();
} catch (e) {
return null;
}
}
_onInterfaceAdded(manager, object, iface) {
Object.defineProperties(iface, {
'name': {
get: this._proxyGetter.bind(iface, 'Name'),
enumerable: true,
},
// TODO: phase this out for icon-name
'type': {
get: this._proxyGetter.bind(iface, 'Type'),
enumerable: true,
},
});
iface.actions = Gio.DBusActionGroup.get(
iface.g_connection,
iface.g_name,
iface.g_object_path
);
this.devices[iface.g_object_path] = iface;
this.sendDeviceList();
}
_onObjectRemoved(manager, object) {
delete this.devices[object.g_object_path];
this.sendDeviceList();
}
});
// NOTE: must not pass ARGV
await (new NativeMessagingHost()).runAsync([system.programInvocationName]);