[gnome] Update extensions for version 48

This commit is contained in:
2025-03-29 17:52:56 -04:00
parent e01589e836
commit a84b79ca08
153 changed files with 3479 additions and 2189 deletions

View File

@ -8,12 +8,13 @@ import GObject from 'gi://GObject';
import Config from '../../config.js';
import * as Core from '../core.js';
import Device from '../device.js';
// Retain compatibility with GLib < 2.80, which lacks GioUnix
let GioUnix;
try {
GioUnix = (await import('gi://GioUnix')).default;
} catch (e) {
} catch {
GioUnix = {
InputStream: Gio.UnixInputStream,
OutputStream: Gio.UnixOutputStream,
@ -42,7 +43,7 @@ try {
Gio.SocketType.STREAM,
Gio.SocketProtocol.TCP
).get_option(6, 5);
} catch (e) {
} catch {
_LINUX_SOCKETS = false;
}
@ -148,6 +149,10 @@ export const ChannelService = GObject.registerClass({
return this._channels;
}
get id() {
return this.certificate.common_name;
}
get port() {
if (this._port === undefined)
this._port = PROTOCOL_PORT_DEFAULT;
@ -172,13 +177,6 @@ export const ChannelService = GObject.registerClass({
}
_initCertificate() {
if (GLib.find_program_in_path(Config.OPENSSL_PATH) === null) {
const error = new Error();
error.name = _('OpenSSL not found');
error.url = `${Config.PACKAGE_URL}/wiki/Error#openssl-not-found`;
throw error;
}
const certPath = GLib.build_filenamev([
Config.CONFIGDIR,
'certificate.pem',
@ -190,12 +188,7 @@ export const ChannelService = GObject.registerClass({
// Ensure a certificate exists with our id as the common name
this._certificate = Gio.TlsCertificate.new_for_paths(certPath, keyPath,
this.id);
// If the service ID doesn't match the common name, this is probably a
// certificate from an older version and we should amend ours to match
if (this.id !== this._certificate.common_name)
this._id = this._certificate.common_name;
null);
}
_initTcpListener() {
@ -282,7 +275,7 @@ export const ChannelService = GObject.registerClass({
this._udp6_source = this._udp6.create_source(GLib.IOCondition.IN, null);
this._udp6_source.set_callback(this._onIncomingIdentity.bind(this, this._udp6));
this._udp6_source.attach(null);
} catch (e) {
} catch {
this._udp6 = null;
}
@ -360,13 +353,24 @@ export const ChannelService = GObject.registerClass({
async _onIdentity(packet) {
try {
// Bail if the deviceId is missing
if (!packet.body.hasOwnProperty('deviceId'))
return;
if (!this.identity.body.deviceId)
throw new Error('missing deviceId');
// Silently ignore our own broadcasts
if (packet.body.deviceId === this.identity.body.deviceId)
return;
// Reject invalid device IDs
if (!Device.validateId(packet.body.deviceId))
throw new Error(`invalid deviceId "${packet.body.deviceId}"`);
if (!packet.body.deviceName)
throw new Error('missing deviceName');
// Reject invalid device names
if (!Device.validateName(packet.body.deviceName))
throw new Error(`invalid deviceName "${packet.body.deviceName}"`);
debug(packet);
// Create a new channel
@ -596,7 +600,7 @@ export const Channel = GObject.registerClass({
* Authenticate a TLS connection.
*
* @param {Gio.TlsConnection} connection - A TLS connection
* @return {Promise} A promise for the operation
* @returns {Promise} A promise for the operation
*/
async _authenticate(connection) {
// Standard TLS Handshake
@ -668,7 +672,7 @@ export const Channel = GObject.registerClass({
* Wrap the connection in Gio.TlsClientConnection and initiate handshake
*
* @param {Gio.TcpConnection} connection - The unauthenticated connection
* @return {Gio.TlsClientConnection} The authenticated connection
* @returns {Gio.TlsClientConnection} The authenticated connection
*/
_encryptClient(connection) {
_configureSocket(connection);
@ -684,7 +688,7 @@ export const Channel = GObject.registerClass({
* Wrap the connection in Gio.TlsServerConnection and initiate handshake
*
* @param {Gio.TcpConnection} connection - The unauthenticated connection
* @return {Gio.TlsServerConnection} The authenticated connection
* @returns {Gio.TlsServerConnection} The authenticated connection
*/
_encryptServer(connection) {
_configureSocket(connection);
@ -729,7 +733,25 @@ export const Channel = GObject.registerClass({
if (!this.identity.body.deviceId)
throw new Error('missing deviceId');
// Reject invalid device IDs
if (!Device.validateId(this.identity.body.deviceId))
throw new Error(`invalid deviceId "${this.identity.body.deviceId}"`);
if (!this.identity.body.deviceName)
throw new Error('missing deviceName');
// Reject invalid device names
if (!Device.validateName(this.identity.body.deviceName))
throw new Error(`invalid deviceName "${this.identity.body.deviceName}"`);
this._connection = await this._encryptClient(connection);
// Starting with protocol version 8, the devices are expected to
// exchange identity packets again after TLS negotiation
if (this.identity.body.protocolVersion >= 8) {
await this.sendPacket(this.backend.identity);
this.identity = await this.readPacket();
}
} catch (e) {
this.close();
throw e;
@ -754,6 +776,13 @@ export const Channel = GObject.registerClass({
this.cancellable);
this._connection = await this._encryptServer(connection);
// Starting with protocol version 8, the devices are expected to
// exchange identity packets again after TLS negotiation
if (this.identity.body.protocolVersion >= 8) {
await this.sendPacket(this.backend.identity);
this.identity = await this.readPacket();
}
} catch (e) {
this.close();
throw e;
@ -886,4 +915,3 @@ export const Channel = GObject.registerClass({
}
}
});

View File

@ -62,7 +62,7 @@ export default class Controller {
modifier = keymap.get_entries_for_keyval(Gdk.KEY_Super_L)[1][0];
XKeycode.Super_L = modifier.keycode;
} catch (e) {
} catch {
debug('using default modifier keycodes');
}
}
@ -304,9 +304,8 @@ export default class Controller {
destroy() {
try {
Atspi.exit();
} catch (e) {
} catch {
// Silence errors
}
}
}

View File

@ -17,7 +17,7 @@ try {
EBook = (await import('gi://EBook')).default;
EBookContacts = (await import('gi://EBookContacts')).default;
EDataServer = (await import('gi://EDataServer')).default;
} catch (e) {
} catch {
HAVE_EDS = false;
}
@ -267,7 +267,7 @@ const Store = GObject.registerClass({
this._ebooks = new Map();
// Get the current EBooks
const registry = await this._getESourceRegistry();
const registry = await EDataServer.SourceRegistry.new(null);
for (const source of registry.list_sources('Address Book'))
await this._onAppeared(null, source);
@ -329,7 +329,7 @@ const Store = GObject.registerClass({
* Save a Uint8Array to file and return the path
*
* @param {Uint8Array} contents - An image byte array
* @return {string|undefined} File path or %undefined on failure
* @returns {string|undefined} File path or %undefined on failure
*/
async storeAvatar(contents) {
const md5 = GLib.compute_checksum_for_data(GLib.ChecksumType.MD5,
@ -353,10 +353,10 @@ const Store = GObject.registerClass({
/**
* Query the Store for a contact by name and/or number.
*
* @param {Object} query - A query object
* @param {object} query - A query object
* @param {string} [query.name] - The contact's name
* @param {string} query.number - The contact's number
* @return {Object} A contact object
* @returns {object} A contact object
*/
query(query) {
// First look for an existing contact by number
@ -410,7 +410,7 @@ const Store = GObject.registerClass({
/**
* Add a contact, checking for validity
*
* @param {Object} contact - A contact object
* @param {object} contact - A contact object
* @param {boolean} write - Write to disk
*/
add(contact, write = true) {
@ -468,8 +468,8 @@ const Store = GObject.registerClass({
*
* { "555-5555": { "name": "...", "numbers": [], ... } }
*
* @param {Object[]} addresses - A list of address objects
* @return {Object} A dictionary of phone numbers and contacts
* @param {object[]} addresses - A list of address objects
* @returns {object} A dictionary of phone numbers and contacts
*/
lookupAddresses(addresses) {
const contacts = {};
@ -502,7 +502,7 @@ const Store = GObject.registerClass({
/**
* Update the contact store from a dictionary of our custom contact objects.
*
* @param {Object} json - an Object of contact Objects
* @param {object} json - an Object of contact Objects
*/
async update(json = {}) {
try {
@ -610,4 +610,3 @@ const Store = GObject.registerClass({
});
export default Store;

View File

@ -41,7 +41,7 @@ const Default = new Map();
* followed by a call to `release()`.
*
* @param {string} name - The module name
* @return {*} The default instance of a component
* @returns {*} The default instance of a component
*/
export function acquire(name) {
if (functionOverrides.acquire)
@ -78,7 +78,7 @@ export function acquire(name) {
* holder, the component will be freed.
*
* @param {string} name - The module name
* @return {null} A %null value, useful for overriding a traced variable
* @returns {null} A %null value, useful for overriding a traced variable
*/
export function release(name) {
if (functionOverrides.release)
@ -99,4 +99,3 @@ export function release(name) {
return null;
}

View File

@ -51,7 +51,7 @@ const RemoteSession = GObject.registerClass({
get session_id() {
try {
return this.get_cached_property('SessionId').unpack();
} catch (e) {
} catch {
return null;
}
}
@ -327,22 +327,24 @@ export default class Controller {
}
async _ensureAdapter() {
// Update the timestamp of the last event
this._sessionExpiry = Math.floor((Date.now() / 1000) + SESSION_TIMEOUT);
// Session is active
if (this._session !== null)
return this._session;
// Mutter's RemoteDesktop is not available, fall back to Atspi
if (this.connection === null) {
debug('Falling back to Atspi');
this._session = new AtspiController();
return this._session;
}
try {
// Update the timestamp of the last event
this._sessionExpiry = Math.floor((Date.now() / 1000) + SESSION_TIMEOUT);
// Session is active
if (this._session !== null)
return;
// Mutter's RemoteDesktop is not available, fall back to Atspi
if (this.connection === null) {
debug('Falling back to Atspi');
this._session = new AtspiController();
// Mutter is available and there isn't another session starting
} else if (this._sessionStarting === false) {
if (this._sessionStarting === false) {
this._sessionStarting = true;
debug('Creating Mutter RemoteDesktop session');
@ -368,18 +370,17 @@ export default class Controller {
this._onSessionExpired.bind(this)
);
}
this._sessionStarting = false;
}
return this._session;
} catch (e) {
logError(e);
if (this._session !== null) {
this._session.destroy();
this._session = null;
}
this._sessionStarting = false;
throw e;
}
}
@ -387,111 +388,81 @@ export default class Controller {
* Pointer Events
*/
movePointer(dx, dy) {
try {
if (dx === 0 && dy === 0)
return;
if (dx === 0 && dy === 0)
return;
this._ensureAdapter();
this._session.movePointer(dx, dy);
} catch (e) {
debug(e);
}
this._ensureAdapter()
.then(session => session?.movePointer(dx, dy))
.catch(e => debug(e));
}
pressPointer(button) {
try {
this._ensureAdapter();
this._session.pressPointer(button);
} catch (e) {
debug(e);
}
this._ensureAdapter()
.then(session => session?.pressPointer(button))
.catch(e => debug(e));
}
releasePointer(button) {
try {
this._ensureAdapter();
this._session.releasePointer(button);
} catch (e) {
debug(e);
}
this._ensureAdapter()
.then(session => session?.releasePointer(button))
.catch(e => debug(e));
}
clickPointer(button) {
try {
this._ensureAdapter();
this._session.clickPointer(button);
} catch (e) {
debug(e);
}
this._ensureAdapter()
.then(session => session?.clickPointer(button))
.catch(e => debug(e));
}
doubleclickPointer(button) {
try {
this._ensureAdapter();
this._session.doubleclickPointer(button);
} catch (e) {
debug(e);
}
this._ensureAdapter()
.then(session => session?.doubleclickPointer(button))
.catch(e => debug(e));
}
scrollPointer(dx, dy) {
if (dx === 0 && dy === 0)
return;
try {
this._ensureAdapter();
this._session.scrollPointer(dx, dy);
} catch (e) {
debug(e);
}
this._ensureAdapter()
.then(session => session?.scrollPointer(dx, dy))
.catch(e => debug(e));
}
/*
* Keyboard Events
*/
pressKeysym(keysym) {
try {
this._ensureAdapter();
this._session.pressKeysym(keysym);
} catch (e) {
debug(e);
}
this._ensureAdapter()
.then(session => session?.pressKeysym(keysym))
.catch(e => debug(e));
}
releaseKeysym(keysym) {
try {
this._ensureAdapter();
this._session.releaseKeysym(keysym);
} catch (e) {
debug(e);
}
this._ensureAdapter()
.then(session => session?.releaseKeysym(keysym))
.catch(e => debug(e));
}
pressreleaseKeysym(keysym) {
try {
this._ensureAdapter();
this._session.pressreleaseKeysym(keysym);
} catch (e) {
debug(e);
}
this._ensureAdapter()
.then(session => session?.pressreleaseKeysym(keysym))
.catch(e => debug(e));
}
/*
* High-level keyboard input
*/
pressKeys(input, modifiers) {
try {
this._ensureAdapter();
this._ensureAdapter()
.then(session => {
if (typeof input === 'string') {
for (let i = 0; i < input.length; i++)
this._session.pressKey(input[i], modifiers);
session?.pressKey(input[i], modifiers);
} else {
this._session.pressKey(input, modifiers);
session?.pressKey(input, modifiers);
}
} catch (e) {
debug(e);
}
}).catch(e => debug(e));
}
destroy() {

View File

@ -472,6 +472,10 @@ const PlayerProxy = GObject.registerClass({
GTypeName: 'GSConnectMPRISPlayer',
}, class PlayerProxy extends Player {
/**
* @constructs GSConnectMPRISPlayer
* @param {string} name - The name of the player
*/
_init(name) {
super._init();
@ -539,7 +543,7 @@ const PlayerProxy = GObject.registerClass({
_get(proxy, name, fallback = null) {
try {
return proxy.get_cached_property(name).recursiveUnpack();
} catch (e) {
} catch {
return fallback;
}
}
@ -707,7 +711,7 @@ const PlayerProxy = GObject.registerClass({
);
return reply.recursiveUnpack()[0];
} catch (e) {
} catch {
return 0;
}
}
@ -914,7 +918,7 @@ const Manager = GObject.registerClass({
* Check for a player by its Identity.
*
* @param {string} identity - A player name
* @return {boolean} %true if the player was found
* @returns {boolean} %true if the player was found
*/
hasPlayer(identity) {
for (const player of this._players.values()) {
@ -929,7 +933,7 @@ const Manager = GObject.registerClass({
* Get a player by its Identity.
*
* @param {string} identity - A player name
* @return {GSConnectMPRISPlayer|null} A player or %null
* @returns {GSConnectMPRISPlayer|null} A player or %null
*/
getPlayer(identity) {
for (const player of this._players.values()) {
@ -943,7 +947,7 @@ const Manager = GObject.registerClass({
/**
* Get a list of player identities.
*
* @return {string[]} A list of player identities
* @returns {string[]} A list of player identities
*/
getIdentities() {
const identities = [];
@ -1000,4 +1004,3 @@ const Manager = GObject.registerClass({
* The service class for this component
*/
export default Manager;

View File

@ -144,7 +144,7 @@ const Listener = GObject.registerClass({
*
* @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
* @returns {string} A well-known name or %null
*/
async _getAppId(sender, appName) {
try {
@ -189,7 +189,7 @@ const Listener = GObject.registerClass({
*
* @param {string} sender - A DBus unique name
* @param {string} [appName] - `appName` supplied by Notify()
* @return {string} A well-known name or %null
* @returns {string} A well-known name or %null
*/
async _getAppName(sender, appName = null) {
// Check the cache first
@ -201,7 +201,7 @@ const Listener = GObject.registerClass({
const appInfo = Gio.DesktopAppInfo.new(`${appId}.desktop`);
this._names[appName] = appInfo.get_name();
appName = appInfo.get_name();
} catch (e) {
} catch {
// Silence errors
}
@ -261,7 +261,7 @@ const Listener = GObject.registerClass({
/**
* Export interfaces for proxying notifications and become a monitor
*
* @return {Promise} A promise for the operation
* @returns {Promise} A promise for the operation
*/
_monitorConnection() {
// libnotify Interface

View File

@ -19,7 +19,7 @@ try {
GIRepository.Repository.prepend_library_path(typelibDir);
Gvc = (await import('gi://Gvc')).default;
} catch (e) {}
} catch {}
/**
@ -33,7 +33,7 @@ if (Gvc) {
return this.description;
return `${this.get_port().human_port} (${this.description})`;
} catch (e) {
} catch {
return this.description;
}
},

View File

@ -9,7 +9,7 @@ import GLib from 'gi://GLib';
let GSound = null;
try {
GSound = (await import('gi://GSound')).default;
} catch (e) {}
} catch {}
const Player = class Player {
@ -169,4 +169,3 @@ const Player = class Player {
* The service class for this component
*/
export default Player;

View File

@ -6,13 +6,14 @@ import Gio from 'gi://Gio';
import GLib from 'gi://GLib';
import GObject from 'gi://GObject';
import Device from './device.js';
import plugins from './plugins/index.js';
/**
* Get the local device type.
*
* @return {string} A device type string
* @returns {string} A device type string
*/
export function _getDeviceType() {
try {
@ -27,7 +28,7 @@ export function _getDeviceType() {
return 'laptop';
return 'desktop';
} catch (e) {
} catch {
return 'desktop';
}
}
@ -69,8 +70,8 @@ export class Packet {
/**
* Deserialize and return a new Packet from an Object or string.
*
* @param {Object|string} data - A string or dictionary to deserialize
* @return {Core.Packet} A new packet object
* @param {object|string} data - A string or dictionary to deserialize
* @returns {Packet} A new packet object
*/
static deserialize(data) {
return new Packet(data);
@ -80,7 +81,7 @@ export class Packet {
* Serialize the packet as a single line with a terminating new-line (`\n`)
* character, ready to be written to a channel.
*
* @return {string} A serialized packet
* @returns {string} A serialized packet
*/
serialize() {
this.id = Date.now();
@ -90,7 +91,7 @@ export class Packet {
/**
* Update the packet from a dictionary or string of JSON
*
* @param {Object|string} data - Source data
* @param {object|string} data - Source data
*/
update(data) {
try {
@ -106,7 +107,7 @@ export class Packet {
/**
* Check if the packet has a payload.
*
* @return {boolean} %true if @packet has a payload
* @returns {boolean} %true if @packet has a payload
*/
hasPayload() {
if (!this.hasOwnProperty('payloadSize'))
@ -219,7 +220,7 @@ export const Channel = GObject.registerClass({
* Read a packet.
*
* @param {Gio.Cancellable} [cancellable] - A cancellable
* @return {Promise<Core.Packet>} The packet
* @returns {Promise<Packet>} The packet
*/
async readPacket(cancellable = null) {
if (cancellable === null)
@ -247,9 +248,9 @@ export const Channel = GObject.registerClass({
/**
* Send a packet.
*
* @param {Core.Packet} packet - The packet to send
* @param {Packet} packet - The packet to send
* @param {Gio.Cancellable} [cancellable] - A cancellable
* @return {Promise<boolean>} %true if successful
* @returns {Promise<boolean>} %true if successful
*/
sendPacket(packet, cancellable = null) {
if (cancellable === null)
@ -262,7 +263,7 @@ export const Channel = GObject.registerClass({
/**
* Reject a transfer.
*
* @param {Core.Packet} packet - A packet with payload info
* @param {Packet} packet - A packet with payload info
*/
rejectTransfer(packet) {
throw new GObject.NotImplementedError();
@ -272,7 +273,7 @@ export const Channel = GObject.registerClass({
* Download a payload from a device. Typically implementations will override
* this with an async function.
*
* @param {Core.Packet} packet - A packet
* @param {Packet} packet - A packet
* @param {Gio.OutputStream} target - The target stream
* @param {Gio.Cancellable} [cancellable] - A cancellable for the upload
*/
@ -285,7 +286,7 @@ export const Channel = GObject.registerClass({
* Upload a payload to a device. Typically implementations will override
* this with an async function.
*
* @param {Core.Packet} packet - The packet describing the transfer
* @param {Packet} packet - The packet describing the transfer
* @param {Gio.InputStream} source - The source stream
* @param {number} size - The payload size
* @param {Gio.Cancellable} [cancellable] - A cancellable for the upload
@ -350,7 +351,7 @@ export const ChannelService = GObject.registerClass({
get name() {
if (this._name === undefined)
this._name = GLib.get_host_name();
this._name = GLib.get_host_name().slice(0, 32);
return this._name;
}
@ -365,7 +366,7 @@ export const ChannelService = GObject.registerClass({
get id() {
if (this._id === undefined)
this._id = GLib.uuid_string_random();
this._id = Device.generateId();
return this._id;
}
@ -406,7 +407,7 @@ export const ChannelService = GObject.registerClass({
deviceId: this.id,
deviceName: this.name,
deviceType: _getDeviceType(),
protocolVersion: 7,
protocolVersion: 8,
incomingCapabilities: [],
outgoingCapabilities: [],
},
@ -429,7 +430,7 @@ export const ChannelService = GObject.registerClass({
/**
* Emit Core.ChannelService::channel
*
* @param {Core.Channel} channel - The new channel
* @param {Channel} channel - The new channel
*/
channel(channel) {
if (!this.emit('channel', channel))
@ -542,7 +543,7 @@ export const Transfer = GObject.registerClass({
/**
* Ensure there is a stream for the transfer item.
*
* @param {Object} item - A transfer item
* @param {object} item - A transfer item
* @param {Gio.Cancellable} [cancellable] - A cancellable
*/
async _ensureStream(item, cancellable = null) {
@ -583,7 +584,7 @@ export const Transfer = GObject.registerClass({
/**
* Add a file to the transfer.
*
* @param {Core.Packet} packet - A packet
* @param {Packet} packet - A packet
* @param {Gio.File} file - A file to transfer
*/
addFile(packet, file) {
@ -600,7 +601,7 @@ export const Transfer = GObject.registerClass({
/**
* Add a filepath to the transfer.
*
* @param {Core.Packet} packet - A packet
* @param {Packet} packet - A packet
* @param {string} path - A filepath to transfer
*/
addPath(packet, path) {
@ -617,7 +618,7 @@ export const Transfer = GObject.registerClass({
/**
* Add a stream to the transfer.
*
* @param {Core.Packet} packet - A packet
* @param {Packet} packet - A packet
* @param {Gio.InputStream|Gio.OutputStream} stream - A stream to transfer
* @param {number} [size] - Payload size
*/

View File

@ -18,6 +18,7 @@ import system from 'system';
import './init.js';
import Config from '../config.js';
import Device from './device.js';
import Manager from './manager.js';
import * as ServiceUI from './ui/service.js';
@ -45,6 +46,61 @@ const Service = GObject.registerClass({
this._initOptions();
}
_migrateConfiguration() {
if (!Device.validateName(this.settings.get_string('name')))
this.settings.set('name', GLib.get_host_name().slice(0, 32));
const [certPath, keyPath] = [
GLib.build_filenamev([Config.CONFIGDIR, 'certificate.pem']),
GLib.build_filenamev([Config.CONFIGDIR, 'private.pem']),
];
const certificate = Gio.TlsCertificate.new_for_paths(certPath, keyPath,
null);
if (Device.validateId(certificate.common_name))
return;
// Remove the old certificate, serving as the single source of truth
// for the device ID
try {
Gio.File.new_for_path(certPath).delete(null);
Gio.File.new_for_path(keyPath).delete(null);
} catch {
// Silence errors
}
// For each device, remove it entirely if it violates the device ID
// constraints, otherwise mark it unpaired to preserve the settings.
const deviceList = this.settings.get_strv('devices').filter(id => {
const settingsPath = `/org/gnome/shell/extensions/gsconnect/device/${id}/`;
if (!Device.validateId(id)) {
GLib.spawn_command_line_async(`dconf reset -f ${settingsPath}`);
Gio.File.rm_rf(GLib.build_filenamev([Config.CACHEDIR, id]));
debug(`Invalid device ID ${id} removed.`);
return false;
}
const settings = new Gio.Settings({
settings_schema: Config.GSCHEMA.lookup(
'org.gnome.Shell.Extensions.GSConnect.Device', true),
path: settingsPath,
});
settings.set_boolean('paired', false);
return true;
});
this.settings.set_strv('devices', deviceList);
// Notify the user
const notification = Gio.Notification.new(_('Settings Migrated'));
notification.set_body(_('GSConnect has updated to support changes to the KDE Connect protocol. Some devices may need to be repaired.'));
notification.set_icon(new Gio.ThemedIcon({name: 'dialog-warning'}));
notification.set_priority(Gio.NotificationPriority.HIGH);
this.send_notification('settings-migrated', notification);
// Finally, reset the service ID to trigger re-generation.
this.settings.reset('id');
}
get settings() {
if (this._settings === undefined) {
this._settings = new Gio.Settings({
@ -154,7 +210,7 @@ const Service = GObject.registerClass({
/**
* Report a service-level error
*
* @param {Object} error - An Error or object with name, message and stack
* @param {object} error - An Error or object with name, message and stack
*/
notify_error(error) {
try {
@ -241,6 +297,9 @@ const Service = GObject.registerClass({
// GActions & GSettings
this._initActions();
// TODO: remove after a reasonable period of time
this._migrateConfiguration();
this.manager.start();
}
@ -699,4 +758,3 @@ const Service = GObject.registerClass({
});
await (new Service()).runAsync([system.programInvocationName].concat(ARGV));

View File

@ -11,6 +11,8 @@ import * as Components from './components/index.js';
import * as Core from './core.js';
import plugins from './plugins/index.js';
const ALLOWED_TIMESTAMP_TIME_DIFFERENCE_SECONDS = 1800; // 30 min
/**
* An object representing a remote device.
*
@ -88,6 +90,7 @@ const Device = GObject.registerClass({
// GLib.Source timeout id's for pairing requests
this._incomingPairRequest = 0;
this._outgoingPairRequest = 0;
this._pairingTimestamp = 0;
// Maps of name->Plugin, packet->Plugin, uuid->Transfer
this._plugins = new Map();
@ -128,6 +131,19 @@ const Device = GObject.registerClass({
this._loadPlugins();
}
static generateId() {
return GLib.uuid_string_random().replaceAll('-', '');
}
static validateId(id) {
return /^[a-zA-Z0-9_-]{32,38}$/.test(id);
}
static validateName(name) {
// None of the forbidden characters and at least one non-whitespace
return name.trim() && /^[^"',;:.!?()[\]<>]{1,32}$/.test(name);
}
get channel() {
if (this._channel === undefined)
this._channel = null;
@ -162,47 +178,34 @@ const Device = GObject.registerClass({
// FIXME: backend should do this stuff
get encryption_info() {
let localCert = null;
let remoteCert = null;
if (!this.channel)
return '';
// Bluetooth connections have no certificate so we use the host address
if (this.connection_type === 'bluetooth') {
// TRANSLATORS: Bluetooth address for remote device
return _('Bluetooth device at %s').format('???');
// If the device is connected use the certificate from the connection
} else if (this.connected) {
remoteCert = this.channel.peer_certificate;
// Otherwise pull it out of the settings
} else if (this.paired) {
remoteCert = Gio.TlsCertificate.new_from_pem(
this.settings.get_string('certificate-pem'),
-1
);
}
// FIXME: another ugly reach-around
let lanBackend;
const localCert = this.service.manager.backends.get('lan')?.certificate;
const remoteCert = this.channel?.peer_certificate;
if (!localCert || !remoteCert)
return '';
if (this.service !== null)
lanBackend = this.service.manager.backends.get('lan');
const checksum = new GLib.Checksum(GLib.ChecksumType.SHA256);
let [a, b] = [localCert.pubkey_der(), remoteCert.pubkey_der()];
if (a.compare(b) < 0)
[a, b] = [b, a]; // swap
checksum.update(a.toArray());
checksum.update(b.toArray());
if (lanBackend && lanBackend.certificate)
localCert = lanBackend.certificate;
if (this.channel?.identity.body.protocolVersion >= 8)
checksum.update(String(this._pairingTimestamp));
let verificationKey = '';
if (localCert && remoteCert) {
let a = localCert.pubkey_der();
let b = remoteCert.pubkey_der();
if (a.compare(b) < 0)
[a, b] = [b, a]; // swap
const checksum = new GLib.Checksum(GLib.ChecksumType.SHA256);
checksum.update(a.toArray());
checksum.update(b.toArray());
verificationKey = checksum.get_string();
}
const verificationKey = checksum.get_string()
.substring(0, 8)
.toUpperCase();
// TRANSLATORS: Label for TLS connection verification key
//
@ -384,7 +387,7 @@ const Device = GObject.registerClass({
*
* @param {string[]} args - process arguments
* @param {Gio.Cancellable} [cancellable] - optional cancellable
* @return {Gio.Subprocess} The subprocess
* @returns {Gio.Subprocess} The subprocess
*/
launchProcess(args, cancellable = null) {
if (this._launcher === undefined) {
@ -418,7 +421,7 @@ const Device = GObject.registerClass({
* Handle a packet and pass it to the appropriate plugin.
*
* @param {Core.Packet} packet - The incoming packet object
* @return {undefined} no return value
* @returns {undefined} no return value
*/
handlePacket(packet) {
try {
@ -443,7 +446,7 @@ const Device = GObject.registerClass({
/**
* Send a packet to the device.
*
* @param {Object} packet - An object of packet data...
* @param {object} packet - An object of packet data...
*/
async sendPacket(packet) {
try {
@ -524,7 +527,7 @@ const Device = GObject.registerClass({
* device menu.
*
* @param {string} actionName - An action name with scope (eg. device.foo)
* @return {number} An 0-based index or -1 if not found
* @returns {number} An 0-based index or -1 if not found
*/
getMenuAction(actionName) {
for (let i = 0, len = this.menu.get_n_items(); i < len; i++) {
@ -533,7 +536,7 @@ const Device = GObject.registerClass({
if (val.unpack() === actionName)
return i;
} catch (e) {
} catch {
continue;
}
}
@ -546,7 +549,7 @@ const Device = GObject.registerClass({
*
* @param {Gio.MenuItem} menuItem - A GMenuItem
* @param {number} [index] - The position to place the item
* @return {number} The position the item was placed
* @returns {number} The position the item was placed
*/
addMenuItem(menuItem, index = -1) {
try {
@ -570,7 +573,7 @@ const Device = GObject.registerClass({
* @param {number} [index] - The position to place the item
* @param {string} label - A label for the item
* @param {string} icon_name - A themed icon name for the item
* @return {number} The position the item was placed
* @returns {number} The position the item was placed
*/
addMenuAction(action, index = -1, label, icon_name) {
try {
@ -600,7 +603,7 @@ const Device = GObject.registerClass({
* Remove a GAction from the top level of the device menu by action name
*
* @param {string} actionName - A GAction name, including scope
* @return {number} The position the item was removed from or -1
* @returns {number} The position the item was removed from or -1
*/
removeMenuAction(actionName) {
try {
@ -631,7 +634,7 @@ const Device = GObject.registerClass({
/**
* Show a device notification.
*
* @param {Object} params - A dictionary of notification parameters
* @param {object} params - A dictionary of notification parameters
* @param {number} [params.id] - A UNIX epoch timestamp (ms)
* @param {string} [params.title] - A title
* @param {string} [params.body] - A body
@ -728,7 +731,7 @@ const Device = GObject.registerClass({
/**
* Create a transfer object.
*
* @return {Core.Transfer} A new transfer
* @returns {Core.Transfer} A new transfer
*/
createTransfer() {
const transfer = new Core.Transfer({device: this});
@ -747,13 +750,11 @@ const Device = GObject.registerClass({
* Reject the transfer payload described by @packet.
*
* @param {Core.Packet} packet - A packet
* @return {Promise} A promise for the operation
* @returns {void}
*/
rejectTransfer(packet) {
if (!packet || !packet.hasPayload())
return;
return this.channel.rejectTransfer(packet);
if (packet?.hasPayload())
return this.channel.rejectTransfer(packet);
}
openPath(action, parameter) {
@ -816,15 +817,19 @@ const Device = GObject.registerClass({
// The device thinks we're unpaired
} else if (this.paired) {
this._setPaired(true);
this._pairingTimestamp = Math.floor(Date.now() / 1000);
this.sendPacket({
type: 'kdeconnect.pair',
body: {pair: true},
body: {
pair: true,
timestamp: this._pairingTimestamp,
},
});
this._triggerPlugins();
// The device is requesting pairing
} else {
this._notifyPairRequest();
this._notifyPairRequest(packet.body?.timestamp);
}
// Device is requesting unpairing/rejecting our request
} else {
@ -835,11 +840,14 @@ const Device = GObject.registerClass({
/**
* Notify the user of an incoming pair request and set a 30s timeout
*
* @param {number} [timestamp] - Timestamp for the pair request
*/
_notifyPairRequest() {
_notifyPairRequest(timestamp = 0) {
// Reset any active request
this._resetPairRequest();
this._pairingTimestamp = timestamp;
this.showNotification({
id: 'pair-request',
// TRANSLATORS: eg. Pair Request from Google Pixel
@ -884,6 +892,8 @@ const Device = GObject.registerClass({
GLib.source_remove(this._outgoingPairRequest);
this._outgoingPairRequest = 0;
}
this._pairingTimestamp = 0;
}
/**
@ -931,8 +941,24 @@ const Device = GObject.registerClass({
// If we're accepting an incoming pair request, set the internal
// paired state and send the confirmation before loading plugins.
if (this._incomingPairRequest) {
this._setPaired(true);
if (this.identity?.body.protocolVersion >= 8) {
const currentTimestamp = Math.floor(Date.now() / 1000);
const diffTimestamp = Number.abs(this._pairingTimestamp - currentTimestamp);
if (diffTimestamp > ALLOWED_TIMESTAMP_TIME_DIFFERENCE_SECONDS) {
this._setPaired(false);
this.showNotification({
id: 'pair-request',
// TRANSLATORS: eg. Failed to pair with Google Pixel
title: _('Failed to pair with %s').format(this.name),
body: _('Device clocks are out of sync'),
icon: new Gio.ThemedIcon({name: 'dialog-warning-symbolic'}),
priority: Gio.NotificationPriority.URGENT,
});
return;
}
}
this._setPaired(true);
this.sendPacket({
type: 'kdeconnect.pair',
body: {pair: true},
@ -945,9 +971,13 @@ const Device = GObject.registerClass({
} else if (!this.paired) {
this._resetPairRequest();
this._pairingTimestamp = Math.floor(Date.now() / 1000);
this.sendPacket({
type: 'kdeconnect.pair',
body: {pair: true},
body: {
pair: true,
timestamp: this._pairingTimestamp,
},
});
this._outgoingPairRequest = GLib.timeout_add_seconds(

View File

@ -9,7 +9,7 @@ import GIRepository from 'gi://GIRepository';
import GLib from 'gi://GLib';
import Config from '../config.js';
import setup, {setupGettext} from '../utils/setup.js';
import {setup, setupGettext} from '../utils/setup.js';
// Promise Wrappers
@ -163,7 +163,7 @@ if (!globalThis.HAVE_GNOME) {
* A simple (for now) pre-comparison sanitizer for phone numbers
* See: https://github.com/KDE/kdeconnect-kde/blob/master/smsapp/conversationlistmodel.cpp#L200-L210
*
* @return {string} Return the string stripped of leading 0, and ' ()-+'
* @returns {string} Return the string stripped of leading 0, and ' ()-+'
*/
String.prototype.toPhoneNumber = function () {
const strippedNumber = this.replace(/^0*|[ ()+-]/g, '');
@ -179,7 +179,7 @@ String.prototype.toPhoneNumber = function () {
* A simple equality check for phone numbers based on `toPhoneNumber()`
*
* @param {string} number - A phone number string to compare
* @return {boolean} If `this` and @number are equivalent phone numbers
* @returns {boolean} If `this` and @number are equivalent phone numbers
*/
String.prototype.equalsPhoneNumber = function (number) {
const a = this.toPhoneNumber();
@ -212,12 +212,12 @@ Gio.File.rm_rf = function (file) {
Gio.File.rm_rf(iter.get_child(info));
iter.close(null);
} catch (e) {
} catch {
// Silence errors
}
file.delete(null);
} catch (e) {
} catch {
// Silence errors
}
};
@ -227,7 +227,7 @@ Gio.File.rm_rf = function (file) {
* Extend GLib.Variant with a static method to recursively pack a variant
*
* @param {*} [obj] - May be a GLib.Variant, Array, standard Object or literal.
* @return {GLib.Variant} The resulting GVariant
* @returns {GLib.Variant} The resulting GVariant
*/
function _full_pack(obj) {
let packed;
@ -283,7 +283,7 @@ GLib.Variant.full_pack = _full_pack;
* Extend GLib.Variant with a method to recursively deepUnpack() a variant
*
* @param {*} [obj] - May be a GLib.Variant, Array, standard Object or literal.
* @return {*} The resulting object
* @returns {*} The resulting object
*/
function _full_unpack(obj) {
obj = (obj === undefined) ? this : obj;
@ -310,7 +310,7 @@ function _full_unpack(obj) {
unpacked[key] = Gio.Icon.deserialize(value);
else
unpacked[key] = _full_unpack(value);
} catch (e) {
} catch {
unpacked[key] = _full_unpack(value);
}
}
@ -326,8 +326,8 @@ GLib.Variant.prototype.full_unpack = _full_unpack;
/**
* Creates a GTlsCertificate from the PEM-encoded data in @cert_path and
* @key_path. If either are missing a new pair will be generated.
* Creates a GTlsCertificate from the PEM-encoded data in %cert_path and
* %key_path. If either are missing a new pair will be generated.
*
* Additionally, the private key will be added using ssh-add to allow sftp
* connections using Gio.
@ -337,9 +337,16 @@ GLib.Variant.prototype.full_unpack = _full_unpack;
* @param {string} certPath - Absolute path to a x509 certificate in PEM format
* @param {string} keyPath - Absolute path to a private key in PEM format
* @param {string} commonName - A unique common name for the certificate
* @return {Gio.TlsCertificate} A TLS certificate
* @returns {Gio.TlsCertificate} A TLS certificate
*/
Gio.TlsCertificate.new_for_paths = function (certPath, keyPath, commonName = null) {
if (GLib.find_program_in_path(Config.OPENSSL_PATH) === null) {
const error = new Error();
error.name = _('OpenSSL not found');
error.url = `${Config.PACKAGE_URL}/wiki/Error#openssl-not-found`;
throw error;
}
// Check if the certificate/key pair already exists
const certExists = GLib.file_test(certPath, GLib.FileTest.EXISTS);
const keyExists = GLib.file_test(keyPath, GLib.FileTest.EXISTS);
@ -348,17 +355,18 @@ Gio.TlsCertificate.new_for_paths = function (certPath, keyPath, commonName = nul
if (!certExists || !keyExists) {
// If we weren't passed a common name, generate a random one
if (!commonName)
commonName = GLib.uuid_string_random();
commonName = GLib.uuid_string_random().replaceAll('-', '');
const proc = new Gio.Subprocess({
argv: [
Config.OPENSSL_PATH, 'req',
'-new', '-x509', '-sha256',
'-out', certPath,
'-newkey', 'rsa:4096', '-nodes',
'-newkey', 'ec',
'-pkeyopt', 'ec_paramgen_curve:prime256v1',
'-keyout', keyPath,
'-new', '-x509', '-nodes',
'-days', '3650',
'-subj', `/O=andyholmes.github.io/OU=GSConnect/CN=${commonName}`,
'-out', certPath,
],
flags: (Gio.SubprocessFlags.STDOUT_SILENCE |
Gio.SubprocessFlags.STDERR_SILENCE),
@ -396,7 +404,7 @@ Object.defineProperties(Gio.TlsCertificate.prototype, {
/**
* Get just the pubkey as a DER ByteArray of a certificate.
*
* @return {GLib.Bytes} The pubkey as DER of the certificate.
* @returns {GLib.Bytes} The pubkey as DER of the certificate.
*/
'pubkey_der': {
value: function () {

View File

@ -7,6 +7,7 @@ import GLib from 'gi://GLib';
import GObject from 'gi://GObject';
import Config from '../config.js';
import * as Core from './core.js';
import * as DBus from './utils/dbus.js';
import Device from './device.js';
@ -42,6 +43,13 @@ const Manager = GObject.registerClass({
GObject.ParamFlags.READWRITE,
false
),
'certificate': GObject.ParamSpec.object(
'certificate',
'Certificate',
'The local TLS certificate',
GObject.ParamFlags.READABLE,
Gio.TlsCertificate
),
'discoverable': GObject.ParamSpec.boolean(
'discoverable',
'Discoverable',
@ -53,7 +61,7 @@ const Manager = GObject.registerClass({
'id',
'Id',
'The hostname or other network unique id',
GObject.ParamFlags.READWRITE,
GObject.ParamFlags.READABLE,
null
),
'name': GObject.ParamSpec.string(
@ -92,6 +100,17 @@ const Manager = GObject.registerClass({
return this._backends;
}
get certificate() {
if (this._certificate === undefined) {
this._certificate = Gio.TlsCertificate.new_for_paths(
GLib.build_filenamev([Config.CONFIGDIR, 'certificate.pem']),
GLib.build_filenamev([Config.CONFIGDIR, 'private.pem']),
null);
}
return this._certificate;
}
get debug() {
if (this._debug === undefined)
this._debug = this.settings.get_boolean('debug');
@ -156,18 +175,7 @@ const Manager = GObject.registerClass({
}
get id() {
if (this._id === undefined)
this._id = this.settings.get_string('id');
return this._id;
}
set id(value) {
if (this.id === value)
return;
this._id = value;
this.notify('id');
return this.certificate.common_name;
}
get name() {
@ -217,17 +225,12 @@ const Manager = GObject.registerClass({
* GSettings
*/
_initSettings() {
// Initialize the ID and name of the service
if (this.settings.get_string('id').length === 0)
this.settings.set_string('id', GLib.uuid_string_random());
if (this.settings.get_string('name').length === 0)
this.settings.set_string('name', GLib.get_host_name());
// Bound Properties
this.settings.bind('debug', this, 'debug', 0);
this.settings.bind('discoverable', this, 'discoverable', 0);
this.settings.bind('id', this, 'id', 0);
this.settings.bind('name', this, 'name', 0);
}
@ -382,7 +385,7 @@ const Manager = GObject.registerClass({
* of known devices if it doesn't exist.
*
* @param {Core.Packet} packet - An identity packet for the device
* @return {Device} A device object
* @returns {Device} A device object
*/
_ensureDevice(packet) {
let device = this.devices.get(packet.body.deviceId);
@ -422,7 +425,7 @@ const Manager = GObject.registerClass({
*/
_removeDevice(id) {
// Delete all GSettings
const settings_path = `/org/gnome/shell/extensions/gsconnect/${id}/`;
const settings_path = `/org/gnome/shell/extensions/gsconnect/device/${id}/`;
GLib.spawn_command_line_async(`dconf reset -f ${settings_path}`);
// Delete the cache
@ -438,7 +441,7 @@ const Manager = GObject.registerClass({
* A GSourceFunc that tries to reconnect to each paired device, while
* pruning unpaired devices that have disconnected.
*
* @return {boolean} Always %true
* @returns {boolean} Always %true
*/
_reconnect() {
for (const [id, device] of this.devices) {

View File

@ -14,7 +14,7 @@ import system from 'system';
let GioUnix;
try {
GioUnix = (await import('gi://GioUnix?version=2.0')).default;
} catch (e) {
} catch {
GioUnix = {
InputStream: Gio.UnixInputStream,
OutputStream: Gio.UnixOutputStream,
@ -142,7 +142,7 @@ const NativeMessagingHost = GObject.registerClass({
}
return GLib.SOURCE_CONTINUE;
} catch (e) {
} catch {
this.quit();
}
}
@ -152,7 +152,7 @@ const NativeMessagingHost = GObject.registerClass({
const data = JSON.stringify(message);
this._stdout.put_int32(data.length, null);
this._stdout.put_string(data, null);
} catch (e) {
} catch {
this.quit();
}
}
@ -186,7 +186,7 @@ const NativeMessagingHost = GObject.registerClass({
_proxyGetter(name) {
try {
return this.get_cached_property(name).unpack();
} catch (e) {
} catch {
return null;
}
}
@ -222,4 +222,3 @@ const NativeMessagingHost = GObject.registerClass({
// NOTE: must not pass ARGV
await (new NativeMessagingHost()).runAsync([system.programInvocationName]);

View File

@ -7,6 +7,7 @@ import GLib from 'gi://GLib';
import GObject from 'gi://GObject';
import Config from '../config.js';
import * as Core from './core.js';
import plugins from './plugins/index.js';
@ -248,4 +249,3 @@ const Plugin = GObject.registerClass({
});
export default Plugin;

View File

@ -7,6 +7,7 @@ import GLib from 'gi://GLib';
import GObject from 'gi://GObject';
import * as Components from '../components/index.js';
import * as Core from '../core.js';
import Plugin from '../plugin.js';

View File

@ -6,6 +6,7 @@ import Gio from 'gi://Gio';
import GLib from 'gi://GLib';
import GObject from 'gi://GObject';
import * as Core from '../core.js';
import Plugin from '../plugin.js';

View File

@ -7,6 +7,7 @@ import GObject from 'gi://GObject';
import Plugin from '../plugin.js';
import Contacts from '../components/contacts.js';
import * as Core from '../core.js';
/*
* We prefer libebook's vCard parser if it's available
@ -18,7 +19,7 @@ export const setEBookContacts = (ebook) => { // This function is only for tests
try {
EBookContacts = (await import('gi://EBookContacts')).default;
} catch (e) {
} catch {
EBookContacts = null;
}
@ -150,7 +151,7 @@ const ContactsPlugin = GObject.registerClass({
* See: https://github.com/mathiasbynens/quoted-printable/blob/master/src/quoted-printable.js
*
* @param {string} input - The QUOTED-PRINTABLE string
* @return {string} The decoded string
* @returns {string} The decoded string
*/
_decodeQuotedPrintable(input) {
return input
@ -171,7 +172,7 @@ const ContactsPlugin = GObject.registerClass({
* See: https://github.com/kvz/locutus/blob/master/src/php/xml/utf8_decode.js
*
* @param {string} input - The UTF-8 string
* @return {string} The decoded string
* @returns {string} The decoded string
*/
_decodeUTF8(input) {
try {
@ -215,7 +216,7 @@ const ContactsPlugin = GObject.registerClass({
return output.join('');
// Fallback to old unfaithful
} catch (e) {
} catch {
try {
return decodeURIComponent(escape(input));
@ -233,7 +234,7 @@ const ContactsPlugin = GObject.registerClass({
* See: http://jsfiddle.net/ARTsinn/P2t2P/
*
* @param {string} vcard_data - The raw VCard data
* @return {Object} dictionary of vCard data
* @returns {object} dictionary of vCard data
*/
_parseVCard21(vcard_data) {
// vcard skeleton

View File

@ -183,7 +183,7 @@ const MousepadPlugin = GObject.registerClass({
/**
* Handle a input event.
*
* @param {Object} input - The body of a `kdeconnect.mousepad.request`
* @param {object} input - The body of a `kdeconnect.mousepad.request`
*/
_handleInput(input) {
if (!this.settings.get_boolean('share-control'))
@ -285,7 +285,7 @@ const MousepadPlugin = GObject.registerClass({
/**
* Handle an echo/ACK of a event we sent, displaying it the dialog entry.
*
* @param {Object} input - The body of a `kdeconnect.mousepad.echo`
* @param {object} input - The body of a `kdeconnect.mousepad.echo`
*/
_handleEcho(input) {
if (!this._dialog || !this._dialog.visible)
@ -308,7 +308,7 @@ const MousepadPlugin = GObject.registerClass({
* Handle a state change from the remote keyboard. This is an indication
* that the remote keyboard is ready to accept input.
*
* @param {Object} packet - A `kdeconnect.mousepad.keyboardstate` packet
* @param {object} packet - A `kdeconnect.mousepad.keyboardstate` packet
*/
_handleState(packet) {
this._state = !!packet.body.state;
@ -318,7 +318,7 @@ const MousepadPlugin = GObject.registerClass({
/**
* Send an echo/ACK of @input, if requested
*
* @param {Object} input - The body of a 'kdeconnect.mousepad.request'
* @param {object} input - The body of a 'kdeconnect.mousepad.request'
*/
_sendEcho(input) {
if (!input.sendAck)
@ -335,8 +335,6 @@ const MousepadPlugin = GObject.registerClass({
/**
* Send the local keyboard state
*
* @param {boolean} state - Whether we're ready to accept input
*/
_sendState() {
this.device.sendPacket({

View File

@ -8,6 +8,7 @@ import GObject from 'gi://GObject';
import * as Components from '../components/index.js';
import Config from '../../config.js';
import * as Core from '../core.js';
import * as DBus from '../utils/dbus.js';
import {Player} from '../components/mpris.js';
import Plugin from '../plugin.js';
@ -42,6 +43,8 @@ const MPRISPlugin = GObject.registerClass({
this._transferring = new WeakSet();
this._updating = new WeakSet();
this._queueTimers = new Map();
this._mpris = Components.acquire('mpris');
this._playerAddedId = this._mpris.connect(
@ -75,6 +78,12 @@ const MPRISPlugin = GObject.registerClass({
disconnected() {
super.disconnected();
for (const [identity, timer] of this._queueTimers) {
if (timer)
GLib.source_remove(timer);
this._queueTimers.delete(identity);
}
for (const [identity, player] of this._players) {
this._players.delete(identity);
player.destroy();
@ -144,7 +153,7 @@ const MPRISPlugin = GObject.registerClass({
/**
* Handle an update for a remote player.
*
* @param {Object} packet - A `kdeconnect.mpris` packet
* @param {object} packet - A `kdeconnect.mpris` packet
*/
_handlePlayerUpdate(packet) {
const player = this._players.get(packet.body.player);
@ -174,7 +183,7 @@ const MPRISPlugin = GObject.registerClass({
* Handle a request for player information or action.
*
* @param {Core.Packet} packet - a `kdeconnect.mpris.request`
* @return {undefined} no return value
* @returns {undefined} no return value
*/
_handleRequest(packet) {
// A request for the list of players
@ -258,18 +267,35 @@ const MPRISPlugin = GObject.registerClass({
}
}
// Information Request
let hasResponse = false;
if (packet.body.hasOwnProperty('requestNowPlaying') ||
packet.body.hasOwnProperty('requestVolume')) {
const response = this._getUpdate(player.Identity, packet);
this._sendUpdate(player, response);
}
const response = {
type: 'kdeconnect.mpris',
body: {
player: packet.body.player,
},
};
} catch (e) {
debug(e, this.device.name);
} finally {
this._updating.delete(player);
}
}
// Respond to information request (or push updated information)
_getUpdate(identity, packet) {
const player = this._mpris?.getPlayer(identity);
if (!player)
return;
const response = {
type: 'kdeconnect.mpris',
body: {
player: player.Identity,
},
};
try {
if (packet.body.hasOwnProperty('requestNowPlaying')) {
hasResponse = true;
Object.assign(response.body, {
pos: Math.floor(player.Position / 1000),
@ -330,31 +356,61 @@ const MPRISPlugin = GObject.registerClass({
}
}
if (packet.body.hasOwnProperty('requestVolume')) {
hasResponse = true;
if (packet.body.hasOwnProperty('requestVolume'))
response.body.volume = Math.floor(player.Volume * 100);
}
if (hasResponse)
this.device.sendPacket(response);
return response;
} catch (e) {
debug(e, this.device.name);
} finally {
this._updating.delete(player);
}
}
_sendUpdate(player, packet = null) {
if (!player || (!packet && this._updating.has(player)))
return GLib.SOURCE_REMOVE;
debug(`Sending update for ${player.Identity}`);
this._updating.add(player);
if (this._queueTimers.has(player.Identity)) {
const timer_id = this._queueTimers.get(player.Identity);
if (timer_id) {
debug(`Stopping timer id ${timer_id}`);
GLib.source_remove(timer_id);
}
this._queueTimers.delete(player.Identity);
}
if (!packet) {
packet = this._getUpdate(player.Identity, {
body: {
requestNowPlaying: true,
requestVolume: true,
},
}, false);
}
this.device.sendPacket(packet);
this._updating.delete(player);
return GLib.SOURCE_REMOVE;
}
_onPlayerChanged(mpris, player) {
if (!this.settings.get_boolean('share-players'))
return;
this._handleCommand({
body: {
player: player.Identity,
requestNowPlaying: true,
requestVolume: true,
},
});
// Set a timer to send the updated state after a short delay.
// Allows further state changes to be bundled into a single packet.
if (this._queueTimers.has(player.Identity))
return;
this._queueTimers.set(player.Identity, 0);
const timer_id = GLib.timeout_add(
GLib.PRIORITY_DEFAULT,
250, // ms (0.25 seconds)
this._sendUpdate.bind(this, player)
);
this._queueTimers.set(player.Identity, timer_id);
debug(`Set update timer id ${timer_id} for ${player.Identity}`);
}
_onPlayerSeeked(mpris, player, offset) {
@ -434,6 +490,11 @@ const MPRISPlugin = GObject.registerClass({
}
destroy() {
for (const [identity, timer] of this._queueTimers) {
this._queueTimers.delete(identity);
GLib.source_remove(timer);
}
if (this._mpris !== undefined) {
this._mpris.disconnect(this._playerAddedId);
this._mpris.disconnect(this._playerRemovedId);

View File

@ -8,6 +8,7 @@ import GObject from 'gi://GObject';
import Gtk from 'gi://Gtk';
import * as Components from '../components/index.js';
import * as Core from '../core.js';
import Config from '../../config.js';
import Plugin from '../plugin.js';
import ReplyDialog from '../ui/notification.js';
@ -103,7 +104,7 @@ const SMS_APPS = [
* Try to determine if an notification is from an SMS app
*
* @param {Core.Packet} packet - A `kdeconnect.notification`
* @return {boolean} Whether the notification is from an SMS app
* @returns {boolean} Whether the notification is from an SMS app
*/
function _isSmsNotification(packet) {
const id = packet.body.id;
@ -123,8 +124,8 @@ function _isSmsNotification(packet) {
/**
* Remove a local libnotify or Gtk notification.
*
* @param {String|Number} id - Gtk (string) or libnotify id (uint32)
* @param {String|null} application - Application Id if Gtk or null
* @param {string | number} id - Gtk (string) or libnotify id (uint32)
* @param {string | null} application - Application Id if Gtk or null
*/
function _removeNotification(id, application = null) {
let name, path, method, variant;
@ -416,7 +417,7 @@ const NotificationPlugin = GObject.registerClass({
*
* @param {Core.Packet} packet - A `kdeconnect.notification`
* @param {Gio.Icon|string|null} icon - An icon or %null
* @return {Promise} A promise for the operation
* @returns {Promise} A promise for the operation
*/
_uploadIcon(packet, icon = null) {
// Normalize strings into GIcons
@ -438,7 +439,7 @@ const NotificationPlugin = GObject.registerClass({
/**
* Send a local notification to the remote device.
*
* @param {Object} notif - A dictionary of notification parameters
* @param {object} notif - A dictionary of notification parameters
* @param {string} notif.appName - The notifying application
* @param {string} notif.id - The notification ID
* @param {string} notif.title - The notification title
@ -631,7 +632,7 @@ const NotificationPlugin = GObject.registerClass({
*
* @param {string} uuid - The requestReplyId for the repliable notification
* @param {string} message - The message to reply with
* @param {Object} notification - The original notification packet
* @param {object} notification - The original notification packet
*/
replyNotification(uuid, message, notification) {
// If this happens for some reason, things will explode

View File

@ -150,14 +150,14 @@ const RunCommandPlugin = GObject.registerClass({
* Parse the response to a request for the remote command list. Remove the
* command menu if there are no commands, otherwise amend the menu.
*
* @param {string|Object[]} commandList - A list of remote commands
* @param {string | object[]} commandList - A list of remote commands
*/
_handleCommandList(commandList) {
// See: https://github.com/GSConnect/gnome-shell-extension-gsconnect/issues/1051
if (typeof commandList === 'string') {
try {
commandList = JSON.parse(commandList);
} catch (e) {
} catch {
commandList = {};
}
}

View File

@ -7,6 +7,7 @@ import GLib from 'gi://GLib';
import GObject from 'gi://GObject';
import Config from '../../config.js';
import * as Core from '../core.js';
import Plugin from '../plugin.js';
@ -246,7 +247,7 @@ const SFTPPlugin = GObject.registerClass({
* Add GSConnect's private key identity to the authentication agent so our
* identity can be verified by Android during private key authentication.
*
* @return {Promise} A promise for the operation
* @returns {Promise} A promise for the operation
*/
async _addPrivateKey() {
const ssh_add = this._launcher.spawnv([

View File

@ -443,7 +443,7 @@ const FileChooserDialog = GObject.registerClass({
chooser.preview_widget.pixbuf = pixbuf;
chooser.preview_widget.visible = true;
chooser.preview_widget_active = true;
} catch (e) {
} catch {
chooser.preview_widget.visible = false;
chooser.preview_widget_active = false;
}

View File

@ -206,7 +206,7 @@ const SMSPlugin = GObject.registerClass({
/**
* Handle a digest of threads.
*
* @param {Object[]} messages - A list of message objects
* @param {object[]} messages - A list of message objects
* @param {string[]} thread_ids - A list of thread IDs as strings
*/
_handleDigest(messages, thread_ids) {
@ -244,7 +244,7 @@ const SMSPlugin = GObject.registerClass({
/**
* Handle a new single message
*
* @param {Object} message - A message object
* @param {object} message - A message object
*/
_handleMessage(message) {
let conversation = null;
@ -261,7 +261,7 @@ const SMSPlugin = GObject.registerClass({
/**
* Parse a conversation (thread of messages) and sort them
*
* @param {Object[]} thread - A list of sms message objects from a thread
* @param {object[]} thread - A list of sms message objects from a thread
*/
_handleThread(thread) {
// If there are no addresses this will cause major problems...
@ -300,7 +300,7 @@ const SMSPlugin = GObject.registerClass({
/**
* Handle a response to telephony.request_conversation(s)
*
* @param {Object[]} messages - A list of sms message objects
* @param {object[]} messages - A list of sms message objects
*/
_handleMessages(messages) {
try {
@ -394,7 +394,7 @@ const SMSPlugin = GObject.registerClass({
/**
* Send a message
*
* @param {Object[]} addresses - A list of address objects
* @param {object[]} addresses - A list of address objects
* @param {string} messageBody - The message text
* @param {number} [event] - An event bitmask
* @param {boolean} [forceSms] - Whether to force SMS
@ -508,8 +508,8 @@ const SMSPlugin = GObject.registerClass({
/**
* Try to find a thread_id in @smsPlugin for @addresses.
*
* @param {Object[]} addresses - a list of address objects
* @return {string|null} a thread ID
* @param {object[]} addresses - a list of address objects
* @returns {string|null} a thread ID
*/
getThreadIdForAddresses(addresses = []) {
const threads = Object.values(this.threads);

View File

@ -2,13 +2,27 @@
//
// SPDX-License-Identifier: GPL-2.0-or-later
import GIRepository from 'gi://GIRepository';
import GLib from 'gi://GLib';
import GObject from 'gi://GObject';
import * as Components from '../components/index.js';
import Config from '../../config.js';
import * as Core from '../core.js';
import Plugin from '../plugin.js';
let Gvc = null;
try {
// Add gnome-shell's typelib dir to the search path
const typelibDir = GLib.build_filenamev([Config.GNOME_SHELL_LIBDIR, 'gnome-shell']);
GIRepository.Repository.prepend_search_path(typelibDir);
GIRepository.Repository.prepend_library_path(typelibDir);
Gvc = (await import('gi://Gvc')).default;
} catch {}
export const Metadata = {
label: _('System Volume'),
description: _('Enable the paired device to control the system volume'),
@ -73,12 +87,6 @@ const SystemVolumePlugin = GObject.registerClass({
}
}
connected() {
super.connected();
this._sendSinkList();
}
/**
* Handle a request to change an output
*
@ -121,7 +129,7 @@ const SystemVolumePlugin = GObject.registerClass({
* Update the cache for @stream
*
* @param {Gvc.MixerStream} stream - The stream to cache
* @return {Object} The updated cache object
* @returns {object} The updated cache object
*/
_updateCache(stream) {
const state = {

View File

@ -8,6 +8,7 @@ import GLib from 'gi://GLib';
import GObject from 'gi://GObject';
import * as Components from '../components/index.js';
import * as Core from '../core.js';
import Plugin from '../plugin.js';
@ -114,7 +115,7 @@ const TelephonyPlugin = GObject.registerClass({
* Load a Gdk.Pixbuf from base64 encoded data
*
* @param {string} data - Base64 encoded JPEG data
* @return {Gdk.Pixbuf|null} A contact photo
* @returns {GdkPixbuf.Pixbuf|null} A contact photo
*/
_getThumbnailPixbuf(data) {
const loader = new GdkPixbuf.PixbufLoader();

View File

@ -16,7 +16,7 @@ import system from 'system';
*
* @param {*} [salt] - If not %null, will be used as salt for generating a color
* @param {number} alpha - A value in the [0...1] range for the alpha channel
* @return {Gdk.RGBA} A new Gdk.RGBA object generated from the input
* @returns {Gdk.RGBA} A new Gdk.RGBA object generated from the input
*/
function randomRGBA(salt = null, alpha = 1.0) {
let red, green, blue;
@ -41,7 +41,7 @@ function randomRGBA(salt = null, alpha = 1.0) {
* See: https://www.w3.org/TR/2008/REC-WCAG20-20081211/#relativeluminancedef
*
* @param {Gdk.RGBA} rgba - A GdkRGBA object
* @return {number} The relative luminance of the color
* @returns {number} The relative luminance of the color
*/
function relativeLuminance(rgba) {
const {red, green, blue} = rgba;
@ -59,7 +59,7 @@ function relativeLuminance(rgba) {
* See: https://www.w3.org/TR/2008/REC-WCAG20-20081211/#contrast-ratiodef
*
* @param {Gdk.RGBA} rgba - A GdkRGBA object for the background color
* @return {Gdk.RGBA} A GdkRGBA object for the foreground color
* @returns {Gdk.RGBA} A GdkRGBA object for the foreground color
*/
function getFgRGBA(rgba) {
const bgLuminance = relativeLuminance(rgba);
@ -78,7 +78,7 @@ function getFgRGBA(rgba) {
* @param {string} path - A local file path
* @param {number} size - Size in pixels
* @param {scale} [scale] - Scale factor for the size
* @return {Gdk.Pixbuf} A pixbuf
* @returns {Gdk.Pixbuf} A pixbuf
*/
function getPixbufForPath(path, size, scale = 1.0) {
let data, loader;
@ -107,6 +107,15 @@ function getPixbufForPath(path, size, scale = 1.0) {
return pixbuf.scale_simple(size, size, GdkPixbuf.InterpType.HYPER);
}
/**
* Retrieve the GdkPixbuf for a named icon
*
* @param {string} name - The icon name to load
* @param {number} size - The pixel size requested
* @param {number} scale - The scale multiplier
* @param {string} bgColor - The background color the icon will be used against
* @returns {GdkPixbuf.pixbuf|null} The icon image
*/
function getPixbufForIcon(name, size, scale, bgColor) {
const color = getFgRGBA(bgColor);
const theme = Gtk.IconTheme.get_default();
@ -126,7 +135,7 @@ function getPixbufForIcon(name, size, scale, bgColor) {
* See: http://www.ietf.org/rfc/rfc2426.txt
*
* @param {string} type - An RFC2426 phone number type
* @return {string} A localized string like 'Mobile'
* @returns {string} A localized string like 'Mobile'
*/
function getNumberTypeLabel(type) {
if (type.includes('fax'))
@ -152,9 +161,9 @@ function getNumberTypeLabel(type) {
/**
* Get a display number from @contact for @address.
*
* @param {Object} contact - A contact object
* @param {object} contact - A contact object
* @param {string} address - A phone number
* @return {string} A (possibly) better display number for the address
* @returns {string} A (possibly) better display number for the address
*/
export function getDisplayNumber(contact, address) {
const number = address.toPhoneNumber();
@ -623,7 +632,7 @@ export const ContactChooser = GObject.registerClass({
/**
* Get a dictionary of number-contact pairs for each selected phone number.
*
* @return {Object[]} A dictionary of contacts
* @returns {object[]} A dictionary of contacts
*/
getSelected() {
try {
@ -639,4 +648,3 @@ export const ContactChooser = GObject.registerClass({
}
}
});

View File

@ -92,7 +92,7 @@ const _cFormat = new Intl.DateTimeFormat('default', {
* Return a human-readable timestamp, formatted for longer contexts.
*
* @param {number} time - Milliseconds since the epoch (local time)
* @return {string} A localized timestamp similar to what Android Messages uses
* @returns {string} A localized timestamp similar to what Android Messages uses
*/
function getTime(time) {
const date = new Date(time);
@ -134,7 +134,7 @@ function getTime(time) {
* Return a human-readable timestamp, formatted for shorter contexts.
*
* @param {number} time - Milliseconds since the epoch (local time)
* @return {string} A localized timestamp similar to what Android Messages uses
* @returns {string} A localized timestamp similar to what Android Messages uses
*/
function getShortTime(time) {
const date = new Date(time);
@ -175,13 +175,19 @@ function getShortTime(time) {
* Return a human-readable timestamp, similar to `strftime()` with `%c`.
*
* @param {number} time - Milliseconds since the epoch (local time)
* @return {string} A localized timestamp
* @returns {string} A localized timestamp
*/
function getDetailedTime(time) {
return _cFormat.format(time);
}
/**
* Make the avatar for an incoming message visible or invisible
*
* @param {ConversationMessage} row - The message row to modify
* @param {boolean} visible - Whether the avatar should be visible
*/
function setAvatarVisible(row, visible) {
const incoming = row.message.type === Sms.MessageBox.INBOX;
@ -546,8 +552,8 @@ const Conversation = GObject.registerClass({
* Create a message row, ensuring a contact object has been retrieved or
* generated for the message.
*
* @param {Object} message - A dictionary of message data
* @return {ConversationMessage} A message row
* @param {object} message - A dictionary of message data
* @returns {ConversationMessage} A message row
*/
_createMessageRow(message) {
// Ensure we have a contact
@ -655,7 +661,7 @@ const Conversation = GObject.registerClass({
/**
* Log the next message in the conversation.
*
* @param {Object} message - A message object
* @param {object} message - A message object
*/
logNext(message) {
try {
@ -1073,8 +1079,8 @@ export const Window = GObject.registerClass({
/**
* Find the thread row for @contacts
*
* @param {Object[]} contacts - A contact group
* @return {ConversationSummary|null} The thread row or %null
* @param {object[]} contacts - A contact group
* @returns {ConversationSummary|null} The thread row or %null
*/
_getRowForContacts(contacts) {
const addresses = Object.keys(contacts).map(address => {
@ -1156,8 +1162,8 @@ export const Window = GObject.registerClass({
/**
* Try and find an existing conversation widget for @message.
*
* @param {Object} message - A message object
* @return {Conversation|null} A conversation widget or %null
* @param {object} message - A message object
* @returns {Conversation|null} A conversation widget or %null
*/
getConversationForMessage(message) {
// TODO: This shouldn't happen?
@ -1317,4 +1323,3 @@ export const ConversationChooser = GObject.registerClass({
this.destroy();
}
});

View File

@ -11,12 +11,26 @@ import GObject from 'gi://GObject';
/*
* Some utility methods
*/
/**
* Convert a label from kebab-case to CamelCase.
* Will also remove snake_case separators (without capitalizing)
*
* @param {string} string - The label to reformat
* @returns {string} The CamelCased label
*/
function toDBusCase(string) {
return string.replace(/(?:^\w|[A-Z]|\b\w)/g, (ltr, offset) => {
return ltr.toUpperCase();
}).replace(/[\s_-]+/g, '');
}
/**
* Convert a label from CamelCase to snake_case.
*
* @param {string} string - The label to reformat
* @returns {string} - The snake_cased label
*/
function toUnderscoreCase(string) {
return string.replace(/(?:^\w|[A-Z]|_|\b\w)/g, (ltr, offset) => {
if (ltr === '_')
@ -95,7 +109,7 @@ export const Interface = GObject.registerClass({
* Invoke an instance's method for a DBus method call.
*
* @param {Gio.DBusInterfaceInfo} info - The DBus interface
* @param {DBus.Interface} iface - The DBus interface
* @param {Gio.DBusInterface} 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
@ -231,7 +245,7 @@ export const Interface = GObject.registerClass({
*
* @param {Gio.BusType} [busType] - a Gio.BusType constant
* @param {Gio.Cancellable} [cancellable] - an optional Gio.Cancellable
* @return {Promise<Gio.DBusConnection>} A new DBus connection
* @returns {Promise<Gio.DBusConnection>} A new DBus connection
*/
export function newConnection(busType = Gio.BusType.SESSION, cancellable = null) {
return new Promise((resolve, reject) => {
@ -252,4 +266,3 @@ export function newConnection(busType = Gio.BusType.SESSION, cancellable = null)
});
}

View File

@ -79,7 +79,7 @@ const _numberRegex = new RegExp(
* the position within @str where the URL was found.
*
* @param {string} str - the string to search
* @return {Object[]} the list of match objects, as described above
* @returns {object[]} the list of match objects, as described above
*/
export function findUrls(str) {
_urlRegexp.lastIndex = 0;
@ -103,7 +103,7 @@ export function findUrls(str) {
*
* @param {string} str - The string to be modified
* @param {string} [title] - An optional title (eg. alt text, tooltip)
* @return {string} the modified text
* @returns {string} the modified text
*/
export function linkify(str, title = null) {
const text = GLib.markup_escape_text(str, -1);
@ -166,4 +166,3 @@ export default class URI {
return this.body ? `${uri}?body=${escape(this.body)}` : uri;
}
}