From 2e2851dc13d73352530dd4495c7e05603b2e520d Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Wed, 10 Apr 2024 23:38:38 +0200 Subject: Adding upstream version 2.1.2~dev0+20240219. Signed-off-by: Daniel Baumann --- deluge/ui/web/json_api.py | 1022 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1022 insertions(+) create mode 100644 deluge/ui/web/json_api.py (limited to 'deluge/ui/web/json_api.py') diff --git a/deluge/ui/web/json_api.py b/deluge/ui/web/json_api.py new file mode 100644 index 0000000..5f4b3dc --- /dev/null +++ b/deluge/ui/web/json_api.py @@ -0,0 +1,1022 @@ +# +# Copyright (C) 2009-2010 Damien Churchill +# +# This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with +# the additional special exception to link portions of this program with the OpenSSL library. +# See LICENSE for more details. +# + +import cgi +import json +import logging +import os +import shutil +import tempfile +from base64 import b64encode +from types import FunctionType + +from twisted.internet import defer, reactor +from twisted.internet.defer import Deferred, DeferredList +from twisted.web import http, resource, server + +from deluge import component, httpdownloader +from deluge.common import AUTH_LEVEL_DEFAULT, get_magnet_info, is_magnet +from deluge.configmanager import get_config_dir +from deluge.error import NotAuthorizedError +from deluge.i18n import get_languages +from deluge.ui.client import Client, client +from deluge.ui.common import FileTree2, TorrentInfo +from deluge.ui.coreconfig import CoreConfig +from deluge.ui.hostlist import HostList +from deluge.ui.sessionproxy import SessionProxy +from deluge.ui.web.common import _ + +log = logging.getLogger(__name__) + + +class JSONComponent(component.Component): + def __init__(self, name, interval=1, depend=None): + super().__init__(name, interval, depend) + self._json = component.get('JSON') + self._json.register_object(self, name) + + +def export(auth_level=AUTH_LEVEL_DEFAULT): + """ + Decorator function to register an object's method as a RPC. The object + will need to be registered with a `:class:JSON` to be effective. + + :param func: the function to export + :type func: function + :param auth_level: the auth level required to call this method + :type auth_level: int + + """ + + def wrap(func, *args, **kwargs): + func._json_export = True + func._json_auth_level = auth_level + return func + + if isinstance(auth_level, FunctionType): + func = auth_level + auth_level = AUTH_LEVEL_DEFAULT + return wrap(func) + else: + return wrap + + +class JSONException(Exception): + def __init__(self, inner_exception): + self.inner_exception = inner_exception + Exception.__init__(self, str(inner_exception)) + + +class JSON(resource.Resource, component.Component): + """ + A Twisted Web resource that exposes a JSON-RPC interface for web clients \ + to use. + """ + + def __init__(self): + resource.Resource.__init__(self) + component.Component.__init__(self, 'JSON') + self._remote_methods = [] + self._local_methods = {} + if client.is_standalone(): + self.get_remote_methods() + + def get_remote_methods(self, result=None): + """ + Updates remote methods from the daemon. + + Returns: + t.i.d.Deferred: A deferred returning the available remote methods + """ + + def on_get_methods(methods): + self._remote_methods = methods + return methods + + return client.daemon.get_method_list().addCallback(on_get_methods) + + def _exec_local(self, method, params, request): + """ + Handles executing all local methods. + """ + if method == 'system.listMethods': + d = Deferred() + methods = list(self._remote_methods) + methods.extend(self._local_methods) + d.callback(methods) + return d + elif method in self._local_methods: + # This will eventually process methods that the server adds + # and any plugins. + meth = self._local_methods[method] + meth.__globals__['__request__'] = request + component.get('Auth').check_request(request, meth) + return meth(*params) + raise JSONException('Unknown system method') + + def _exec_remote(self, method, params, request): + """ + Executes methods using the Deluge client. + """ + component.get('Auth').check_request(request, level=AUTH_LEVEL_DEFAULT) + core_component, method = method.split('.') + return getattr(getattr(client, core_component), method)(*params) + + def _handle_request(self, request): + """ + Takes some json data as a string and attempts to decode it, and process + the rpc object that should be contained, returning a deferred for all + procedure calls and the request id. + """ + try: + request_data = json.loads(request.json.decode()) + except (ValueError, TypeError): + raise JSONException('JSON not decodable') + + try: + method = request_data['method'] + params = request_data['params'] + request_id = request_data['id'] + except KeyError as ex: + message = 'Invalid JSON request, missing param {} in {}'.format( + ex, + request_data, + ) + raise JSONException(message) + + result = None + error = None + + try: + if method.startswith('system.') or method in self._local_methods: + result = self._exec_local(method, params, request) + elif method in self._remote_methods: + result = self._exec_remote(method, params, request) + else: + error = {'message': 'Unknown method', 'code': 2} + except NotAuthorizedError: + error = {'message': 'Not authenticated', 'code': 1} + except Exception as ex: + log.error('Error calling method `%s`: %s', method, ex) + log.exception(ex) + error = {'message': f'{ex.__class__.__name__}: {str(ex)}', 'code': 3} + + return request_id, result, error + + def _on_rpc_request_finished(self, result, response, request): + """ + Sends the response of any rpc calls back to the json-rpc client. + """ + response['result'] = result + return self._send_response(request, response) + + def _on_rpc_request_failed(self, reason, response, request): + """ + Handles any failures that occurred while making an rpc call. + """ + log.error(reason) + response['error'] = { + 'message': f'{reason.__class__.__name__}: {str(reason)}', + 'code': 4, + } + return self._send_response(request, response) + + def _on_json_request(self, request): + """ + Handler to take the json data as a string and pass it on to the + _handle_request method for further processing. + """ + content_type, _ = cgi.parse_header(request.getHeader(b'content-type').decode()) + if content_type != 'application/json': + message = 'Invalid JSON request content-type: %s' % content_type + raise JSONException(message) + + log.debug('json-request: %s', request.json) + response = {'result': None, 'error': None, 'id': None} + response['id'], d, response['error'] = self._handle_request(request) + + if isinstance(d, Deferred): + d.addCallback(self._on_rpc_request_finished, response, request) + d.addErrback(self._on_rpc_request_failed, response, request) + return d + else: + response['result'] = d + return self._send_response(request, response) + + def _on_json_request_failed(self, reason, request): + """ + Returns the error in json response. + """ + log.error(reason) + response = { + 'result': None, + 'id': None, + 'error': { + 'code': 5, + 'message': f'{reason.__class__.__name__}: {str(reason)}', + }, + } + return self._send_response(request, response) + + def _send_response(self, request, response): + if request._disconnected: + return '' + response = json.dumps(response) + request.setHeader(b'content-type', b'application/json') + request.write(response.encode()) + request.finish() + return server.NOT_DONE_YET + + def render(self, request): + """ + Handles all the POST requests made to the /json controller. + """ + if request.method != b'POST': + request.setResponseCode(http.NOT_ALLOWED) + request.finish() + return server.NOT_DONE_YET + + try: + request.content.seek(0) + request.json = request.content.read() + self._on_json_request(request) + return server.NOT_DONE_YET + except Exception as ex: + return self._on_json_request_failed(ex, request) + + def register_object(self, obj, name=None): + """Registers an object to export it's rpc methods. + + These methods should be exported with the export decorator prior + to registering the object. + + Args: + obj (object): The object that we want to export. + name (str): The name to use. If None, uses the object class name. + + """ + name = name or obj.__class__.__name__ + name = name.lower() + + for d in dir(obj): + if d[0] == '_': + continue + if getattr(getattr(obj, d), '_json_export', False): + log.debug('Registering method: %s', name + '.' + d) + self._local_methods[name + '.' + d] = getattr(obj, d) + + def deregister_object(self, obj): + """Deregisters an objects exported rpc methods. + + Args: + obj (object): The object that was previously registered. + + """ + for key, value in self._local_methods.items(): + if value.__self__ == obj: + del self._local_methods[key] + + +FILES_KEYS = ['files', 'file_progress', 'file_priorities'] + + +class EventQueue: + """ + This class subscribes to events from the core and stores them until all + the subscribed listeners have received the events. + """ + + def __init__(self): + self.__events = {} + self.__handlers = {} + self.__queue = {} + self.__requests = {} + + def add_listener(self, listener_id, event): + """ + Add a listener to the event queue. + + :param listener_id: A unique id for the listener + :type listener_id: string + :param event: The event name + :type event: string + """ + if event not in self.__events: + + def on_event(*args): + for listener in self.__events[event]: + if listener not in self.__queue: + self.__queue[listener] = [] + self.__queue[listener].append((event, args)) + + client.register_event_handler(event, on_event) + self.__handlers[event] = on_event + self.__events[event] = [listener_id] + elif listener_id not in self.__events[event]: + self.__events[event].append(listener_id) + + def get_events(self, listener_id): + """ + Retrieve the pending events for the listener. + + :param listener_id: A unique id for the listener + :type listener_id: string + """ + + # Check to see if we have anything to return immediately + if listener_id in self.__queue: + queue = self.__queue[listener_id] + del self.__queue[listener_id] + return queue + + # Create a deferred to and check again in 100ms + d = Deferred() + reactor.callLater(0.1, self._get_events, listener_id, 0, d) + return d + + def _get_events(self, listener_id, count, d): + if listener_id in self.__queue: + queue = self.__queue[listener_id] + del self.__queue[listener_id] + d.callback(queue) + else: + # Prevent this loop going on indefinitely incase a client leaves + # the page or disconnects uncleanly. + if count >= 50: + d.callback(None) + else: + reactor.callLater(0.1, self._get_events, listener_id, count + 1, d) + + def remove_listener(self, listener_id, event): + """ + Remove a listener from the event queue. + + :param listener_id: The unique id for the listener + :type listener_id: string + :param event: The event name + :type event: string + """ + self.__events[event].remove(listener_id) + if not self.__events[event]: + client.deregister_event_handler(event, self.__handlers[event]) + del self.__events[event] + del self.__handlers[event] + + +class WebApi(JSONComponent): + """ + The component that implements all the methods required for managing + the web interface. The complete web json interface also exposes all the + methods available from the core RPC. + """ + + def __init__(self): + super().__init__('Web', depend=['SessionProxy']) + self.hostlist = HostList() + self.core_config = CoreConfig() + self.event_queue = EventQueue() + try: + self.sessionproxy = component.get('SessionProxy') + except KeyError: + self.sessionproxy = SessionProxy() + + def disable(self): + client.deregister_event_handler( + 'PluginEnabledEvent', self._json.get_remote_methods + ) + client.deregister_event_handler( + 'PluginDisabledEvent', self._json.get_remote_methods + ) + + if client.is_standalone(): + component.get('Web.PluginManager').stop() + else: + client.disconnect() + client.set_disconnect_callback(None) + + def enable(self): + client.register_event_handler( + 'PluginEnabledEvent', self._json.get_remote_methods + ) + client.register_event_handler( + 'PluginDisabledEvent', self._json.get_remote_methods + ) + + if client.is_standalone(): + component.get('Web.PluginManager').start() + else: + client.set_disconnect_callback(self._on_client_disconnect) + default_host_id = component.get('DelugeWeb').config['default_daemon'] + if default_host_id: + return self.connect(default_host_id) + + return defer.succeed(True) + + def _on_client_connect(self, *args): + """Handles client successfully connecting to the daemon. + + Invokes retrieving the method names and starts webapi and plugins. + + """ + d_methods = self._json.get_remote_methods() + component.get('Web.PluginManager').start() + self.start() + return d_methods + + def _on_client_connect_fail(self, result, host_id): + log.error( + 'Unable to connect to daemon, check host_id "%s" is correct.', host_id + ) + + def _on_client_disconnect(self, *args): + component.get('Web.PluginManager').stop() + return self.stop() + + def start(self): + self.core_config.start() + return self.sessionproxy.start() + + def stop(self): + self.core_config.stop() + self.sessionproxy.stop() + return defer.succeed(True) + + @export + def connect(self, host_id): + """Connect the web client to a daemon. + + Args: + host_id (str): The id of the daemon in the host list. + + Returns: + Deferred: List of methods the daemon supports. + """ + d = self.hostlist.connect_host(host_id) + d.addCallback(self._on_client_connect) + d.addErrback(self._on_client_connect_fail, host_id) + return d + + @export + def connected(self): + """ + The current connection state. + + :returns: True if the client is connected + :rtype: boolean + """ + return client.connected() + + @export + def disconnect(self): + """ + Disconnect the web interface from the connected daemon. + """ + d = client.disconnect() + + def on_disconnect(reason): + return str(reason) + + d.addCallback(on_disconnect) + return d + + @export + def update_ui(self, keys, filter_dict): + """ + Gather the information required for updating the web interface. + + :param keys: the information about the torrents to gather + :type keys: list + :param filter_dict: the filters to apply when selecting torrents. + :type filter_dict: dictionary + :returns: The torrent and UI information. + :rtype: dictionary + """ + d = Deferred() + ui_info = { + 'connected': client.connected(), + 'torrents': None, + 'filters': None, + 'stats': { + 'max_download': self.core_config.get('max_download_speed'), + 'max_upload': self.core_config.get('max_upload_speed'), + 'max_num_connections': self.core_config.get('max_connections_global'), + }, + } + + if not client.connected(): + d.callback(ui_info) + return d + + def got_stats(stats): + ui_info['stats']['num_connections'] = stats['peer.num_peers_connected'] + ui_info['stats']['upload_rate'] = stats['payload_upload_rate'] + ui_info['stats']['download_rate'] = stats['payload_download_rate'] + ui_info['stats']['download_protocol_rate'] = ( + stats['download_rate'] - stats['payload_download_rate'] + ) + ui_info['stats']['upload_protocol_rate'] = ( + stats['upload_rate'] - stats['payload_upload_rate'] + ) + ui_info['stats']['dht_nodes'] = stats['dht.dht_nodes'] + ui_info['stats']['has_incoming_connections'] = stats[ + 'net.has_incoming_connections' + ] + + def got_filters(filters): + ui_info['filters'] = filters + + def got_free_space(free_space): + ui_info['stats']['free_space'] = free_space + + def got_external_ip(external_ip): + ui_info['stats']['external_ip'] = external_ip + + def got_torrents(torrents): + ui_info['torrents'] = torrents + + def on_complete(result): + d.callback(ui_info) + + d1 = component.get('SessionProxy').get_torrents_status(filter_dict, keys) + d1.addCallback(got_torrents) + + d2 = client.core.get_filter_tree() + d2.addCallback(got_filters) + + d3 = client.core.get_session_status( + [ + 'peer.num_peers_connected', + 'payload_download_rate', + 'payload_upload_rate', + 'download_rate', + 'upload_rate', + 'dht.dht_nodes', + 'net.has_incoming_connections', + ] + ) + d3.addCallback(got_stats) + + d4 = client.core.get_free_space(self.core_config.get('download_location')) + d4.addCallback(got_free_space) + + d5 = client.core.get_external_ip() + d5.addCallback(got_external_ip) + + dl = DeferredList([d1, d2, d3, d4, d5], consumeErrors=True) + dl.addCallback(on_complete) + return d + + def _on_got_files(self, torrent, d): + files = torrent.get('files') + file_progress = torrent.get('file_progress') + file_priorities = torrent.get('file_priorities') + + paths = [] + info = {} + for index, torrent_file in enumerate(files): + path = torrent_file['path'] + paths.append(path) + torrent_file['progress'] = file_progress[index] + torrent_file['priority'] = file_priorities[index] + torrent_file['index'] = index + torrent_file['path'] = path + info[path] = torrent_file + + # update the directory info + dirname = os.path.dirname(path) + while dirname: + dirinfo = info.setdefault(dirname, {}) + dirinfo['size'] = dirinfo.get('size', 0) + torrent_file['size'] + if 'priority' not in dirinfo: + dirinfo['priority'] = torrent_file['priority'] + else: + if dirinfo['priority'] != torrent_file['priority']: + dirinfo['priority'] = 9 + + progresses = dirinfo.setdefault('progresses', []) + progresses.append(torrent_file['size'] * torrent_file['progress'] / 100) + if dirinfo['size'] > 0: + dirinfo['progress'] = sum(progresses) / dirinfo['size'] * 100 + else: + dirinfo['progress'] = 100 + dirinfo['path'] = dirname + dirname = os.path.dirname(dirname) + + def walk(path, item): + if item['type'] == 'dir': + item.update(info[path]) + return item + else: + item.update(info[path]) + return item + + file_tree = FileTree2(paths) + file_tree.walk(walk) + d.callback(file_tree.get_tree()) + + @export + def get_torrent_status(self, torrent_id, keys): + """Get the status for a torrent, filtered by status keys.""" + return component.get('SessionProxy').get_torrent_status(torrent_id, keys) + + @export + def get_torrent_files(self, torrent_id): + """ + Gets the files for a torrent in tree format + + :param torrent_id: the id of the torrent to retrieve. + :type torrent_id: string + :returns: The torrents files in a tree + :rtype: dictionary + """ + main_deferred = Deferred() + d = component.get('SessionProxy').get_torrent_status(torrent_id, FILES_KEYS) + d.addCallback(self._on_got_files, main_deferred) + return main_deferred + + @export + def download_torrent_from_url(self, url, cookie=None): + """ + Download a torrent file from a URL to a temporary directory. + + :param url: the URL of the torrent + :type url: string + :returns: the temporary file name of the torrent file + :rtype: string + """ + + def on_download_success(result): + log.debug('Successfully downloaded %s to %s', url, result) + return result + + def on_download_fail(result): + log.error('Failed to add torrent from url %s', url) + return result + + tempdir = tempfile.mkdtemp(prefix='delugeweb-') + tmp_file = os.path.join(tempdir, url.split('/')[-1]) + log.debug('filename: %s', tmp_file) + headers = {} + if cookie: + headers['Cookie'] = cookie + log.debug('cookie: %s', cookie) + d = httpdownloader.download_file(url, tmp_file, headers=headers) + d.addCallbacks(on_download_success, on_download_fail) + return d + + @export + def get_torrent_info(self, filename): + """ + Return information about a torrent on the filesystem. + + :param filename: the path to the torrent + :type filename: string + + :returns: information about the torrent: + + :: + + { + "name": the torrent name, + "files_tree": the files the torrent contains, + "info_hash" the torrents info_hash + } + + :rtype: dictionary + """ + try: + torrent_info = TorrentInfo(filename.strip(), 2) + return torrent_info.as_dict('name', 'info_hash', 'files_tree') + except Exception as ex: + log.error(ex) + return False + + @export + def get_magnet_info(self, uri): + """Parse a magnet URI for hash and name.""" + return get_magnet_info(uri) + + @export + def add_torrents(self, torrents): + """ + Add torrents by file + + :param torrents: A list of dictionaries containing the torrent \ + path and torrent options to add with. + :type torrents: list + + :: + + json_api.web.add_torrents([{ + "path": "/tmp/deluge-web/some-torrent-file.torrent", + "options": {"download_location": "/home/deluge/"} + }]) + + """ + deferreds = [] + + for torrent in torrents: + if is_magnet(torrent['path']): + log.info( + 'Adding torrent from magnet uri `%s` with options `%r`', + torrent['path'], + torrent['options'], + ) + d = client.core.add_torrent_magnet(torrent['path'], torrent['options']) + deferreds.append(d) + else: + filename = os.path.basename(torrent['path']) + with open(torrent['path'], 'rb') as _file: + fdump = b64encode(_file.read()) + log.info( + 'Adding torrent from file `%s` with options `%r`', + filename, + torrent['options'], + ) + d = client.core.add_torrent_file_async( + filename, fdump, torrent['options'] + ) + deferreds.append(d) + return DeferredList(deferreds, consumeErrors=False) + + def _get_host(self, host_id): + """Information about a host from supplied host id. + + Args: + host_id (str): The id of the host. + + Returns: + list: The host information, empty list if not found. + + """ + return list(self.hostlist.get_host_info(host_id)) + + @export + def get_hosts(self): + """ + Return the hosts in the hostlist. + """ + log.debug('get_hosts called') + return self.hostlist.get_hosts_info() + + @export + def get_host_status(self, host_id): + """ + Returns the current status for the specified host. + + :param host_id: the hash id of the host + :type host_id: string + + """ + + def response(result): + return result + + return self.hostlist.get_host_status(host_id).addCallback(response) + + @export + def add_host(self, host, port, username='', password=''): + """Adds a host to the list. + + Args: + host (str): The IP or hostname of the deluge daemon. + port (int): The port of the deluge daemon. + username (str): The username to login to the daemon with. + password (str): The password to login to the daemon with. + + Returns: + tuple: A tuple of (bool, str). If True will contain the host_id, otherwise + if False will contain the error message. + """ + try: + host_id = self.hostlist.add_host(host, port, username, password) + except ValueError as ex: + return False, str(ex) + else: + return True, host_id + + @export + def edit_host(self, host_id, host, port, username='', password=''): + """Edit host details in the hostlist. + + Args: + host_id (str): The host identifying hash. + host (str): The IP or hostname of the deluge daemon. + port (int): The port of the deluge daemon. + username (str): The username to login to the daemon with. + password (str): The password to login to the daemon with. + + Returns: + bool: True if successful, False otherwise. + + """ + return self.hostlist.update_host(host_id, host, port, username, password) + + @export + def remove_host(self, host_id): + """Removes a host from the hostlist. + + Args: + host_id (str): The host identifying hash. + + Returns: + bool: True if successful, False otherwise. + + """ + return self.hostlist.remove_host(host_id) + + @export + def start_daemon(self, port): + """ + Starts a local daemon. + """ + client.start_daemon(port, get_config_dir()) + + @export + def stop_daemon(self, host_id): + """ + Stops a running daemon. + + :param host_id: the hash id of the host + :type host_id: string + """ + main_deferred = Deferred() + host = self._get_host(host_id) + if not host: + main_deferred.callback((False, _('Daemon does not exist'))) + return main_deferred + + try: + + def on_connect(connected, c): + if not connected: + main_deferred.callback((False, _('Daemon not running'))) + return + c.daemon.shutdown() + main_deferred.callback((True,)) + + def on_connect_failed(reason): + main_deferred.callback((False, reason)) + + host, port, user, password = host[1:5] + c = Client() + d = c.connect(host, port, user, password) + d.addCallback(on_connect, c) + d.addErrback(on_connect_failed) + except Exception: + main_deferred.callback((False, 'An error occurred')) + return main_deferred + + @export + def get_config(self): + """ + Get the configuration dictionary for the web interface. + + :rtype: dictionary + :returns: the configuration + """ + config = component.get('DelugeWeb').config.config.copy() + del config['sessions'] + del config['pwd_salt'] + del config['pwd_sha1'] + return config + + @export + def set_config(self, config): + """ + Sets the configuration dictionary for the web interface. + + :param config: The configuration options to update + :type config: dictionary + """ + web_config = component.get('DelugeWeb').config + for key in config: + if key in ['sessions', 'pwd_salt', 'pwd_sha1']: + log.warning('Ignored attempt to overwrite web config key: %s', key) + continue + web_config[key] = config[key] + + @export + def get_plugins(self): + """All available and enabled plugins within WebUI. + + Note: + This does not represent all plugins from deluge.client.core. + + Returns: + dict: A dict containing 'available_plugins' and 'enabled_plugins' lists. + + """ + + return { + 'enabled_plugins': list(component.get('Web.PluginManager').plugins), + 'available_plugins': component.get('Web.PluginManager').available_plugins, + } + + @export + def get_plugin_info(self, name): + """Get the details for a plugin.""" + return component.get('Web.PluginManager').get_plugin_info(name) + + @export + def get_plugin_resources(self, name): + """Get the resource data files for a plugin.""" + return component.get('Web.PluginManager').get_plugin_resources(name) + + @export + def upload_plugin(self, filename, path): + """Upload a plugin to config.""" + main_deferred = Deferred() + + shutil.copyfile(path, os.path.join(get_config_dir(), 'plugins', filename)) + component.get('Web.PluginManager').scan_for_plugins() + + if client.is_localhost(): + client.core.rescan_plugins() + return True + with open(path, 'rb') as _file: + plugin_data = b64encode(_file.read()) + + def on_upload_complete(*args): + client.core.rescan_plugins() + component.get('Web.PluginManager').scan_for_plugins() + main_deferred.callback(True) + + def on_upload_error(*args): + main_deferred.callback(False) + + d = client.core.upload_plugin(filename, plugin_data) + d.addCallback(on_upload_complete) + d.addErrback(on_upload_error) + return main_deferred + + @export + def register_event_listener(self, event): + """ + Add a listener to the event queue. + + :param event: The event name + :type event: string + """ + self.event_queue.add_listener(__request__.session_id, event) + + @export + def deregister_event_listener(self, event): + """ + Remove an event listener from the event queue. + + :param event: The event name + :type event: string + """ + self.event_queue.remove_listener(__request__.session_id, event) + + @export + def get_events(self): + """ + Retrieve the pending events for the session. + """ + return self.event_queue.get_events(__request__.session_id) + + @export + def set_theme(self, theme): + """ + Sets a new Theme to the WebUI + + Args: + theme (str): the theme to apply + """ + component.get('DelugeWeb').set_theme(theme) + + +class WebUtils(JSONComponent): + """ + Utility functions for the Web UI that do not fit in the WebApi. + """ + + def __init__(self): + super().__init__('WebUtils') + + @export + def get_languages(self): + """ + Get the available translated languages + + Returns: + list: of tuples ``[(lang-id, language-name), ...]`` + """ + return get_languages() + + @export + def get_themes(self): + """ + Get the available themes + + Returns: + list: of themes ``[theme1, theme2, ...]`` + """ + return component.get('DelugeWeb').get_themes() -- cgit v1.2.3