[gnome] Update extensions for version 48
This commit is contained in:
@ -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({
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
}
|
||||
},
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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
|
||||
*/
|
||||
|
||||
@ -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));
|
||||
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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 () {
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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]);
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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';
|
||||
|
||||
|
||||
|
||||
@ -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';
|
||||
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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({
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 = {};
|
||||
}
|
||||
}
|
||||
|
||||
@ -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([
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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 = {
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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({
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@ -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();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user