From 27dbee8e620877f9a1668b1d58c7269a48c7e229 Mon Sep 17 00:00:00 2001 From: Robert Adam Date: Wed, 14 Apr 2021 19:47:00 +0200 Subject: [PATCH 1/2] FEAT(client): Plugin framework This commit introduces a new plugin framework into the codebase of the Mumble client. Note that "plugin" here really refers to a (more or less) general purpose plugin and is therefore not to be confused with the previously available positional data plugins (only responsible for fetching positional data from a running game and passing that to Mumble). The plugin interface is written in C, removing the compiler-dependence the old "plugins" had. Instead plugins can now be written in an arbitrary language as long as that language is capable of being compiled into a shared library and also being capable of being C-compatible. As already indicated a plugin is essentially a shared library that provides certain functions that allow Mumble to interface with it. Inside Mumble the so-called PluginManager is responsible for managing the plugins and relaying events to the respective callbacks. Plugins themselves can also interact with Mumble on their own initiative by using the provided API functions. Fixes #2455 Fixes #2148 Fixes #1594 Fixes #2051 Fixes #3742 Fixes #4575 Fixes #4751 --- .../install-environment_linux.bash | 3 +- .cirrus.yml | 2 +- .../install_ubuntu_shared_64bit.sh | 3 +- CMakeLists.txt | 1 + docs/dev/build-instructions/cmake_options.md | 10 + plugins/CMakeLists.txt | 5 + plugins/HostLinux.cpp | 2 +- plugins/HostWindows.cpp | 2 +- plugins/MumbleAPI_v_1_0_x.h | 492 ++++ plugins/MumblePlugin_v_1_0_x.h | 402 ++++ plugins/PluginComponents_v_1_0_x.h | 411 ++++ plugins/Process.cpp | 2 +- plugins/ProcessWindows.cpp | 2 +- plugins/amongus/Game.cpp | 2 +- plugins/amongus/amongus.cpp | 6 +- plugins/aoc/aoc.cpp | 5 +- plugins/arma2/arma2.cpp | 5 +- plugins/bf1/bf1.cpp | 7 +- plugins/bf1942/bf1942.cpp | 5 +- plugins/bf2/bf2.cpp | 5 +- plugins/bf2142/bf2142.cpp | 7 +- plugins/bf3/bf3.cpp | 5 +- plugins/bf4/bf4.cpp | 7 +- plugins/bf4_x86/bf4_x86.cpp | 7 +- plugins/bfbc2/bfbc2.cpp | 5 +- plugins/bfheroes/bfheroes.cpp | 5 +- plugins/blacklight/blacklight.cpp | 5 +- plugins/borderlands/borderlands.cpp | 5 +- plugins/borderlands2/borderlands2.cpp | 5 +- plugins/breach/breach.cpp | 5 +- plugins/cod2/cod2.cpp | 6 +- plugins/cod4/cod4.cpp | 5 +- plugins/cod5/cod5.cpp | 5 +- plugins/codmw2/codmw2.cpp | 5 +- plugins/codmw2so/codmw2so.cpp | 5 +- plugins/cs/cs.cpp | 5 +- plugins/dys/dys.cpp | 5 +- plugins/etqw/etqw.cpp | 5 +- plugins/ffxiv/ffxiv.cpp | 7 +- plugins/gmod/gmod.cpp | 5 +- plugins/gtaiv/gtaiv.cpp | 5 +- plugins/gtasa/gtasa.cpp | 5 +- plugins/gtav/gtav.cpp | 7 +- plugins/gw/gw.cpp | 5 +- plugins/insurgency/insurgency.cpp | 5 +- plugins/jc2/jc2.cpp | 5 +- plugins/link/link-posix.cpp | 3 +- plugins/link/link.cpp | 3 +- plugins/lol/lol.cpp | 5 +- plugins/lotro/lotro.cpp | 5 +- ...mumble_plugin.h => mumble_legacy_plugin.h} | 19 +- ...inux.h => mumble_positional_audio_linux.h} | 12 +- ..._main.h => mumble_positional_audio_main.h} | 15 +- ...tils.h => mumble_positional_audio_utils.h} | 6 +- ...in32.h => mumble_positional_audio_win32.h} | 10 +- ...mumble_positional_audio_win32_internals.h} | 6 +- plugins/null_plugin.cpp | 3 +- plugins/ql/ql.cpp | 7 +- plugins/rl/rl.cpp | 5 +- plugins/se/se.cpp | 6 +- plugins/sr/sr.cpp | 5 +- plugins/testPlugin/CMakeLists.txt | 9 + plugins/testPlugin/testPlugin.cpp | 480 ++++ plugins/ut2004/ut2004.cpp | 5 +- plugins/ut3/ut3.cpp | 5 +- plugins/ut99/ut99.cpp | 7 +- plugins/wolfet/wolfet.cpp | 5 +- plugins/wow/wow.cpp | 7 +- plugins/wow_x64/wow_x64.cpp | 7 +- src/CMakeLists.txt | 2 + src/Channel.cpp | 19 +- src/Channel.h | 14 + src/Message.h | 3 +- src/Mumble.proto | 13 + src/MumbleConstants.h | 20 + src/ProcessResolver.cpp | 307 +++ src/ProcessResolver.h | 40 + src/mumble/API.h | 188 ++ src/mumble/API_v_1_0_x.cpp | 1980 +++++++++++++++++ src/mumble/Audio.cpp | 10 + src/mumble/AudioInput.cpp | 21 +- src/mumble/AudioInput.h | 8 + src/mumble/AudioOutput.cpp | 145 +- src/mumble/AudioOutput.h | 19 + src/mumble/CMakeLists.txt | 107 +- src/mumble/ClientUser.cpp | 4 + src/mumble/Global.cpp | 2 +- src/mumble/Global.h | 5 +- src/mumble/LegacyPlugin.cpp | 267 +++ src/mumble/LegacyPlugin.h | 82 + src/mumble/Log.cpp | 8 +- src/mumble/Log.h | 10 +- src/mumble/MainWindow.cpp | 37 +- src/mumble/MainWindow.h | 8 + src/mumble/ManualPlugin.cpp | 39 +- src/mumble/ManualPlugin.h | 23 +- src/mumble/Messages.cpp | 21 +- src/mumble/NetworkConfig.cpp | 11 +- src/mumble/NetworkConfig.ui | 13 +- src/mumble/Plugin.cpp | 694 ++++++ src/mumble/Plugin.h | 417 ++++ src/mumble/PluginConfig.cpp | 247 ++ src/mumble/PluginConfig.h | 66 + src/mumble/{Plugins.ui => PluginConfig.ui} | 41 +- src/mumble/PluginInstaller.cpp | 200 ++ src/mumble/PluginInstaller.h | 84 + src/mumble/PluginInstaller.ui | 243 ++ src/mumble/PluginManager.cpp | 933 ++++++++ src/mumble/PluginManager.h | 279 +++ src/mumble/PluginUpdater.cpp | 379 ++++ src/mumble/PluginUpdater.h | 107 + src/mumble/PluginUpdater.ui | 224 ++ src/mumble/Plugins.cpp | 792 ------- src/mumble/Plugins.h | 91 - src/mumble/PositionalData.cpp | 242 ++ src/mumble/PositionalData.h | 171 ++ src/mumble/ServerHandler.cpp | 18 + src/mumble/ServerHandler.h | 10 + src/mumble/Settings.cpp | 86 +- src/mumble/Settings.h | 16 +- src/mumble/UserModel.cpp | 11 + src/mumble/UserModel.h | 21 + src/mumble/main.cpp | 97 +- src/murmur/Messages.cpp | 56 + src/murmur/Meta.cpp | 6 + src/murmur/Meta.h | 3 + src/murmur/Server.cpp | 11 + src/murmur/Server.h | 3 + src/murmur/ServerUser.cpp | 3 +- src/murmur/ServerUser.h | 1 + 130 files changed, 9981 insertions(+), 1145 deletions(-) create mode 100644 plugins/MumbleAPI_v_1_0_x.h create mode 100644 plugins/MumblePlugin_v_1_0_x.h create mode 100644 plugins/PluginComponents_v_1_0_x.h rename plugins/{mumble_plugin.h => mumble_legacy_plugin.h} (87%) rename plugins/{mumble_plugin_linux.h => mumble_positional_audio_linux.h} (95%) rename plugins/{mumble_plugin_main.h => mumble_positional_audio_main.h} (95%) rename plugins/{mumble_plugin_utils.h => mumble_positional_audio_utils.h} (97%) rename plugins/{mumble_plugin_win32.h => mumble_positional_audio_win32.h} (91%) rename plugins/{mumble_plugin_win32_internals.h => mumble_positional_audio_win32_internals.h} (95%) create mode 100644 plugins/testPlugin/CMakeLists.txt create mode 100644 plugins/testPlugin/testPlugin.cpp create mode 100644 src/MumbleConstants.h create mode 100644 src/ProcessResolver.cpp create mode 100644 src/ProcessResolver.h create mode 100644 src/mumble/API.h create mode 100644 src/mumble/API_v_1_0_x.cpp create mode 100644 src/mumble/LegacyPlugin.cpp create mode 100644 src/mumble/LegacyPlugin.h create mode 100644 src/mumble/Plugin.cpp create mode 100644 src/mumble/Plugin.h create mode 100644 src/mumble/PluginConfig.cpp create mode 100644 src/mumble/PluginConfig.h rename src/mumble/{Plugins.ui => PluginConfig.ui} (77%) create mode 100644 src/mumble/PluginInstaller.cpp create mode 100644 src/mumble/PluginInstaller.h create mode 100644 src/mumble/PluginInstaller.ui create mode 100644 src/mumble/PluginManager.cpp create mode 100644 src/mumble/PluginManager.h create mode 100644 src/mumble/PluginUpdater.cpp create mode 100644 src/mumble/PluginUpdater.h create mode 100644 src/mumble/PluginUpdater.ui delete mode 100644 src/mumble/Plugins.cpp delete mode 100644 src/mumble/Plugins.h create mode 100644 src/mumble/PositionalData.cpp create mode 100644 src/mumble/PositionalData.h diff --git a/.ci/azure-pipelines/install-environment_linux.bash b/.ci/azure-pipelines/install-environment_linux.bash index 83da2482912..e55587a2044 100755 --- a/.ci/azure-pipelines/install-environment_linux.bash +++ b/.ci/azure-pipelines/install-environment_linux.bash @@ -14,4 +14,5 @@ sudo apt-get -y install build-essential g++-multilib ninja-build pkg-config \ libasound2-dev libasound2-plugins libasound2-plugins-extra\ libogg-dev libsndfile1-dev libspeechd-dev \ libavahi-compat-libdnssd-dev libzeroc-ice-dev \ - zsync appstream libgrpc++-dev protobuf-compiler-grpc + zsync appstream libgrpc++-dev protobuf-compiler-grpc \ + libpoco-dev diff --git a/.cirrus.yml b/.cirrus.yml index 5fc851fd29e..18f5a745028 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -8,7 +8,7 @@ freebsd_instance: freebsd_task: pkg_script: - pkg update && pkg upgrade -y - - pkg install -y git ninja pkgconf cmake qt5-buildtools qt5-qmake qt5-linguisttools qt5-concurrent qt5-network qt5-xml qt5-sql qt5-svg qt5-testlib boost-libs libsndfile protobuf ice avahi-libdns grpc + - pkg install -y git ninja pkgconf cmake qt5-buildtools qt5-qmake qt5-linguisttools qt5-concurrent qt5-network qt5-xml qt5-sql qt5-svg qt5-testlib boost-libs libsndfile protobuf ice avahi-libdns grpc poco fetch_submodules_script: git submodule --quiet update --init --recursive build_script: - mkdir build && cd build diff --git a/.github/actions/install-dependencies/install_ubuntu_shared_64bit.sh b/.github/actions/install-dependencies/install_ubuntu_shared_64bit.sh index f185ddb90ea..1765eb2903d 100755 --- a/.github/actions/install-dependencies/install_ubuntu_shared_64bit.sh +++ b/.github/actions/install-dependencies/install_ubuntu_shared_64bit.sh @@ -29,4 +29,5 @@ sudo apt -y install \ zsync \ appstream \ libgrpc++-dev \ - protobuf-compiler-grpc + protobuf-compiler-grpc \ + libpoco-dev diff --git a/CMakeLists.txt b/CMakeLists.txt index 79249dd35f4..af4d35e9574 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -30,6 +30,7 @@ project(Mumble ) set(3RDPARTY_DIR "${CMAKE_SOURCE_DIR}/3rdparty") +set(PLUGINS_DIR "${CMAKE_SOURCE_DIR}/plugins") set(CMAKE_CXX_STANDARD 14) set(CMAKE_OSX_DEPLOYMENT_TARGET 10.9) diff --git a/docs/dev/build-instructions/cmake_options.md b/docs/dev/build-instructions/cmake_options.md index 60be982482f..ac1952cc9b9 100644 --- a/docs/dev/build-instructions/cmake_options.md +++ b/docs/dev/build-instructions/cmake_options.md @@ -149,6 +149,16 @@ Build 32 bit overlay library, necessary for the overlay to work with 32 bit proc Build package. (Default: OFF) +### plugin-callback-debug + +Build Mumble with debug output for plugin callbacks inside of Mumble. +(Default: OFF) + +### plugin-debug + +Build Mumble with debug output for plugin developers. +(Default: OFF) + ### plugins Build plugins. diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index 41a45c6d440..e15043c8592 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -30,6 +30,11 @@ foreach(ITEM ${ITEMS}) # PLUGIN_RETRACTED variable in the parent scope so that we can access it here add_subdirectory(${ITEM}) + if(${ITEM} STREQUAL "testPlugin" AND NOT ${CMAKE_BUILD_TYPE} MATCHES Debug) + # The testPlugin is only included in Debug builds + continue() + endif() + if(PLUGIN_RETRACTED AND NOT retracted-plugins) # The included subdir didn't actually add a target since the associated plugin is retracted # and therefore it should not be built. diff --git a/plugins/HostLinux.cpp b/plugins/HostLinux.cpp index 8d53155a567..58feb673ed1 100644 --- a/plugins/HostLinux.cpp +++ b/plugins/HostLinux.cpp @@ -5,7 +5,7 @@ #include "HostLinux.h" -#include "mumble_plugin_utils.h" +#include "mumble_positional_audio_utils.h" #include #include diff --git a/plugins/HostWindows.cpp b/plugins/HostWindows.cpp index c354b4657f2..1c0574fc5ab 100644 --- a/plugins/HostWindows.cpp +++ b/plugins/HostWindows.cpp @@ -5,7 +5,7 @@ #include "HostWindows.h" -#include "mumble_plugin_utils.h" +#include "mumble_positional_audio_utils.h" #include #include diff --git a/plugins/MumbleAPI_v_1_0_x.h b/plugins/MumbleAPI_v_1_0_x.h new file mode 100644 index 00000000000..34b178f87c4 --- /dev/null +++ b/plugins/MumbleAPI_v_1_0_x.h @@ -0,0 +1,492 @@ +// Copyright 2021 The Mumble Developers. All rights reserved. +// Use of this source code is governed by a BSD-style license +// that can be found in the LICENSE file at the root of the +// Mumble source tree or at . + +/// This header file contains the definition of Mumble's API + +#ifndef MUMBLE_PLUGIN_API_H_ +#define MUMBLE_PLUGIN_API_H_ + +#include "PluginComponents_v_1_0_x.h" +#include + + +// API version +#define MUMBLE_PLUGIN_API_MAJOR_MACRO 1 +#define MUMBLE_PLUGIN_API_MINOR_MACRO 0 +#define MUMBLE_PLUGIN_API_PATCH_MACRO 0 + +constexpr int32_t MUMBLE_PLUGIN_API_MAJOR = MUMBLE_PLUGIN_API_MAJOR_MACRO; +constexpr int32_t MUMBLE_PLUGIN_API_MINOR = MUMBLE_PLUGIN_API_MINOR_MACRO; +constexpr int32_t MUMBLE_PLUGIN_API_PATCH = MUMBLE_PLUGIN_API_PATCH_MACRO; +const mumble_version_t MUMBLE_PLUGIN_API_VERSION = { MUMBLE_PLUGIN_API_MAJOR, MUMBLE_PLUGIN_API_MINOR, MUMBLE_PLUGIN_API_PATCH }; + +// Create macro for casting the pointer to the API object to the proper struct. +// Note that this must only be used if the API uses MUMBLE_PLUGIN_API_VERSION of the API. +#define MUMBLE_CONCAT_HELPER(a, b) a ## _ ## b +#define MUMBLE_CONCAT(a, b) MUMBLE_CONCAT_HELPER(a, b) +#define MUMBLE_API_STRUCT MUMBLE_CONCAT(MumbleAPI_v, MUMBLE_CONCAT(MUMBLE_PLUGIN_API_MAJOR_MACRO, MUMBLE_CONCAT(MUMBLE_PLUGIN_API_MINOR_MACRO, x))) +#define MUMBLE_API_CAST(ptrName) ( *((struct MUMBLE_API_STRUCT *) ptrName) ) + + +struct MumbleAPI_v_1_0_x { + ///////////////////////////////////////////////////////// + ////////////////////// GENERAL NOTES //////////////////// + ///////////////////////////////////////////////////////// + // + // All functions that take in a connection as a paremeter may only be called **after** the connection + // has finished synchronizing. The only exception from this is isConnectionSynchronized. + // + // Strings returned by the API are UTF-8 encoded + // Strings passed to the API are expected to be UTF-8 encoded + // + // All API functions are synchronized and will be executed in Mumble's "main thread" from which most plugin + // callbacks are called as well. Note however that an API call is BLOCKING if invoked from a different + // thread. This means that they can cause deadlocks if used without caution. An example that will lead + // to a deadlock is: + // - plugin callback gets called from the main thread + // - callback messages a separate thread to do something and waits for the action to have completed + // - Separate thread calls an API function + // - The function blocks and waits to be executed in the main thread which is currently blocked waiting + // - deadlock + + + // -------- Memory management -------- + + /// Frees the given pointer. + /// + /// @param callerID The ID of the plugin calling this function + /// @param pointer The pointer to free + /// @returns The error code. If everything went well, STATUS_OK will be returned. + mumble_error_t (PLUGIN_CALLING_CONVENTION *freeMemory)(mumble_plugin_id_t callerID, const void *pointer); + + + + // -------- Getter functions -------- + + /// Gets the connection ID of the server the user is currently active on (the user's audio output is directed at). + /// + /// @param callerID The ID of the plugin calling this function + /// @param[out] connection A pointer to the memory location the ID should be written to + /// @returns The error code. If everything went well, STATUS_OK will be returned. Only then it is valid to access the + /// value of the provided pointer + mumble_error_t (PLUGIN_CALLING_CONVENTION *getActiveServerConnection)(mumble_plugin_id_t callerID, mumble_connection_t *connection); + + /// Checks whether the given connection has finished initializing yet. + /// + /// @param callerID The ID of the plugin calling this function + /// @param connection The ID of the server-connection to use as a context + /// @param[out] A pointer to the boolean variable that'll hold the info whether the server has finished synchronization yet + /// after this function has executed successfully. + /// @returns The error code. If everything went well, STATUS_OK will be returned. Only then the passed pointer + /// may be accessed + mumble_error_t (PLUGIN_CALLING_CONVENTION *isConnectionSynchronized)(mumble_plugin_id_t callerID, mumble_connection_t connection, bool *synchronized); + + /// Fills in the information about the local user. + /// + /// @param callerID The ID of the plugin calling this function + /// @param connection The ID of the server-connection to use as a context + /// @param[out] userID A pointer to the memory the user's ID shall be written to + /// @returns The error code. If everything went well, STATUS_OK will be returned. Only then the passed pointer + /// may be accessed + mumble_error_t (PLUGIN_CALLING_CONVENTION *getLocalUserID)(mumble_plugin_id_t callerID, mumble_connection_t connection, mumble_userid_t *userID); + + /// Fills in the information about the given user's name. + /// + /// @param callerID The ID of the plugin calling this function + /// @param connection The ID of the server-connection to use as a context + /// @param userID The user's ID whose name should be obtained + /// @param[out] userName A pointer to where the pointer to the allocated string (C-encoded) should be written to. The + /// allocated memory has to be freed by a call to freeMemory by the plugin eventually. The memory will only be + /// allocated if this function returns STATUS_OK. + /// @returns The error code. If everything went well, STATUS_OK will be returned. Only then the passed pointer + /// may be accessed + mumble_error_t (PLUGIN_CALLING_CONVENTION *getUserName)(mumble_plugin_id_t callerID, mumble_connection_t connection, + mumble_userid_t userID, const char **userName); + + /// Fills in the information about the given channel's name. + /// + /// @param callerID The ID of the plugin calling this function + /// @param connection The ID of the server-connection to use as a context + /// @param channelID The channel's ID whose name should be obtained + /// @param[out] channelName A pointer to where the pointer to the allocated string (C-ecoded) should be written to. The + /// allocated memory has to be freed by a call to freeMemory by the plugin eventually. The memory will only be + /// allocated if this function returns STATUS_OK. + /// @returns The error code. If everything went well, STATUS_OK will be returned. Only then the passed pointer + /// may be accessed + mumble_error_t (PLUGIN_CALLING_CONVENTION *getChannelName)(mumble_plugin_id_t callerID, mumble_connection_t connection, + mumble_channelid_t channelID, const char **channelName); + + /// Gets an array of all users that are currently connected to the provided server. Passing a nullptr as any of the out-parameter + /// will prevent that property to be set/allocated. If you are only interested in the user count you can thus pass nullptr as the + /// users parameter and save time on allocating + freeing the channels-array while still getting the size out. + /// + /// @param callerID The ID of the plugin calling this function + /// @param connection The ID of the server-connection to use as a context + /// @param[out] users A pointer to where the pointer of the allocated array shall be written. The + /// allocated memory has to be freed by a call to freeMemory by the plugin eventually. The memory will only be + /// allocated if this function returns STATUS_OK. + /// @param[out] userCount A pointer to where the size of the allocated user-array shall be written to + /// @returns The error code. If everything went well, STATUS_OK will be returned. Only then the passed pointer + /// may be accessed + mumble_error_t (PLUGIN_CALLING_CONVENTION *getAllUsers)(mumble_plugin_id_t callerID, mumble_connection_t connection, mumble_userid_t **users, + size_t *userCount); + + /// Gets an array of all channels on the provided server. Passing a nullptr as any of the out-parameter will prevent + /// that property to be set/allocated. If you are only interested in the channel count you can thus pass nullptr as the + /// channels parameter and save time on allocating + freeing the channels-array while still getting the size out. + /// + /// @param callerID The ID of the plugin calling this function + /// @param connection The ID of the server-connection to use as a context + /// @param[out] channels A pointer to where the pointer of the allocated array shall be written. The + /// allocated memory has to be freed by a call to freeMemory by the plugin eventually. The memory will only be + /// allocated if this function returns STATUS_OK. + /// @param[out] channelCount A pointer to where the size of the allocated channel-array shall be written to + /// @returns The error code. If everything went well, STATUS_OK will be returned. Only then the passed pointer + /// may be accessed + mumble_error_t (PLUGIN_CALLING_CONVENTION *getAllChannels)(mumble_plugin_id_t callerID, mumble_connection_t connection, + mumble_channelid_t **channels, size_t *channelCount); + + /// Gets the ID of the channel the given user is currently connected to. + /// + /// @param callerID The ID of the plugin calling this function + /// @param connection The ID of the server-connection to use as a context + /// @param userID The ID of the user to search for + /// @param[out] A pointer to where the ID of the channel shall be written + /// @returns The error code. If everything went well, STATUS_OK will be returned. Only then the passed pointer + /// may be accessed + mumble_error_t (PLUGIN_CALLING_CONVENTION *getChannelOfUser)(mumble_plugin_id_t callerID, mumble_connection_t connection, mumble_userid_t userID, + mumble_channelid_t *channel); + + /// Gets an array of all users in the specified channel. + /// + /// @param callerID The ID of the plugin calling this function + /// @param connection The ID of the server-connection to use as a context + /// @param channelID The ID of the channel whose users shall be retrieved + /// @param[out] userList A pointer to where the pointer of the allocated array shall be written. The allocated memory has + /// to be freed by a call to freeMemory by the plugin eventually. The memory will only be allocated if this function + /// returns STATUS_OK. + /// @param[out] userCount A pointer to where the size of the allocated user-array shall be written to + /// @returns The error code. If everything went well, STATUS_OK will be returned. Only then the passed pointer + /// may be accessed + mumble_error_t (PLUGIN_CALLING_CONVENTION *getUsersInChannel)(mumble_plugin_id_t callerID, mumble_connection_t connection, + mumble_channelid_t channelID, mumble_userid_t **userList, size_t *userCount); + + /// Gets the current transmission mode of the local user. + /// + /// @param callerID The ID of the plugin calling this function + /// @param[out] transmissionMode A pointer to where the transmission mode shall be written. + /// @returns The error code. If everything went well, STATUS_OK will be returned. Only then the passed pointer + /// may be accessed + mumble_error_t (PLUGIN_CALLING_CONVENTION *getLocalUserTransmissionMode)(mumble_plugin_id_t callerID, mumble_transmission_mode_t *transmissionMode); + + /// Checks whether the given user is currently locally muted. + /// + /// @param callerID The ID of the plugin calling this function + /// @param connection The ID of the server-connection to use as a context + /// @param userID The ID of the user to check for + /// @param[out] muted A pointer to where the local mute state of that user shall be written + /// @returns The error code. If everything went well, STATUS_OK will be returned. Only then the passed pointer + /// may be accessed + mumble_error_t (PLUGIN_CALLING_CONVENTION *isUserLocallyMuted)(mumble_plugin_id_t callerID, mumble_connection_t connection, + mumble_userid_t userID, bool *muted); + + /// Checks whether the local user is currently muted. + /// + /// @param callerID The ID of the plugin calling this function + /// @param[out] muted A pointer to where the mute state of the local user shall be written + /// @returns The error code. If everything went well, STATUS_OK will be returned. Only then the passed pointer + /// may be accessed + mumble_error_t (PLUGIN_CALLING_CONVENTION *isLocalUserMuted)(mumble_plugin_id_t callerID, bool *muted); + + /// Checks whether the local user is currently deafened. + /// + /// @param callerID The ID of the plugin calling this function + /// @param[out] deafened A pointer to where the deaf state of the local user shall be written + /// @returns The error code. If everything went well, STATUS_OK will be returned. Only then the passed pointer + /// may be accessed + mumble_error_t (PLUGIN_CALLING_CONVENTION *isLocalUserDeafened)(mumble_plugin_id_t callerID, bool *deafened); + + /// Gets the hash of the given user (can be used to recognize users between restarts) + /// + /// @param callerID The ID of the plugin calling this function + /// @param connection The ID of the server-connection to use as a context + /// @param userID The ID of the user to search for + /// @param[out] hash A pointer to where the pointer to the allocated string (C-encoded) should be written to. The + /// allocated memory has to be freed by a call to freeMemory by the plugin eventually. The memory will only be + /// allocated if this function returns STATUS_OK. + /// @returns The error code. If everything went well, STATUS_OK will be returned. Only then the passed pointer + /// may be accessed + mumble_error_t (PLUGIN_CALLING_CONVENTION *getUserHash)(mumble_plugin_id_t callerID, mumble_connection_t connection, + mumble_userid_t userID, const char **hash); + + /// Gets the hash of the server for the given connection (can be used to recognize servers between restarts) + /// + /// @param callerID The ID of the plugin calling this function + /// @param connection The ID of the server-connection + /// @param[out] hash A pointer to where the pointer to the allocated string (C-encoded) should be written to. The + /// allocated memory has to be freed by a call to freeMemory by the plugin eventually. The memory will only be + /// allocated if this function returns STATUS_OK. + /// @returns The error code. If everything went well, STATUS_OK will be returned. Only then the passed pointer + /// may be accessed + mumble_error_t (PLUGIN_CALLING_CONVENTION *getServerHash)(mumble_plugin_id_t callerID, mumble_connection_t connection, const char **hash); + + /// Gets the comment of the given user. Note that a user might have a comment configured that hasn't been synchronized + /// to this client yet. In this case this function will return EC_UNSYNCHRONIZED_BLOB. As of now there is now way + /// to request the synchronization to happen via the Plugin-API. + /// + /// @param callerID The ID of the plugin calling this function + /// @param connection The ID of the server-connection + /// @param userID the ID of the user whose comment should be obtained + /// @param[out] comment A pointer to where the pointer to the allocated string (C-encoded) should be written to. The + /// allocated memory has to be freed by a call to freeMemory by the plugin eventually. The memory will only be + /// allocated if this function returns STATUS_OK. + /// @returns The error code. If everything went well, STATUS_OK will be returned. Only then the passed pointer + /// may be accessed + mumble_error_t (PLUGIN_CALLING_CONVENTION *getUserComment)(mumble_plugin_id_t callerID, mumble_connection_t connection, + mumble_userid_t userID, const char **comment); + + /// Gets the description of the given channel. Note that a channel might have a description configured that hasn't been synchronized + /// to this client yet. In this case this function will return EC_UNSYNCHRONIZED_BLOB. As of now there is now way + /// to request the synchronization to happen via the Plugin-API. + /// + /// @param callerID The ID of the plugin calling this function + /// @param connection The ID of the server-connection + /// @param channelID the ID of the channel whose comment should be obtained + /// @param[out] description A pointer to where the pointer to the allocated string (C-encoded) should be written to. The + /// allocated memory has to be freed by a call to freeMemory by the plugin eventually. The memory will only be + /// allocated if this function returns STATUS_OK. + /// @returns The error code. If everything went well, STATUS_OK will be returned. Only then the passed pointer + /// may be accessed + mumble_error_t (PLUGIN_CALLING_CONVENTION *getChannelDescription)(mumble_plugin_id_t callerID, mumble_connection_t connection, + mumble_channelid_t channelID, const char **description); + + + // -------- Request functions -------- + + /// Requests Mumble to set the local user's transmission mode to the specified one. If you only need to temporarily set + /// the transmission mode to continous, use requestMicrophoneActivationOverwrite instead as this saves you the work of + /// restoring the previous state afterwards. + /// + /// @param callerID The ID of the plugin calling this function + /// @param transmissionMode The requested transmission mode + /// @returns The error code. If everything went well, STATUS_OK will be returned. + mumble_error_t (PLUGIN_CALLING_CONVENTION *requestLocalUserTransmissionMode)(mumble_plugin_id_t callerID, mumble_transmission_mode_t transmissionMode); + + /// Requests Mumble to move the given user into the given channel + /// + /// @param callerID The ID of the plugin calling this function + /// @param connection The ID of the server-connection to use as a context + /// @param userID The ID of the user that shall be moved + /// @param channelID The ID of the channel to move the user to + /// @param password The password of the target channel (UTF-8 encoded as a C-string). Pass NULL if the target channel does not require a + /// password for entering + /// @returns The error code. If everything went well, STATUS_OK will be returned. + mumble_error_t (PLUGIN_CALLING_CONVENTION *requestUserMove)(mumble_plugin_id_t callerID, mumble_connection_t connection, mumble_userid_t userID, + mumble_channelid_t channelID, const char *password); + + /// Requests Mumble to overwrite the microphone activation so that the microphone is always on (same as if the user had chosen + /// the continous transmission mode). If a plugin requests this overwrite, it is responsible for deactivating the overwrite again + /// once it is no longer required + /// + /// @param callerID The ID of the plugin calling this function + /// @param activate Whether to activate the overwrite (false deactivates an existing overwrite) + /// @returns The error code. If everything went well, STATUS_OK will be returned. + mumble_error_t (PLUGIN_CALLING_CONVENTION *requestMicrophoneActivationOvewrite)(mumble_plugin_id_t callerID, bool activate); + + /// Requests Mumble to set the local mute state of the given client. Note that this only affects the **local** mute state + /// opposed to a server-mute (client is globally muted by the server) or the client's own mute-state (client has muted its + /// microphone and thus isn't transmitting any audio). + /// Furthermore it must be noted that muting the local user with this function does not work (it doesn't make sense). If + /// you try to do so, this function will fail. In order to make this work, this function will also fail if the server + /// has not finished synchronizing with the client yet. + /// For muting the local user, use requestLocalUserMute instead. + /// + /// @param callerID The ID of the plugin calling this function. + /// @param connection The ID of the server-connection to use as a context + /// @param userID The ID of the user that shall be muted + /// @param muted Whether to locally mute the given client (opposed to unmuting it) + /// @returns The error code. If everything went well, STATUS_OK will be returned. + mumble_error_t (PLUGIN_CALLING_CONVENTION *requestLocalMute)(mumble_plugin_id_t callerID, mumble_connection_t connection, + mumble_userid_t userID, bool muted); + + /// Requests Mumble to set the mute state of the local user. In the UI this is referred to as "self-mute". + /// + /// @param callerID The ID of the plugin calling this function. + /// @param muted Whether to locally mute the local user (opposed to unmuting it) + /// @returns The error code. If everything went well, STATUS_OK will be returned. + mumble_error_t (PLUGIN_CALLING_CONVENTION *requestLocalUserMute)(mumble_plugin_id_t callerID, bool muted); + + /// Requests Mumble to set the deaf state of the local user. In the UI this is referred to as "self-deaf". + /// + /// @param callerID The ID of the plugin calling this function. + /// @param deafened Whether to locally deafen the local user (opposed to undeafening it) + /// @returns The error code. If everything went well, STATUS_OK will be returned. + mumble_error_t (PLUGIN_CALLING_CONVENTION *requestLocalUserDeaf)(mumble_plugin_id_t callerID, bool deafened); + + /// Sets the comment of the local user + /// + /// @param callerID The ID of the plugin calling this function + /// @param connection The ID of the server-connection + /// @param comment The new comment to use (C-encoded). A subset of HTML formatting is supported. + /// @returns The error code. If everything went well, STATUS_OK will be returned. Only then the passed pointer + /// may be accessed + mumble_error_t (PLUGIN_CALLING_CONVENTION *requestSetLocalUserComment)(mumble_plugin_id_t callerID, mumble_connection_t connection, + const char *comment); + + + + // -------- Find functions -------- + + /// Fills in the information about a user with the specified name, if such a user exists. The search is case-sensitive. + /// + /// @param callerID The ID of the plugin calling this function + /// @param connection The ID of the server-connection to use as a context + /// @param userName The respective user's name + /// @param[out] userID A pointer to the memory the user's ID shall be written to + /// @returns The error code. If everything went well, STATUS_OK will be returned. Only then the passed pointer may + /// be accessed. + mumble_error_t (PLUGIN_CALLING_CONVENTION *findUserByName)(mumble_plugin_id_t callerID, mumble_connection_t connection, const char *userName, + mumble_userid_t *userID); + + /// Fills in the information about a channel with the specified name, if such a channel exists. The search is case-sensitive. + /// + /// @param callerID The ID of the plugin calling this function + /// @param connection The ID of the server-connection to use as a context + /// @param channelName The respective channel's name + /// @param[out] channelID A pointer to the memory the channel's ID shall be written to + /// @returns The error code. If everything went well, STATUS_OK will be returned. Only then the passed pointer may + /// be accessed. + mumble_error_t (PLUGIN_CALLING_CONVENTION *findChannelByName)(mumble_plugin_id_t callerID, mumble_connection_t connection, + const char *channelName, mumble_channelid_t *channelID); + + + + // -------- Settings -------- + + /// Fills in the current value of the setting with the given key. Note that this function can only be used for settings whose value + /// is a bool! + /// + /// @param callerID The ID of the plugin calling this function + /// @param key The key to the desired setting + /// @param[out] outValue A pointer to the memory the setting's value shall be written to. + /// @returns The error code. If everything went well, STATUS_OK will be returned. Only then the passed pointer may + /// be accessed. + mumble_error_t (PLUGIN_CALLING_CONVENTION *getMumbleSetting_bool)(mumble_plugin_id_t callerID, mumble_settings_key_t key, bool *outValue); + + /// Fills in the current value of the setting with the given key. Note that this function can only be used for settings whose value + /// is an int! + /// + /// @param callerID The ID of the plugin calling this function + /// @param key The key to the desired setting + /// @param[out] outValue A pointer to the memory the setting's value shall be written to. + /// @returns The error code. If everything went well, STATUS_OK will be returned. Only then the passed pointer may + /// be accessed. + mumble_error_t (PLUGIN_CALLING_CONVENTION *getMumbleSetting_int)(mumble_plugin_id_t callerID, mumble_settings_key_t key, int64_t *outValue); + + /// Fills in the current value of the setting with the given key. Note that this function can only be used for settings whose value + /// is a double! + /// + /// @param callerID The ID of the plugin calling this function + /// @param key The key to the desired setting + /// @param[out] outValue A pointer to the memory the setting's value shall be written to. + /// @returns The error code. If everything went well, STATUS_OK will be returned. Only then the passed pointer may + /// be accessed. + mumble_error_t (PLUGIN_CALLING_CONVENTION *getMumbleSetting_double)(mumble_plugin_id_t callerID, mumble_settings_key_t key, double *outValue); + + /// Fills in the current value of the setting with the given key. Note that this function can only be used for settings whose value + /// is a String! + /// + /// @param callerID The ID of the plugin calling this function + /// @param key The key to the desired setting + /// @param[out] outValue The memory address to which the pointer to the setting's value (the String) will be written. The + /// allocated memory has to be freed by a call to freeMemory by the plugin eventually. The memory will only be + /// allocated if this function returns STATUS_OK. + /// @returns The error code. If everything went well, STATUS_OK will be returned. Only then the passed pointer may + /// be accessed. + mumble_error_t (PLUGIN_CALLING_CONVENTION *getMumbleSetting_string)(mumble_plugin_id_t callerID, mumble_settings_key_t key, const char **outValue); + + + /// Sets the value of the setting with the given key. Note that this function can only be used for settings whose value + /// is a bool! + /// + /// @param callerID The ID of the plugin calling this function + /// @param key The key to the desired setting + /// @param value The value that should be set for the given setting + /// @returns The error code. If everything went well, STATUS_OK will be returned. + mumble_error_t (PLUGIN_CALLING_CONVENTION *setMumbleSetting_bool)(mumble_plugin_id_t callerID, mumble_settings_key_t key, bool value); + + /// Sets the value of the setting with the given key. Note that this function can only be used for settings whose value + /// is an int! + /// + /// @param callerID The ID of the plugin calling this function + /// @param key The key to the desired setting + /// @param value The value that should be set for the given setting + /// @returns The error code. If everything went well, STATUS_OK will be returned. + mumble_error_t (PLUGIN_CALLING_CONVENTION *setMumbleSetting_int)(mumble_plugin_id_t callerID, mumble_settings_key_t key, int64_t value); + + /// Sets the value of the setting with the given key. Note that this function can only be used for settings whose value + /// is a double! + /// + /// @param callerID The ID of the plugin calling this function + /// @param key The key to the desired setting + /// @param value The value that should be set for the given setting + /// @returns The error code. If everything went well, STATUS_OK will be returned. + mumble_error_t (PLUGIN_CALLING_CONVENTION *setMumbleSetting_double)(mumble_plugin_id_t callerID, mumble_settings_key_t key, double value); + + /// Sets the value of the setting with the given key. Note that this function can only be used for settings whose value + /// is a string! + /// + /// @param callerID The ID of the plugin calling this function + /// @param key The key to the desired setting + /// @param value The value that should be set for the given setting + /// @returns The error code. If everything went well, STATUS_OK will be returned. + mumble_error_t (PLUGIN_CALLING_CONVENTION *setMumbleSetting_string)(mumble_plugin_id_t callerID, mumble_settings_key_t key, const char *value); + + + + + // -------- Miscellaneous -------- + + /// Sends the provided data to the provided client(s). This kind of data can only be received by another plugin active + /// on that client. The sent data can be seen by any active plugin on the receiving client. Therefore the sent data + /// must not contain sensitive information or anything else that shouldn't be known by others. + /// + /// NOTE: Messages sent via this API function are rate-limited by the server. If the rate-limit is hit, the message + /// will be dropped without an error message. The rate-limiting is global (e.g. it doesn't matter which plugin sent + /// the respective messages - they all count to the same limit). + /// Therefore if you have multiple messages to send, you should consider sending them asynchronously one at a time + /// with a little delay in between (~1 second). + /// + /// @param callerID The ID of the plugin calling this function + /// @param connection The ID of the server-connection to send the data through (the server the given users are on) + /// @param users An array of user IDs to send the data to + /// @param userCount The size of the provided user-array + /// @param data The data array that shall be sent. This can be an arbitrary sequence of bytes. Note that the size of + /// is restricted to <= 1KB. + /// @param dataLength The length of the data array + /// @param dataID The ID of the sent data. This has to be used by the receiving plugin(s) to figure out what to do with + /// the data. This has to be a C-encoded String. It is recommended that the ID starts with a plugin-specific prefix + /// in order to avoid name clashes. Note that the size of this string is restricted to <= 100 bytes. + /// @returns The error code. If everything went well, STATUS_OK will be returned. + mumble_error_t (PLUGIN_CALLING_CONVENTION *sendData)(mumble_plugin_id_t callerID, mumble_connection_t connection, const mumble_userid_t *users, + size_t userCount, const uint8_t *data, size_t dataLength, const char *dataID); + + /// Logs the given message (typically to Mumble's console). All passed strings have to be UTF-8 encoded. + /// + /// @param callerID The ID of the plugin calling this function + /// @param message The message to log + /// @returns The error code. If everything went well, STATUS_OK will be returned. + mumble_error_t (PLUGIN_CALLING_CONVENTION *log)(mumble_plugin_id_t callerID, const char *message); + + /// Plays the provided sample. It uses libsndfile as a backend so the respective file format needs to be supported by it + /// in order for this to work out (see http://www.mega-nerd.com/libsndfile/). + /// + /// @param callerID The ID of the plugin calling this function + /// @param samplePath The path to the sample that shall be played (UTF-8 encoded) + /// @returns The error code. If everything went well, STATUS_OK will be returned. + mumble_error_t (PLUGIN_CALLING_CONVENTION *playSample)(mumble_plugin_id_t callerID, const char *samplePath); +}; + +#endif diff --git a/plugins/MumblePlugin_v_1_0_x.h b/plugins/MumblePlugin_v_1_0_x.h new file mode 100644 index 00000000000..f3feb740b73 --- /dev/null +++ b/plugins/MumblePlugin_v_1_0_x.h @@ -0,0 +1,402 @@ +// Copyright 2021 The Mumble Developers. All rights reserved. +// Use of this source code is governed by a BSD-style license +// that can be found in the LICENSE file at the root of the +// Mumble source tree or at . + +/// This header file specifies the Mumble plugin interface + +#ifndef EXTERNAL_MUMBLE_PLUGIN_H_ +#define EXTERNAL_MUMBLE_PLUGIN_H_ + +#include "PluginComponents_v_1_0_x.h" +#include "MumbleAPI_v_1_0_x.h" +#include +#include +#include + +#if defined(__GNUC__) && !defined(__MINGW32__) // GCC on Unix-like systems + #define PLUGIN_EXPORT __attribute__((visibility("default"))) +#elif defined(_MSC_VER) + #define PLUGIN_EXPORT __declspec(dllexport) +#elif defined(__MINGW32__) + #define PLUGIN_EXPORT __attribute__((dllexport)) +#else + #error No PLUGIN_EXPORT definition available +#endif + + +#ifdef __cplusplus +extern "C" { +#endif + + ////////////////////////////////////////////////////////////////////////////////// + ////////////////////////////// MANDATORY FUNCTIONS /////////////////////////////// + ////////////////////////////////////////////////////////////////////////////////// + + /// Gets called right after loading the plugin in order to let the plugin initialize. + /// + /// Registers the ID of this plugin. + /// @param id The ID for this plugin. This is the ID Mumble will reference this plugin with + /// and by which this plugin can identify itself when communicating with Mumble. + /// @returns The status of the initialization. If everything went fine, return STATUS_OK + PLUGIN_EXPORT mumble_error_t PLUGIN_CALLING_CONVENTION mumble_init(uint32_t id); + + /// Gets called when unloading the plugin in order to allow it to clean up after itself. + /// Note that it is still safe to call API functions from within this callback. + PLUGIN_EXPORT void PLUGIN_CALLING_CONVENTION mumble_shutdown(); + + /// Gets the name of the plugin. + /// + /// NOTE: This function may be called without the plugin being loaded + /// + /// @returns A String-wrapper containing the requested name + PLUGIN_EXPORT struct MumbleStringWrapper PLUGIN_CALLING_CONVENTION mumble_getName(); + + /// Gets the Version of the plugin-API this plugin intends to use. + /// Mumble will decide whether this plugin is loadable or not based on the return value of this function. + /// + /// NOTE: This function may be called without the plugin being loaded + /// + /// @returns The respective API Version + PLUGIN_EXPORT mumble_version_t PLUGIN_CALLING_CONVENTION mumble_getAPIVersion(); + + /// Provides the MumbleAPI struct to the plugin. This struct contains function pointers that can be used + /// to interact with the Mumble client. It is up to the plugin to store this struct somewhere if it wants to make use + /// of it at some point. + /// + /// NOTE: This function may be called without the plugin being loaded + /// + /// @param api A pointer to the MumbleAPI struct. The API struct must be cast to the version corresponding to the + /// user API version. If your plugin is e.g. using the 1.0.x API, then you have to cast this pointer to + /// MumbleAPI_v_1_0_x. Note also that you **must not store this pointer**. It will become invalid. Therefore + /// you have to copy the struct in order to use it later on. + PLUGIN_EXPORT void PLUGIN_CALLING_CONVENTION mumble_registerAPIFunctions(void *apiStruct); + + /// Releases the resource pointed to by the given pointer. If the respective resource has been allocated before, + /// this would be the time to free/delete it. + /// The resources processed by this functions are only those that have been specifically allocated in order to return + /// them in one of the plugin functions to Mumble (e.g. the String returned by mumble_getName) and has nothing to do + /// with your plugin's internal resource management. + /// In short: Only resources passed from the plugin to Mumble via a return value may be processed by this function. + /// + /// NOTE1: This function may be called without the plugin being loaded + /// + /// NOTE2: that the pointer might be pointing to memory that had to be allocated without the plugin being loaded. + /// Therefore you should be very sure that there'll be another callback in which you want to free this memory, + /// should you decide to not do it here (which is hereby explicitly advised against). + /// + /// NOTE3: The pointer is const as Mumble won't mess with the memory allocated by the plugin (no modifications). + /// Nontheless this function is explicitly responsible for freeing the respective memory parts. If the memory has + /// been allocated using malloc(), it needs to be freed using free() which requires a const-cast. If however the + /// memory has been created using the new operator you have to cast the pointer back to its original type and then + /// use the delete operator on it (no const-cast necessary in this case). + /// See https://stackoverflow.com/questions/2819535/unable-to-free-const-pointers-in-c + /// and https://stackoverflow.com/questions/941832/is-it-safe-to-delete-a-void-pointer + /// + /// @param pointer The pointer to the memory that needs free-ing + PLUGIN_EXPORT void PLUGIN_CALLING_CONVENTION mumble_releaseResource(const void *pointer); + + + + ////////////////////////////////////////////////////////////////////////////////// + ///////////////////////////// GENERAL FUNCTIONS ////////////////////////////////// + ////////////////////////////////////////////////////////////////////////////////// + + /// Tells the plugin some basic information about the Mumble client loading it. + /// This function will be the first one that is being called on this plugin - even before it is decided whether to load + /// the plugin at all. + /// + /// @param mumbleVersion The Version of the Mumble client + /// @param mumbleAPIVersion The Version of the plugin-API the Mumble client runs with + /// @param minimumExpectedAPIVersion The minimum Version the Mumble clients expects this plugin to meet in order to load it + PLUGIN_EXPORT void PLUGIN_CALLING_CONVENTION mumble_setMumbleInfo(mumble_version_t mumbleVersion, mumble_version_t mumbleAPIVersion, mumble_version_t minimumExpectedAPIVersion); + + /// Gets the Version of this plugin + /// + /// NOTE: This function may be called without the plugin being loaded + /// + /// @returns The plugin's version + PLUGIN_EXPORT mumble_version_t PLUGIN_CALLING_CONVENTION mumble_getVersion(); + + /// Gets the name of the plugin author(s). + /// + /// NOTE: This function may be called without the plugin being loaded + /// + /// @returns A String-wrapper containing the requested author name(s) + PLUGIN_EXPORT struct MumbleStringWrapper PLUGIN_CALLING_CONVENTION mumble_getAuthor(); + + /// Gets the description of the plugin. + /// + /// NOTE: This function may be called without the plugin being loaded + /// + /// @returns A String-wrapper containing the requested description + PLUGIN_EXPORT struct MumbleStringWrapper PLUGIN_CALLING_CONVENTION mumble_getDescription(); + + /// Gets the feature set of this plugin. The feature set is described by bitwise or'ing the elements of the Mumble_PluginFeature enum + /// together. + /// + /// NOTE: This function may be called without the plugin being loaded + /// + /// @returns The feature set of this plugin + PLUGIN_EXPORT uint32_t PLUGIN_CALLING_CONVENTION mumble_getFeatures(); + + /// Requests this plugin to deactivate the given (sub)set of provided features. + /// If this is not possible, the features that can't be deactivated shall be returned by this function. + /// + /// Example (check if FEATURE_POSITIONAL shall be deactivated): + /// @code + /// if (features & FEATURE_POSITIONAL) { + /// // positional shall be deactivated + /// }; + /// @endcode + /// + /// @param features The feature set that shall be deactivated + /// @returns The feature set that can't be disabled (bitwise or'ed). If all requested features can be disabled, return + /// FEATURE_NONE. If none of the requested features can be disabled return the unmodified features parameter. + PLUGIN_EXPORT uint32_t PLUGIN_CALLING_CONVENTION mumble_deactivateFeatures(uint32_t features); + + + + ////////////////////////////////////////////////////////////////////////////////// + //////////////////////////// POSITIONAL DATA ///////////////////////////////////// + ////////////////////////////////////////////////////////////////////////////////// + // If this plugin wants to provide positional data, ALL functions of this category + // have to be implemented + + /// Indicates that Mumble wants to use this plugin to request positional data. Therefore it should check whether it is currently + /// able to do so and allocate memory that is needed for that process. + /// As a parameter this function gets an array of names and an array of PIDs. They are of same length and the PID at index i + /// belongs to a program whose name is listed at index i in the "name-array". + /// + /// @param programNames An array of pointers to the program names + /// @param programPIDs An array of the corresponding program PIDs + /// @param programCount The length of programNames and programPIDs + /// @returns The error code. If everything went fine PDEC_OK shall be returned. In that case Mumble will start frequently + /// calling fetchPositionalData. If this returns anything but PDEC_OK, Mumble will assume that the plugin is (currently) + /// uncapable of providing positional data. In this case this function must not have allocated any memory that needs to be + /// cleaned up later on. Depending on the returned error code, Mumble might try to call this function again at some point. + PLUGIN_EXPORT uint8_t PLUGIN_CALLING_CONVENTION mumble_initPositionalData(const char *const*programNames, const uint64_t *programPIDs, size_t programCount); + + /// Retrieves the positional data. If no data can be fetched, set all float-vectors to 0 and return false. + /// + /// @param[out] avatarPos A float-array of size 3 representing the cartesian position of the player/avatar in the ingame world. + /// One unit represents one meter of distance. + /// @param[out] avatarDir A float-array of size 3 representing the cartesian direction-vector of the player/avatar ingame (where it + /// is facing). + /// @param[out] avatarAxis A float-array of size 3 representing the vector pointing from the toes of the character to its head. One + /// unit represents one meter of distance. + /// @param[out] cameraPos A float-array of size 3 representing the cartesian position of the camera in the ingame world. + /// One unit represents one meter of distance. + /// @param[out] cameraDir A float-array of size 3 representing the cartesian direction-vector of the camera ingame (where it + /// is facing). + /// @param[out] cameraAxis A float-array of size 3 representing a vector from the bottom of the camera to its top. One unit + /// represents one meter of distance. + /// @param[out] context A pointer to where the pointer to a C-encoded string storing the context of the provided positional data + /// shall be written. This context should include information about the server (and team) the player is on. Only players with identical + /// context will be able to hear each other's audio. The returned pointer has to remain valid until the next invokation of this function + /// or until shutdownPositionalData is called. + /// @param[out] identity A pointer to where the pointer to a C-encoded string storing the identity of the player shall be written. It can + /// be polled by external scripts from the server and should uniquely identify the player in the game. The pointer has to remain valid + /// until the next invokation of this function or until shutdownPositionalData is called. + /// @returns Whether this plugin can continue delivering positional data. If this function returns false, shutdownPositionalData will + /// be called. + PLUGIN_EXPORT bool PLUGIN_CALLING_CONVENTION mumble_fetchPositionalData(float *avatarPos, float *avatarDir, float *avatarAxis, float *cameraPos, float *cameraDir, + float *cameraAxis, const char **context, const char **identity); + + /// Indicates that this plugin will not be asked for positional data any longer. Thus any memory allocated for this purpose should + /// be freed at this point. + PLUGIN_EXPORT void PLUGIN_CALLING_CONVENTION mumble_shutdownPositionalData(); + + + + ////////////////////////////////////////////////////////////////////////////////// + ////////////////////// EVENTHANDLERS / CALLBACK FUNCTIONS //////////////////////// + ////////////////////////////////////////////////////////////////////////////////// + + /// Called when connecting to a server. + /// Note that in most cases you'll want to use mumble_onServerSynchronized instead. + /// Note also that this callback will be called from a DIFFERENT THREAD! + /// + /// @param connection The ID of the newly established server-connection + PLUGIN_EXPORT void PLUGIN_CALLING_CONVENTION mumble_onServerConnected(mumble_connection_t connection); + + /// Called when disconnecting from a server. + /// Note that this callback is called from a DIFFERENT THREAD! + /// + /// @param connection The ID of the server-connection that has been terminated + PLUGIN_EXPORT void PLUGIN_CALLING_CONVENTION mumble_onServerDisconnected(mumble_connection_t connection); + + /// Called when the client has finished synchronizing with the server + /// + /// @param connection The ID of the server-connection that has been terminated + PLUGIN_EXPORT void PLUGIN_CALLING_CONVENTION mumble_onServerSynchronized(mumble_connection_t connection); + + /// Called whenever any user on the server enters a channel + /// This function will also be called when freshly connecting to a server as each user on that + /// server needs to be "added" to the respective channel as far as the local client is concerned. + /// + /// @param connection The ID of the server-connection this event is connected to + /// @param userID The ID of the user this event has been triggered for + /// @param previousChannelID The ID of the chanel the user is coming from. Negative IDs indicate that there is no previous channel (e.g. the user + /// freshly connected to the server) or the channel isn't available because of any other reason. + /// @param newChannelID The ID of the channel the user has entered. If the ID is negative, the new channel could not be retrieved. This means + /// that the ID is invalid. + PLUGIN_EXPORT void PLUGIN_CALLING_CONVENTION mumble_onChannelEntered(mumble_connection_t connection, mumble_userid_t userID, mumble_channelid_t previousChannelID, + mumble_channelid_t newChannelID); + + /// Called whenever a user leaves a channel. + /// This includes a client disconnecting from the server as this will also lead to the user not being in that channel anymore. + /// + /// @param connection The ID of the server-connection this event is connected to + /// @param userID The ID of the user that left the channel + /// @param channelID The ID of the channel the user left. If the ID is negative, the channel could not be retrieved. This means that the ID is + /// invalid. + PLUGIN_EXPORT void PLUGIN_CALLING_CONVENTION mumble_onChannelExited(mumble_connection_t connection, mumble_userid_t userID, mumble_channelid_t channelID); + + /// Called when any user changes his/her talking state. + /// + /// @param connection The ID of the server-connection this event is connected to + /// @param userID The ID of the user whose talking state has been changed + /// @param talkingState The new TalkingState the user has switched to. + PLUGIN_EXPORT void PLUGIN_CALLING_CONVENTION mumble_onUserTalkingStateChanged(mumble_connection_t connection, mumble_userid_t userID, mumble_talking_state_t talkingState); + + /// Called whenever there is audio input. + /// Note that this callback will be called from the AUDIO THREAD. + /// Note also that blocking this callback will cause Mumble's audio processing to get suspended. + /// + /// @param inputPCM A pointer to a short-array holding the pulse-code-modulation (PCM) representing the audio input. Its length + /// is sampleCount * channelCount. The PCM format for stereo input is [LRLRLR...] where L and R are samples of the left and right + /// channel respectively. + /// @param sampleCount The amount of sample points per channel + /// @param channelCount The amount of channels in the audio + /// @param sampleRate The used sample rate in Hz + /// @param isSpeech A boolean flag indicating whether Mumble considers the input as part of speech (instead of background noise) + /// @returns Whether this callback has modified the audio input-array + PLUGIN_EXPORT bool PLUGIN_CALLING_CONVENTION mumble_onAudioInput(short *inputPCM, uint32_t sampleCount, uint16_t channelCount, + uint32_t sampleRate, bool isSpeech); + + /// Called whenever Mumble fetches data from an active audio source (could be a voice packet or a playing sample). + /// The provided audio buffer is the raw buffer without any processing applied to it yet. + /// Note that this callback will be called from the AUDIO THREAD. + /// Note also that blocking this callback will cause Mumble's audio processing to get suspended. + /// + /// @param outputPCM A pointer to a float-array holding the pulse-code-modulation (PCM) representing the audio output. Its length + /// is sampleCount * channelCount. The PCM format for stereo output is [LRLRLR...] where L and R are samples of the left and right + /// channel respectively. + /// @param sampleCount The amount of sample points per channel + /// @param channelCount The amount of channels in the audio + /// @param sampleRate The used sample rate in Hz + /// @param isSpeech Whether this audio belongs to a received voice packet (and will thus (most likely) contain speech) + /// @param userID If isSpeech is true, this contains the ID of the user this voice packet belongs to. If isSpeech is false, + /// the content of this parameter is unspecified and should not be accessed + /// @returns Whether this callback has modified the audio output-array + PLUGIN_EXPORT bool PLUGIN_CALLING_CONVENTION mumble_onAudioSourceFetched(float *outputPCM, uint32_t sampleCount, uint16_t channelCount, + uint32_t sampleRate, bool isSpeech, mumble_userid_t userID); + + /// Called whenever the fully mixed and processed audio is about to be handed to the audio backend (about to be played). + /// Note that this happens immediately before Mumble clips the audio buffer. + /// Note that this callback will be called from the AUDIO THREAD. + /// Note also that blocking this callback will cause Mumble's audio processing to get suspended. + /// + /// @param outputPCM A pointer to a float-array holding the pulse-code-modulation (PCM) representing the audio output. Its length + /// is sampleCount * channelCount. The PCM format for stereo output is [LRLRLR...] where L and R are samples of the left and right + /// channel respectively. + /// @param sampleCount The amount of sample points per channel + /// @param channelCount The amount of channels in the audio + /// @param sampleRate The used sample rate in Hz + /// @returns Whether this callback has modified the audio output-array + PLUGIN_EXPORT bool PLUGIN_CALLING_CONVENTION mumble_onAudioOutputAboutToPlay(float *outputPCM, uint32_t sampleCount, uint16_t channelCount, + uint32_t sampleRate); + + /// Called whenever data has been received that has been sent by a plugin. This data should only be processed by the + /// intended plugin. For this reason a dataID is provided that should be used to determine whether the data is intended + /// for this plugin or not. As soon as the data has been processed, no further plugins will be notified about it. + /// + /// @param connection The ID of the server-connection the data is coming from + /// @param sender The ID of the user whose client's plugin has sent the data + /// @param data The sent data array. This can be an arbitrary sequence of bytes. + /// @param dataLength The length of the data array + /// @param dataID The ID of this data (C-encoded) + /// @return Whether the given data has been processed by this plugin + PLUGIN_EXPORT bool PLUGIN_CALLING_CONVENTION mumble_onReceiveData(mumble_connection_t connection, mumble_userid_t sender, + const uint8_t *data, size_t dataLength, const char *dataID); + + /// Called when a new user gets added to the user model. This is the case when that new user freshly connects to the server the + /// local user is on but also when the local user connects to a server other clients are already connected to (in this case this + /// method will be called for every client already on that server). + /// + /// @param connection An object used to identify the current connection + /// @param userID The ID of the user that has been added + + PLUGIN_EXPORT void PLUGIN_CALLING_CONVENTION mumble_onUserAdded(mumble_connection_t connection, mumble_userid_t userID); + + /// Called when a user gets removed from the user model. This is the case when that user disconnects from the server the + /// local user is on but also when the local user disconnects from a server other clients are connected to (in this case this + /// method will be called for every client on that server). + /// + /// @param connection An object used to identify the current connection + /// @param userID The ID of the user that has been removed + PLUGIN_EXPORT void PLUGIN_CALLING_CONVENTION mumble_onUserRemoved(mumble_connection_t connection, mumble_userid_t userID); + + /// Called when a new channel gets added to the user model. This is the case when a new channel is created on the server the local + /// user is on but also when the local user connects to a server that contains channels other than the root-channel (in this case + /// this method will be called for ever non-root channel on that server). + /// + /// @param connection An object used to identify the current connection + /// @param channelID The ID of the channel that has been added + PLUGIN_EXPORT void PLUGIN_CALLING_CONVENTION mumble_onChannelAdded(mumble_connection_t connection, mumble_channelid_t channelID); + + /// Called when a channel gets removed from the user model. This is the case when a channel is removed on the server the local + /// user is on but also when the local user disconnects from a server that contains channels other than the root-channel (in this case + /// this method will be called for ever non-root channel on that server). + /// + /// @param connection An object used to identify the current connection + /// @param channelID The ID of the channel that has been removed + PLUGIN_EXPORT void PLUGIN_CALLING_CONVENTION mumble_onChannelRemoved(mumble_connection_t connection, mumble_channelid_t channelID); + + /// Called when a channel gets renamed. This also applies when a new channel is created (thus assigning it an initial name is also + /// considered renaming). + /// + /// @param connection An object used to identify the current connection + /// @param channelID The ID of the channel that has been renamed + PLUGIN_EXPORT void PLUGIN_CALLING_CONVENTION mumble_onChannelRenamed(mumble_connection_t connection, mumble_channelid_t channelID); + + /// Called when a key has been pressed or released while Mumble has keyboard focus. + /// Note that this callback will only work if the user has explicitly given permission to monitor keyboard + /// events for this plugin. Thus if you want to use this callback, make sure your users know that they have to + /// enable that. + /// + /// @param keyCode The key code of the respective key. The character codes are defined + /// via the Mumble_KeyCode enum. For printable 7-bit ASCII characters these codes conform + /// to the ASCII code-page with the only difference that case is not distinguished. Therefore + /// always the upper-case letter code will be used for letters. + /// @param wasPres Whether the respective key has been pressed (instead of released) + PLUGIN_EXPORT void PLUGIN_CALLING_CONVENTION mumble_onKeyEvent(uint32_t keyCode, bool wasPress); + + + + ////////////////////////////////////////////////////////////////////////////////// + ////////////////////////////// PLUGIN UPDATES //////////////////////////////////// + ////////////////////////////////////////////////////////////////////////////////// + + /// This function is used to determine whether the plugin can find an update for itself that is available for download. + /// + /// NOTE: This function may be called without the plugin being loaded + /// + /// @return Whether the plugin was able to find an update for itself + PLUGIN_EXPORT bool PLUGIN_CALLING_CONVENTION mumble_hasUpdate(); + + /// This function is used to retrieve the URL for downloading the newer/updated version of this plugin. + /// + /// NOTE: This function may be called without the plugin being loaded + /// + /// @returns A String-wrapper containing the requested URL + PLUGIN_EXPORT struct MumbleStringWrapper PLUGIN_CALLING_CONVENTION mumble_getUpdateDownloadURL(); + + +#ifdef __cplusplus +} +#endif + + +#endif diff --git a/plugins/PluginComponents_v_1_0_x.h b/plugins/PluginComponents_v_1_0_x.h new file mode 100644 index 00000000000..628283f2259 --- /dev/null +++ b/plugins/PluginComponents_v_1_0_x.h @@ -0,0 +1,411 @@ +// Copyright 2021 The Mumble Developers. All rights reserved. +// Use of this source code is governed by a BSD-style license +// that can be found in the LICENSE file at the root of the +// Mumble source tree or at . + +/// This header file contains definitions of types and other components used in Mumble's plugin system + +#ifndef MUMBLE_PLUGINCOMPONENT_H_ +#define MUMBLE_PLUGINCOMPONENT_H_ + +#include +#include +#include + +#ifdef __cplusplus +# include +#endif + +#ifdef QT_VERSION + #include +#endif + +// define the calling convention macro based on the compiler being used +#if defined(_MSC_VER) + #define PLUGIN_CALLING_CONVENTION __cdecl +#elif defined(__MINGW32__) + #define PLUGIN_CALLING_CONVENTION __attribute__((cdecl)) +#else + #define PLUGIN_CALLING_CONVENTION +#endif + + +/// A macro holding the exit status of a successful operation +#define STATUS_OK EC_OK +/// A macro holding the version object that is considered to correspond to an unknown version +#define VERSION_UNKNOWN Version({0,0,0}) + + +/// This enum's values correspond to special feature sets a plugin may provide. +/// They are meant to be or'ed together to represent the total feature set of a plugin. +enum Mumble_PluginFeature { + /// None of the below + FEATURE_NONE = 0, + /// The plugin provides positional data from a game + FEATURE_POSITIONAL = 1 << 0, + /// The plugin modifies the input/output audio itself + FEATURE_AUDIO = 1 << 1 +}; + +/// This enum's values represent talking states a user can be in when using Mumble. +enum Mumble_TalkingState { + INVALID=-1, + PASSIVE=0, + TALKING, + WHISPERING, + SHOUTING, + TALKING_MUTED +}; + +/// This enum's values represent transmission modes a user might have configured. Transmission mode +/// in this context is referring to a method that determines when a user is speaking and thus when +/// to transmit audio packets. +enum Mumble_TransmissionMode { + TM_CONTINOUS, + TM_VOICE_ACTIVATION, + TM_PUSH_TO_TALK +}; + +/// This enum's values represent the error codes that are being used by the MumbleAPI. +/// You can get a string-representation for each error code via the errorMessage function. +enum Mumble_ErrorCode { + EC_INTERNAL_ERROR = -2, + EC_GENERIC_ERROR = -1, + EC_OK = 0, + EC_POINTER_NOT_FOUND, + EC_NO_ACTIVE_CONNECTION, + EC_USER_NOT_FOUND, + EC_CHANNEL_NOT_FOUND, + EC_CONNECTION_NOT_FOUND, + EC_UNKNOWN_TRANSMISSION_MODE, + EC_AUDIO_NOT_AVAILABLE, + EC_INVALID_SAMPLE, + EC_INVALID_PLUGIN_ID, + EC_INVALID_MUTE_TARGET, + EC_CONNECTION_UNSYNCHRONIZED, + EC_INVALID_API_VERSION, + EC_UNSYNCHRONIZED_BLOB, + EC_UNKNOWN_SETTINGS_KEY, + EC_WRONG_SETTINGS_TYPE, + EC_SETTING_WAS_REMOVED, + EC_DATA_TOO_BIG, + EC_DATA_ID_TOO_LONG, +}; + +/// This enum's values represent error codes specific to the framework of handling positional data +/// gathering (needed for Mumble's positional audio feature). +enum Mumble_PositionalDataErrorCode { + /// Positional data has been initialized properly + PDEC_OK = 0, + /// Positional data is temporarily unavailable (e.g. because the corresponding process isn't running) but might be + /// at another point in time. + PDEC_ERROR_TEMP, + /// Positional data is permanently unavailable (e.g. because the respective memory offsets are outdated). + PDEC_ERROR_PERM +}; + +/// This enum's values represent keys for specific settings inside Mumble. +enum Mumble_SettingsKey { + MSK_INVALID = -1, + MSK_AUDIO_INPUT_VOICE_HOLD = 0, + MSK_AUDIO_INPUT_VAD_SILENCE_THRESHOLD = 1, + MSK_AUDIO_INPUT_VAD_SPEECH_THRESHOLD = 2, + MSK_AUDIO_OUTPUT_PA_MINIMUM_DISTANCE = 3, + MSK_AUDIO_OUTPUT_PA_MAXIMUM_DISTANCE = 4, + MSK_AUDIO_OUTPUT_PA_BLOOM = 5, + MSK_AUDIO_OUTPUT_PA_MINIMUM_VOLUME = 6, +}; + +/// This enum's values represent the key-codes Mumble's API uses to reference keys on the keyboard. +enum Mumble_KeyCode { + KC_INVALID = -1, + + // Non-printable characters first + KC_NULL = 0, + KC_END = 1, + KC_LEFT = 2, + KC_RIGHT = 4, + KC_UP = 5, + KC_DOWN = 6, + KC_DELETE = 7, + KC_BACKSPACE = 8, + KC_TAB = 9, + KC_ENTER = 10, // == '\n' + KC_ESCAPE = 27, + KC_PAGE_UP = 11, + KC_PAGE_DOWN = 12, + KC_SHIFT = 13, + KC_CONTROL = 14, + KC_META = 15, + KC_ALT = 16, + KC_ALT_GR = 17, + KC_CAPSLOCK = 18, + KC_NUMLOCK = 19, + KC_SUPER = 20, // == windows key + KC_HOME = 21, // == Pos1 + KC_PRINT = 22, + KC_SCROLLLOCK = 23, + + // Printable characters are assigned to their ASCII code + KC_SPACE = ' ', + KC_EXCLAMATION_MARK = '!', + KC_DOUBLE_QUOTE = '"', + KC_HASHTAG = '#', + KC_DOLLAR = '$', + KC_PERCENT = '%', + KC_AMPERSAND = '&', + KC_SINGLE_QUOTE = '\'', + KC_OPEN_PARENTHESIS = '(', + KC_CLOSE_PARENTHESIS = ')', + KC_ASTERISK = '*', + KC_PLUS = '+', + KC_COMMA = ',', + KC_MINUS = '-', + KC_PERIOD = '.', + KC_SLASH = '/', + KC_0 = '0', + KC_1 = '1', + KC_2 = '2', + KC_3 = '3', + KC_4 = '4', + KC_5 = '5', + KC_6 = '6', + KC_7 = '7', + KC_8 = '8', + KC_9 = '9', + KC_COLON = ':', + KC_SEMICOLON = ';', + KC_LESS_THAN = '<', + KC_EQUALS = '=', + KC_GREATER_THAN = '>', + KC_QUESTION_MARK = '?', + KC_AT_SYMBOL = '@', + KC_A = 'A', + KC_B = 'B', + KC_C = 'C', + KC_D = 'D', + KC_E = 'E', + KC_F = 'F', + KC_G = 'G', + KC_H = 'H', + KC_I = 'I', + KC_J = 'J', + KC_K = 'K', + KC_L = 'L', + KC_M = 'M', + KC_N = 'N', + KC_O = 'O', + KC_P = 'P', + KC_Q = 'Q', + KC_R = 'R', + KC_S = 'S', + KC_T = 'T', + KC_U = 'U', + KC_V = 'V', + KC_W = 'W', + KC_X = 'X', + KC_Y = 'Y', + KC_Z = 'Z', + // leave out lowercase letters (for now) + KC_OPEN_BRACKET = '[', + KC_BACKSLASH = '\\', + KC_CLOSE_BRACKET = ']', + KC_CIRCUMFLEX = '^', + KC_UNDERSCORE = '_', + KC_GRAVE_AKCENT = '`', + KC_OPEN_BRACE = '{', + KC_VERTICAL_BAR = '|', + KC_CLOSE_BRACE = '}', + KC_TILDE = '~', + + // Some characters from the extended ASCII code + KC_DEGREE_SIGN = 176, + + + + // F-keys + // Start at a value of 256 as extended ASCII codes range up to 255 + KC_F1 = 256, + KC_F2 = 257, + KC_F3 = 258, + KC_F4 = 259, + KC_F5 = 260, + KC_F6 = 261, + KC_F7 = 262, + KC_F8 = 263, + KC_F9 = 264, + KC_F10 = 265, + KC_F11 = 266, + KC_F12 = 267, + KC_F13 = 268, + KC_F14 = 269, + KC_F15 = 270, + KC_F16 = 271, + KC_F17 = 272, + KC_F18 = 273, + KC_F19 = 274, +}; + +/// A struct for representing a version of the form major.minor.patch +struct Version { + int32_t major; + int32_t minor; + int32_t patch; +#ifdef __cplusplus + bool operator<(const Version& other) const { + if (this->major != other.major) { + return this->major < other.major; + } + if (this->minor != other.minor) { + return this->minor < other.minor; + } + // Major and Minor are equal + return this->patch < other.patch; + } + + bool operator>(const Version& other) const { + if (this->major != other.major) { + return this->major > other.major; + } + if (this->minor != other.minor) { + return this->minor > other.minor; + } + // Major and Minor are equal + return this->patch > other.patch; + } + + bool operator>=(const Version& other) const { + if (this->major != other.major) { + return this->major > other.major; + } + if (this->minor != other.minor) { + return this->minor > other.minor; + } + // Major and Minor are equal + return this->patch >= other.patch; + } + + bool operator<=(const Version& other) const { + if (this->major != other.major) { + return this->major < other.major; + } + if (this->minor != other.minor) { + return this->minor < other.minor; + } + // Major and Minor are equal + return this->patch <= other.patch; + } + + bool operator==(const Version& other) const { + return this->major == other.major && this->minor == other.minor && this->patch == other.patch; + } + + bool operator!=(const Version& other) const { + return this->major != other.major || this->minor != other.minor || this->patch != other.patch; + } + + operator std::string() const { + return std::string("v") + std::to_string(this->major) + std::string(".") + std::to_string(this->minor) + std::string(".") + std::to_string(this->patch); + } + +#ifdef QT_VERSION + operator QString() const { + return QString::fromLatin1("v%0.%1.%2").arg(this->major).arg(this->minor).arg(this->patch); + } +#endif +#endif +}; + +/// Obtains a String representation for the given numeric error code. +/// Note that the exact String representation corresponding to an error code may change and is thus +/// not part of the plugin API as such. This function acts merely as a convenience helper for printing +/// errors in a meaningful way. +/// +/// @param errorCode The error code to get the String representation for +/// @returns The error message coresponding to the given error code. The message +/// is encoded as a C-string and is static, meaning that it is safe to use the +/// returned pointer in your code. +inline const char* errorMessage(int16_t errorCode) { + switch (errorCode) { + case EC_GENERIC_ERROR: + return "Generic error"; + case EC_OK: + return "Ok - this is not an error"; + case EC_POINTER_NOT_FOUND: + return "Can't find the passed pointer"; + case EC_NO_ACTIVE_CONNECTION: + return "There is currently no active connection to a server"; + case EC_USER_NOT_FOUND: + return "Can't find the requested user"; + case EC_CHANNEL_NOT_FOUND: + return "Can't find the requested channel"; + case EC_CONNECTION_NOT_FOUND: + return "Can't identify the requested connection"; + case EC_UNKNOWN_TRANSMISSION_MODE: + return "Unknown transmission mode encountered"; + case EC_AUDIO_NOT_AVAILABLE: + return "There is currently no audio output available"; + case EC_INVALID_SAMPLE: + return "Attempted to use invalid sample (can't play it)"; + case EC_INVALID_PLUGIN_ID: + return "Used an invalid plugin ID"; + case EC_INVALID_MUTE_TARGET: + return "Used an invalid mute-target"; + case EC_CONNECTION_UNSYNCHRONIZED: + return "The requested server connection has not yet finished synchronizing"; + case EC_INVALID_API_VERSION: + return "The used API version is invalid or not supported"; + case EC_UNSYNCHRONIZED_BLOB: + return "The requested blob (content) has not yet been synchronized between the client and the server"; + case EC_UNKNOWN_SETTINGS_KEY: + return "The used settings-key does not match any key known to Mumble"; + case EC_WRONG_SETTINGS_TYPE: + return "The referenced setting has a different type than requested"; + case EC_SETTING_WAS_REMOVED: + return "The referenced setting got removed from Mumble and is no longer used"; + case EC_DATA_TOO_BIG: + return "The given data is too large (exceeds limit)"; + case EC_DATA_ID_TOO_LONG: + return "The given data ID is too long (exceeds limit)"; + default: + return "Unknown error code"; + } +} + + +/// This struct is used to return Strings from a plugin to Mumble. It is needed in order to +/// work around the limitation of std::string not being part of C (it holds important information +/// about the String's lifetime management requirements). +struct MumbleStringWrapper { + /// The pointer to the actual String data + const char *data; + /// The size of the pointed String data + size_t size; + /// Whether the wrapped String needs to be released + /// after its usage. Instances for which this would be + /// false: Static Strings, String literals + bool needsReleasing; +}; + +/// Typedef for the type of a talking state +typedef enum Mumble_TalkingState mumble_talking_state_t; +/// Typedef for the type of a transmission mode +typedef enum Mumble_TransmissionMode mumble_transmission_mode_t; +/// Typedef for the type of a version +typedef struct Version mumble_version_t; +/// Typedef for the type of a connection +typedef int32_t mumble_connection_t; +/// Typedef for the type of a user +typedef uint32_t mumble_userid_t; +/// Typedef for the type of a channel +typedef int32_t mumble_channelid_t; +/// Typedef for the type of an error (code) +typedef enum Mumble_ErrorCode mumble_error_t; +/// Typedef for the type of a plugin ID +typedef uint32_t mumble_plugin_id_t; +/// Typedef for the type of a key to a setting in Mumble +typedef enum Mumble_SettingsKey mumble_settings_key_t; +/// Typedef for the type of a key-code +typedef enum Mumble_KeyCode mumble_keycode_t; + +#endif // MUMBLE_PLUGINCOMPONENT_H_ diff --git a/plugins/Process.cpp b/plugins/Process.cpp index 8d222a26672..6f7042a53cb 100644 --- a/plugins/Process.cpp +++ b/plugins/Process.cpp @@ -5,7 +5,7 @@ #include "Process.h" -#include "mumble_plugin_utils.h" +#include "mumble_positional_audio_utils.h" #include diff --git a/plugins/ProcessWindows.cpp b/plugins/ProcessWindows.cpp index 234cb612334..3dc3472a933 100644 --- a/plugins/ProcessWindows.cpp +++ b/plugins/ProcessWindows.cpp @@ -5,7 +5,7 @@ #include "ProcessWindows.h" -#include "mumble_plugin_win32_internals.h" +#include "mumble_positional_audio_win32_internals.h" ProcessWindows::ProcessWindows(const procid_t id, const std::string &name) : Process(id, name) { const auto mods = modules(); diff --git a/plugins/amongus/Game.cpp b/plugins/amongus/Game.cpp index decf24576ac..aa384d33123 100644 --- a/plugins/amongus/Game.cpp +++ b/plugins/amongus/Game.cpp @@ -5,7 +5,7 @@ #include "Game.h" -#include "mumble_plugin_utils.h" +#include "../mumble_positional_audio_utils.h" Game::Game(const procid_t id, const std::string name) : m_ok(false), m_proc(id, name) { if (!m_proc.isOk()) { diff --git a/plugins/amongus/amongus.cpp b/plugins/amongus/amongus.cpp index 40d9caa9c0a..c671e72ea4d 100644 --- a/plugins/amongus/amongus.cpp +++ b/plugins/amongus/amongus.cpp @@ -5,8 +5,10 @@ #include "Game.h" -#include "mumble_plugin.h" -#include "mumble_plugin_utils.h" +#define MUMBLE_ALLOW_DEPRECATED_LEGACY_PLUGIN_API +#include "../mumble_legacy_plugin.h" + +#include "../mumble_positional_audio_utils.h" #include diff --git a/plugins/aoc/aoc.cpp b/plugins/aoc/aoc.cpp index 0d818b1fd8f..9eb0c11c593 100644 --- a/plugins/aoc/aoc.cpp +++ b/plugins/aoc/aoc.cpp @@ -3,7 +3,10 @@ // that can be found in the LICENSE file at the root of the // Mumble source tree or at . -#include "../mumble_plugin_main.h" +#define MUMBLE_ALLOW_DEPRECATED_LEGACY_PLUGIN_API +#include "../mumble_legacy_plugin.h" + +#include "../mumble_positional_audio_main.h" using namespace std; diff --git a/plugins/arma2/arma2.cpp b/plugins/arma2/arma2.cpp index af103973c53..b73ad5721ec 100644 --- a/plugins/arma2/arma2.cpp +++ b/plugins/arma2/arma2.cpp @@ -3,7 +3,10 @@ // that can be found in the LICENSE file at the root of the // Mumble source tree or at . -#include "../mumble_plugin_main.h" +#define MUMBLE_ALLOW_DEPRECATED_LEGACY_PLUGIN_API +#include "../mumble_legacy_plugin.h" + +#include "../mumble_positional_audio_main.h" procptr_t posptr, frontptr, topptr; diff --git a/plugins/bf1/bf1.cpp b/plugins/bf1/bf1.cpp index 17c1cee9dc4..7663160184a 100644 --- a/plugins/bf1/bf1.cpp +++ b/plugins/bf1/bf1.cpp @@ -3,8 +3,11 @@ // that can be found in the LICENSE file at the root of the // Mumble source tree or at . -#include "../mumble_plugin_main.h" // Include standard plugin header. -#include "../mumble_plugin_utils.h" // Include plugin header for special functions, like "escape". +#define MUMBLE_ALLOW_DEPRECATED_LEGACY_PLUGIN_API +#include "../mumble_legacy_plugin.h" + +#include "../mumble_positional_audio_main.h" // Include standard positional audio header +#include "../mumble_positional_audio_utils.h" // Include positional audio header for special functions, like "escape". static int fetch(float *avatar_pos, float *avatar_front, float *avatar_top, float *camera_pos, float *camera_front, float *camera_top, std::string &context, std::wstring &identity) { diff --git a/plugins/bf1942/bf1942.cpp b/plugins/bf1942/bf1942.cpp index a35a2978456..4d2cc00f88f 100644 --- a/plugins/bf1942/bf1942.cpp +++ b/plugins/bf1942/bf1942.cpp @@ -3,7 +3,10 @@ // that can be found in the LICENSE file at the root of the // Mumble source tree or at . -#include "../mumble_plugin_main.h" +#define MUMBLE_ALLOW_DEPRECATED_LEGACY_PLUGIN_API +#include "../mumble_legacy_plugin.h" + +#include "../mumble_positional_audio_main.h" procptr_t faceptr, topptr; // BYTE *stateptr; diff --git a/plugins/bf2/bf2.cpp b/plugins/bf2/bf2.cpp index 4e744487cb6..a08442eaef2 100644 --- a/plugins/bf2/bf2.cpp +++ b/plugins/bf2/bf2.cpp @@ -3,7 +3,10 @@ // that can be found in the LICENSE file at the root of the // Mumble source tree or at . -#include "../mumble_plugin_main.h" +#define MUMBLE_ALLOW_DEPRECATED_LEGACY_PLUGIN_API +#include "../mumble_legacy_plugin.h" + +#include "../mumble_positional_audio_main.h" using namespace std; diff --git a/plugins/bf2142/bf2142.cpp b/plugins/bf2142/bf2142.cpp index aa1b2e4efeb..7f7bb91d30d 100644 --- a/plugins/bf2142/bf2142.cpp +++ b/plugins/bf2142/bf2142.cpp @@ -3,8 +3,11 @@ // that can be found in the LICENSE file at the root of the // Mumble source tree or at . -#include "../mumble_plugin_main.h" // Include standard plugin header. -#include "../mumble_plugin_utils.h" // Include plugin header for special functions, like "escape". +#define MUMBLE_ALLOW_DEPRECATED_LEGACY_PLUGIN_API +#include "../mumble_legacy_plugin.h" + +#include "../mumble_positional_audio_main.h" // Include standard positional audio header. +#include "../mumble_positional_audio_utils.h" // Include positional audio header for special functions, like "escape". // Variable to contain module's addresses procptr_t RendDX9 = 0; diff --git a/plugins/bf3/bf3.cpp b/plugins/bf3/bf3.cpp index 812e4913451..e1919876877 100644 --- a/plugins/bf3/bf3.cpp +++ b/plugins/bf3/bf3.cpp @@ -36,7 +36,10 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -#include "../mumble_plugin_main.h" +#define MUMBLE_ALLOW_DEPRECATED_LEGACY_PLUGIN_API +#include "../mumble_legacy_plugin.h" + +#include "../mumble_positional_audio_main.h" static bool ptr_chain_valid = false; diff --git a/plugins/bf4/bf4.cpp b/plugins/bf4/bf4.cpp index f06fb379e56..287dd8dc6f7 100644 --- a/plugins/bf4/bf4.cpp +++ b/plugins/bf4/bf4.cpp @@ -3,8 +3,11 @@ // that can be found in the LICENSE file at the root of the // Mumble source tree or at . -#include "../mumble_plugin_main.h" // Include standard plugin header. -#include "../mumble_plugin_utils.h" // Include plugin header for special functions, like "escape". +#define MUMBLE_ALLOW_DEPRECATED_LEGACY_PLUGIN_API +#include "../mumble_legacy_plugin.h" + +#include "../mumble_positional_audio_main.h" // Include standard positional audio header. +#include "../mumble_positional_audio_utils.h" // Include positional audio header for special functions, like "escape". static int fetch(float *avatar_pos, float *avatar_front, float *avatar_top, float *camera_pos, float *camera_front, float *camera_top, std::string &context, std::wstring &identity) { diff --git a/plugins/bf4_x86/bf4_x86.cpp b/plugins/bf4_x86/bf4_x86.cpp index c4d49620fbf..af7400e0048 100644 --- a/plugins/bf4_x86/bf4_x86.cpp +++ b/plugins/bf4_x86/bf4_x86.cpp @@ -3,8 +3,11 @@ // that can be found in the LICENSE file at the root of the // Mumble source tree or at . -#include "../mumble_plugin_main.h" // Include standard plugin header. -#include "../mumble_plugin_utils.h" // Include plugin header for special functions, like "escape". +#define MUMBLE_ALLOW_DEPRECATED_LEGACY_PLUGIN_API +#include "../mumble_legacy_plugin.h" + +#include "../mumble_positional_audio_main.h" // Include standard positional audio header. +#include "../mumble_positional_audio_utils.h" // Include positional audio header for special functions, like "escape". static int fetch(float *avatar_pos, float *avatar_front, float *avatar_top, float *camera_pos, float *camera_front, float *camera_top, std::string &context, std::wstring &identity) { diff --git a/plugins/bfbc2/bfbc2.cpp b/plugins/bfbc2/bfbc2.cpp index 312498e3766..cd31f737afa 100644 --- a/plugins/bfbc2/bfbc2.cpp +++ b/plugins/bfbc2/bfbc2.cpp @@ -3,7 +3,10 @@ // that can be found in the LICENSE file at the root of the // Mumble source tree or at . -#include "../mumble_plugin_main.h" +#define MUMBLE_ALLOW_DEPRECATED_LEGACY_PLUGIN_API +#include "../mumble_legacy_plugin.h" + +#include "../mumble_positional_audio_main.h" bool is_steam = false; diff --git a/plugins/bfheroes/bfheroes.cpp b/plugins/bfheroes/bfheroes.cpp index fc7136737de..f92ded261dd 100644 --- a/plugins/bfheroes/bfheroes.cpp +++ b/plugins/bfheroes/bfheroes.cpp @@ -3,7 +3,10 @@ // that can be found in the LICENSE file at the root of the // Mumble source tree or at . -#include "../mumble_plugin_main.h" +#define MUMBLE_ALLOW_DEPRECATED_LEGACY_PLUGIN_API +#include "../mumble_legacy_plugin.h" + +#include "../mumble_positional_audio_main.h" procptr_t posptr, faceptr, topptr, stateptr; diff --git a/plugins/blacklight/blacklight.cpp b/plugins/blacklight/blacklight.cpp index ffc016f9842..e94677761e8 100644 --- a/plugins/blacklight/blacklight.cpp +++ b/plugins/blacklight/blacklight.cpp @@ -34,7 +34,10 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -#include "../mumble_plugin_main.h" +#define MUMBLE_ALLOW_DEPRECATED_LEGACY_PLUGIN_API +#include "../mumble_legacy_plugin.h" + +#include "../mumble_positional_audio_main.h" /* Arrays of bytes to find addresses accessed by respective functions so we don't have to blindly search for addresses diff --git a/plugins/borderlands/borderlands.cpp b/plugins/borderlands/borderlands.cpp index 46aabf210a7..bca3f2ac6a5 100644 --- a/plugins/borderlands/borderlands.cpp +++ b/plugins/borderlands/borderlands.cpp @@ -34,7 +34,10 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -#include "../mumble_plugin_main.h" +#define MUMBLE_ALLOW_DEPRECATED_LEGACY_PLUGIN_API +#include "../mumble_legacy_plugin.h" + +#include "../mumble_positional_audio_main.h" procptr_t posptr, frontptr, topptr, contextptraddress, stateaddress, loginaddress; diff --git a/plugins/borderlands2/borderlands2.cpp b/plugins/borderlands2/borderlands2.cpp index 2ef7b0a1ff9..4e58f0dfcd3 100644 --- a/plugins/borderlands2/borderlands2.cpp +++ b/plugins/borderlands2/borderlands2.cpp @@ -35,7 +35,10 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -#include "../mumble_plugin_main.h" +#define MUMBLE_ALLOW_DEPRECATED_LEGACY_PLUGIN_API +#include "../mumble_legacy_plugin.h" + +#include "../mumble_positional_audio_main.h" #include diff --git a/plugins/breach/breach.cpp b/plugins/breach/breach.cpp index 605b16a94e0..16185714b81 100644 --- a/plugins/breach/breach.cpp +++ b/plugins/breach/breach.cpp @@ -34,7 +34,10 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -#include "../mumble_plugin_main.h" +#define MUMBLE_ALLOW_DEPRECATED_LEGACY_PLUGIN_API +#include "../mumble_legacy_plugin.h" + +#include "../mumble_positional_audio_main.h" procptr_t posptr, frontptr, topptr; diff --git a/plugins/cod2/cod2.cpp b/plugins/cod2/cod2.cpp index 1baef190b8c..05739facb5f 100644 --- a/plugins/cod2/cod2.cpp +++ b/plugins/cod2/cod2.cpp @@ -5,8 +5,10 @@ #include "ProcessWindows.h" -#include "mumble_plugin.h" -#include "mumble_plugin_utils.h" +#define MUMBLE_ALLOW_DEPRECATED_LEGACY_PLUGIN_API +#include "../mumble_legacy_plugin.h" + +#include "../mumble_positional_audio_utils.h" std::unique_ptr< ProcessWindows > process; diff --git a/plugins/cod4/cod4.cpp b/plugins/cod4/cod4.cpp index 05c50401436..9a6155bdd98 100644 --- a/plugins/cod4/cod4.cpp +++ b/plugins/cod4/cod4.cpp @@ -3,7 +3,10 @@ // that can be found in the LICENSE file at the root of the // Mumble source tree or at . -#include "../mumble_plugin_main.h" +#define MUMBLE_ALLOW_DEPRECATED_LEGACY_PLUGIN_API +#include "../mumble_legacy_plugin.h" + +#include "../mumble_positional_audio_main.h" using namespace std; diff --git a/plugins/cod5/cod5.cpp b/plugins/cod5/cod5.cpp index cf5982383f5..e27cfa8920a 100644 --- a/plugins/cod5/cod5.cpp +++ b/plugins/cod5/cod5.cpp @@ -3,7 +3,10 @@ // that can be found in the LICENSE file at the root of the // Mumble source tree or at . -#include "../mumble_plugin_main.h" +#define MUMBLE_ALLOW_DEPRECATED_LEGACY_PLUGIN_API +#include "../mumble_legacy_plugin.h" + +#include "../mumble_positional_audio_main.h" static int fetch(float *avatar_pos, float *avatar_front, float *avatar_top, float *camera_pos, float *camera_front, float *camera_top, std::string &, std::wstring &) { diff --git a/plugins/codmw2/codmw2.cpp b/plugins/codmw2/codmw2.cpp index 44b4d9527d0..9daf9a84fbc 100644 --- a/plugins/codmw2/codmw2.cpp +++ b/plugins/codmw2/codmw2.cpp @@ -3,7 +3,10 @@ // that can be found in the LICENSE file at the root of the // Mumble source tree or at . -#include "../mumble_plugin_main.h" +#define MUMBLE_ALLOW_DEPRECATED_LEGACY_PLUGIN_API +#include "../mumble_legacy_plugin.h" + +#include "../mumble_positional_audio_main.h" static int fetch(float *avatar_pos, float *avatar_front, float *avatar_top, float *camera_pos, float *camera_front, float *camera_top, std::string &, std::wstring &) { diff --git a/plugins/codmw2so/codmw2so.cpp b/plugins/codmw2so/codmw2so.cpp index cac4484a7b8..25122bb62fe 100644 --- a/plugins/codmw2so/codmw2so.cpp +++ b/plugins/codmw2so/codmw2so.cpp @@ -35,7 +35,10 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -#include "../mumble_plugin_main.h" +#define MUMBLE_ALLOW_DEPRECATED_LEGACY_PLUGIN_API +#include "../mumble_legacy_plugin.h" + +#include "../mumble_positional_audio_main.h" static int fetch(float *avatar_pos, float *avatar_front, float *avatar_top, float *camera_pos, float *camera_front, float *camera_top, std::string &, std::wstring &) { diff --git a/plugins/cs/cs.cpp b/plugins/cs/cs.cpp index 2d8e8730e76..2097dd8fe7a 100644 --- a/plugins/cs/cs.cpp +++ b/plugins/cs/cs.cpp @@ -35,7 +35,10 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -#include "../mumble_plugin_main.h" +#define MUMBLE_ALLOW_DEPRECATED_LEGACY_PLUGIN_API +#include "../mumble_legacy_plugin.h" + +#include "../mumble_positional_audio_main.h" using namespace std; diff --git a/plugins/dys/dys.cpp b/plugins/dys/dys.cpp index 946731c3dcb..2fa0a9034f2 100644 --- a/plugins/dys/dys.cpp +++ b/plugins/dys/dys.cpp @@ -3,7 +3,10 @@ // that can be found in the LICENSE file at the root of the // Mumble source tree or at . -#include "../mumble_plugin_main.h" +#define MUMBLE_ALLOW_DEPRECATED_LEGACY_PLUGIN_API +#include "../mumble_legacy_plugin.h" + +#include "../mumble_positional_audio_main.h" using namespace std; diff --git a/plugins/etqw/etqw.cpp b/plugins/etqw/etqw.cpp index 124848d2a5f..e5f1a2b5776 100644 --- a/plugins/etqw/etqw.cpp +++ b/plugins/etqw/etqw.cpp @@ -3,7 +3,10 @@ // that can be found in the LICENSE file at the root of the // Mumble source tree or at . -#include "../mumble_plugin_main.h" +#define MUMBLE_ALLOW_DEPRECATED_LEGACY_PLUGIN_API +#include "../mumble_legacy_plugin.h" + +#include "../mumble_positional_audio_main.h" using namespace std; diff --git a/plugins/ffxiv/ffxiv.cpp b/plugins/ffxiv/ffxiv.cpp index c1643a24c64..f7742bcdc79 100644 --- a/plugins/ffxiv/ffxiv.cpp +++ b/plugins/ffxiv/ffxiv.cpp @@ -3,8 +3,11 @@ // that can be found in the LICENSE file at the root of the // Mumble source tree or at . -#include "../mumble_plugin_main.h" // Include standard plugin header. -#include "../mumble_plugin_utils.h" // Include plugin header for special functions, like "escape". +#define MUMBLE_ALLOW_DEPRECATED_LEGACY_PLUGIN_API +#include "../mumble_legacy_plugin.h" + +#include "../mumble_positional_audio_main.h" // Include standard positional audio header. +#include "../mumble_positional_audio_utils.h" // Include positional audio header for special functions, like "escape". #include diff --git a/plugins/gmod/gmod.cpp b/plugins/gmod/gmod.cpp index fd2bb21533d..d13b9ab881c 100644 --- a/plugins/gmod/gmod.cpp +++ b/plugins/gmod/gmod.cpp @@ -3,7 +3,10 @@ // that can be found in the LICENSE file at the root of the // Mumble source tree or at . -#include "../mumble_plugin_main.h" +#define MUMBLE_ALLOW_DEPRECATED_LEGACY_PLUGIN_API +#include "../mumble_legacy_plugin.h" + +#include "../mumble_positional_audio_main.h" using namespace std; diff --git a/plugins/gtaiv/gtaiv.cpp b/plugins/gtaiv/gtaiv.cpp index b57cb7d84fe..18db5d8171d 100644 --- a/plugins/gtaiv/gtaiv.cpp +++ b/plugins/gtaiv/gtaiv.cpp @@ -34,7 +34,10 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -#include "../mumble_plugin_main.h" +#define MUMBLE_ALLOW_DEPRECATED_LEGACY_PLUGIN_API +#include "../mumble_legacy_plugin.h" + +#include "../mumble_positional_audio_main.h" static unsigned int playerid; static procptr_t base_address; diff --git a/plugins/gtasa/gtasa.cpp b/plugins/gtasa/gtasa.cpp index cb166b5d4e6..293b2d115d0 100644 --- a/plugins/gtasa/gtasa.cpp +++ b/plugins/gtasa/gtasa.cpp @@ -3,7 +3,10 @@ // that can be found in the LICENSE file at the root of the // Mumble source tree or at . -#include "../mumble_plugin_main.h" +#define MUMBLE_ALLOW_DEPRECATED_LEGACY_PLUGIN_API +#include "../mumble_legacy_plugin.h" + +#include "../mumble_positional_audio_main.h" struct Matrix { float right[4]; diff --git a/plugins/gtav/gtav.cpp b/plugins/gtav/gtav.cpp index a10e75888f6..d00237c7619 100644 --- a/plugins/gtav/gtav.cpp +++ b/plugins/gtav/gtav.cpp @@ -3,8 +3,11 @@ // that can be found in the LICENSE file at the root of the // Mumble source tree or at . -#include "../mumble_plugin_main.h" // Include standard plugin header. -#include "../mumble_plugin_utils.h" // Include plugin header for special functions, like "escape". +#define MUMBLE_ALLOW_DEPRECATED_LEGACY_PLUGIN_API +#include "../mumble_legacy_plugin.h" + +#include "../mumble_positional_audio_main.h" // Include standard positional audio header. +#include "../mumble_positional_audio_utils.h" // Include positional audio header for special functions, like "escape". #include // Include algorithm header for the game version detector diff --git a/plugins/gw/gw.cpp b/plugins/gw/gw.cpp index 567a5d14f7e..b8fbeab57c8 100644 --- a/plugins/gw/gw.cpp +++ b/plugins/gw/gw.cpp @@ -34,7 +34,10 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -#include "../mumble_plugin_main.h" +#define MUMBLE_ALLOW_DEPRECATED_LEGACY_PLUGIN_API +#include "../mumble_legacy_plugin.h" + +#include "../mumble_positional_audio_main.h" /* Arrays of bytes to find addresses accessed by respective functions so we don't have to blindly search for addresses diff --git a/plugins/insurgency/insurgency.cpp b/plugins/insurgency/insurgency.cpp index 89f4ce81199..f7ce9d40534 100644 --- a/plugins/insurgency/insurgency.cpp +++ b/plugins/insurgency/insurgency.cpp @@ -3,7 +3,10 @@ // that can be found in the LICENSE file at the root of the // Mumble source tree or at . -#include "../mumble_plugin_main.h" +#define MUMBLE_ALLOW_DEPRECATED_LEGACY_PLUGIN_API +#include "../mumble_legacy_plugin.h" + +#include "../mumble_positional_audio_main.h" using namespace std; diff --git a/plugins/jc2/jc2.cpp b/plugins/jc2/jc2.cpp index 5d8c66b5806..521da373e23 100644 --- a/plugins/jc2/jc2.cpp +++ b/plugins/jc2/jc2.cpp @@ -35,7 +35,10 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -#include "../mumble_plugin_main.h" +#define MUMBLE_ALLOW_DEPRECATED_LEGACY_PLUGIN_API +#include "../mumble_legacy_plugin.h" + +#include "../mumble_positional_audio_main.h" const unsigned int off_character_manager = 0xD8FB24; const unsigned int off_local_player = 0x3570; diff --git a/plugins/link/link-posix.cpp b/plugins/link/link-posix.cpp index 43306e21be1..c29e6de4b99 100644 --- a/plugins/link/link-posix.cpp +++ b/plugins/link/link-posix.cpp @@ -3,7 +3,8 @@ // that can be found in the LICENSE file at the root of the // Mumble source tree or at . -#include "../mumble_plugin.h" +#define MUMBLE_ALLOW_DEPRECATED_LEGACY_PLUGIN_API +#include "../mumble_legacy_plugin.h" #include #include diff --git a/plugins/link/link.cpp b/plugins/link/link.cpp index a899affd508..f98bed2e4e3 100644 --- a/plugins/link/link.cpp +++ b/plugins/link/link.cpp @@ -10,7 +10,8 @@ #include #include -#include "../mumble_plugin.h" +#define MUMBLE_ALLOW_DEPRECATED_LEGACY_PLUGIN_API +#include "../mumble_legacy_plugin.h" static std::wstring wsPluginName; static std::wstring wsDescription; diff --git a/plugins/lol/lol.cpp b/plugins/lol/lol.cpp index 9746cc4a80b..4f70ced4f3e 100644 --- a/plugins/lol/lol.cpp +++ b/plugins/lol/lol.cpp @@ -34,7 +34,10 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -#include "../mumble_plugin_main.h" +#define MUMBLE_ALLOW_DEPRECATED_LEGACY_PLUGIN_API +#include "../mumble_legacy_plugin.h" + +#include "../mumble_positional_audio_main.h" /* Arrays of bytes to find addresses accessed by respective functions so we don't have to blindly search for addresses diff --git a/plugins/lotro/lotro.cpp b/plugins/lotro/lotro.cpp index ec5c79cb10f..f3562236fd0 100644 --- a/plugins/lotro/lotro.cpp +++ b/plugins/lotro/lotro.cpp @@ -34,7 +34,10 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -#include "../mumble_plugin_main.h" +#define MUMBLE_ALLOW_DEPRECATED_LEGACY_PLUGIN_API +#include "../mumble_legacy_plugin.h" + +#include "../mumble_positional_audio_main.h" static int fetch(float *avatar_pos, float *avatar_front, float *avatar_top, float *camera_pos, float *camera_front, float *camera_top, std::string &context, std::wstring &) { diff --git a/plugins/mumble_plugin.h b/plugins/mumble_legacy_plugin.h similarity index 87% rename from plugins/mumble_plugin.h rename to plugins/mumble_legacy_plugin.h index bed4383aa68..14c35cf4095 100644 --- a/plugins/mumble_plugin.h +++ b/plugins/mumble_legacy_plugin.h @@ -1,10 +1,23 @@ -// Copyright 2005-2021 The Mumble Developers. All rights reserved. +// Copyright 2021 The Mumble Developers. All rights reserved. // Use of this source code is governed by a BSD-style license // that can be found in the LICENSE file at the root of the // Mumble source tree or at . -#ifndef MUMBLE_MUMBLE_PLUGIN_H_ -#define MUMBLE_MUMBLE_PLUGIN_H_ +// This header describes the deprecated "plugin" API from the times in which "plugins" could only be used +// alongside the positional audio feature of Mumble. +// By default translation units including this file will not compile due to a preprocessor error. This is intended +// behaviour as you shouldn't be using this API any longer. Use the API from MumblePlugin.h instead. +// +// If for some reason you absolutely have to include this header file, you have to define the macro MUMBLE_ALLOW_DEPRECATED_LEGACY_PLUGIN_API +// before including this header. +#ifndef MUMBLE_LEGACY_PLUGIN_H_ +#define MUMBLE_LEGACY_PLUGIN_H_ + +#ifndef MUMBLE_ALLOW_DEPRECATED_LEGACY_PLUGIN_API + #error "You are trying to use a deprecated plugin API. Use the new API from MumblePlugin.h instead. If you think you really need this deprecated one, see the instructions at the top of this file." +#else + #undef MUMBLE_ALLOW_DEPRECATED_LEGACY_PLUGIN_API +#endif #include diff --git a/plugins/mumble_plugin_linux.h b/plugins/mumble_positional_audio_linux.h similarity index 95% rename from plugins/mumble_plugin_linux.h rename to plugins/mumble_positional_audio_linux.h index 15a26f26e72..6758e63b847 100644 --- a/plugins/mumble_plugin_linux.h +++ b/plugins/mumble_positional_audio_linux.h @@ -1,16 +1,16 @@ -// Copyright 2016-2021 The Mumble Developers. All rights reserved. +// Copyright 2021 The Mumble Developers. All rights reserved. // Use of this source code is governed by a BSD-style license // that can be found in the LICENSE file at the root of the // Mumble source tree or at . -#ifndef MUMBLE_PLUGIN_LINUX_H_ -#define MUMBLE_PLUGIN_LINUX_H_ +#ifndef MUMBLE_POSITIONAL_AUDIO_LINUX_H_ +#define MUMBLE_POSITIONAL_AUDIO_LINUX_H_ -#ifndef MUMBLE_PLUGIN_MAIN_H_ -# error "Include mumble_plugin_main.h instead of mumble_plugin_linux.h" +#ifndef MUMBLE_POSITIONAL_AUDIO_MAIN_H_ +# error "Include mumble_positional_audio_main.h instead of mumble_positional_audio_linux.h" #endif -#include "mumble_plugin_utils.h" +#include "mumble_positional_audio_utils.h" #include #include diff --git a/plugins/mumble_plugin_main.h b/plugins/mumble_positional_audio_main.h similarity index 95% rename from plugins/mumble_plugin_main.h rename to plugins/mumble_positional_audio_main.h index 1024c431b16..c8966535f80 100644 --- a/plugins/mumble_plugin_main.h +++ b/plugins/mumble_positional_audio_main.h @@ -1,4 +1,4 @@ -// Copyright 2019-2021 The Mumble Developers. All rights reserved. +// Copyright 2021 The Mumble Developers. All rights reserved. // Use of this source code is governed by a BSD-style license // that can be found in the LICENSE file at the root of the // Mumble source tree or at . @@ -11,15 +11,14 @@ // base address of a module (e.g. shared library), the latter reads memory at the // specified address and stores it in a variable. -#ifndef MUMBLE_PLUGIN_MAIN_H_ -#define MUMBLE_PLUGIN_MAIN_H_ +#ifndef MUMBLE_POSITIONAL_AUDIO_MAIN_H_ +#define MUMBLE_POSITIONAL_AUDIO_MAIN_H_ #if !defined(OS_WINDOWS) && !defined(OS_LINUX) # error "Please define either OS_WINDOWS or OS_LINUX" #endif -#include "mumble_plugin.h" -#include "mumble_plugin_win32_internals.h" +#include "mumble_positional_audio_win32_internals.h" #include #include @@ -228,10 +227,10 @@ static inline int8_t isWin32Process64Bit(const procptr_t &baseAddress) { } } -#ifdef OS_WINDOWS -# include "../mumble_plugin_win32.h" +#ifdef WIN32 +# include "../mumble_positional_audio_win32.h" #else -# include "../mumble_plugin_linux.h" +# include "../mumble_positional_audio_linux.h" #endif #endif diff --git a/plugins/mumble_plugin_utils.h b/plugins/mumble_positional_audio_utils.h similarity index 97% rename from plugins/mumble_plugin_utils.h rename to plugins/mumble_positional_audio_utils.h index 355239d2c67..ca6b5b4f082 100644 --- a/plugins/mumble_plugin_utils.h +++ b/plugins/mumble_positional_audio_utils.h @@ -1,10 +1,10 @@ -// Copyright 2016-2021 The Mumble Developers. All rights reserved. +// Copyright 2021 The Mumble Developers. All rights reserved. // Use of this source code is governed by a BSD-style license // that can be found in the LICENSE file at the root of the // Mumble source tree or at . -#ifndef MUMBLE_MUMBLE_PLUGIN_UTILS_H_ -#define MUMBLE_MUMBLE_PLUGIN_UTILS_H_ +#ifndef MUMBLE_POSITIONAL_AUDIO_UTILS_H_ +#define MUMBLE_POSITIONAL_AUDIO_UTILS_H_ #include #include diff --git a/plugins/mumble_plugin_win32.h b/plugins/mumble_positional_audio_win32.h similarity index 91% rename from plugins/mumble_plugin_win32.h rename to plugins/mumble_positional_audio_win32.h index b2d530579f7..246002d01d9 100644 --- a/plugins/mumble_plugin_win32.h +++ b/plugins/mumble_positional_audio_win32.h @@ -1,13 +1,13 @@ -// Copyright 2010-2021 The Mumble Developers. All rights reserved. +// Copyright 2021 The Mumble Developers. All rights reserved. // Use of this source code is governed by a BSD-style license // that can be found in the LICENSE file at the root of the // Mumble source tree or at . -#ifndef MUMBLE_MUMBLE_PLUGIN_WIN32_H_ -#define MUMBLE_MUMBLE_PLUGIN_WIN32_H_ +#ifndef MUMBLE_POSITIONAL_AUDIO_WIN32_H_ +#define MUMBLE_POSITIONAL_AUDIO_WIN32_H_ -#ifndef MUMBLE_PLUGIN_MAIN_H_ -# error "Include mumble_plugin_main.h instead of mumble_plugin_win32.h" +#ifndef MUMBLE_POSITIONAL_AUDIO_MAIN_H_ +# error "Include mumble_positional_audio_main.h instead of mumble_positional_audio_win32.h" #endif #include diff --git a/plugins/mumble_plugin_win32_internals.h b/plugins/mumble_positional_audio_win32_internals.h similarity index 95% rename from plugins/mumble_plugin_win32_internals.h rename to plugins/mumble_positional_audio_win32_internals.h index 81662589e3d..671a39dac97 100644 --- a/plugins/mumble_plugin_win32_internals.h +++ b/plugins/mumble_positional_audio_win32_internals.h @@ -1,10 +1,10 @@ -// Copyright 2019-2021 The Mumble Developers. All rights reserved. +// Copyright 2021 The Mumble Developers. All rights reserved. // Use of this source code is governed by a BSD-style license // that can be found in the LICENSE file at the root of the // Mumble source tree or at . -#ifndef MUMBLE_MUMBLE_PLUGIN_WIN32_INTERNALS_H_ -#define MUMBLE_MUMBLE_PLUGIN_WIN32_INTERNALS_H_ +#ifndef MUMBLE_MUMBLE_POSITIONAL_AUDIO_WIN32_INTERNALS_H_ +#define MUMBLE_MUMBLE_POSITIONAL_AUDIO_WIN32_INTERNALS_H_ // These structures represent the header(s) of an NT image. // diff --git a/plugins/null_plugin.cpp b/plugins/null_plugin.cpp index a41f3044466..ed9d38cce30 100644 --- a/plugins/null_plugin.cpp +++ b/plugins/null_plugin.cpp @@ -3,7 +3,8 @@ // that can be found in the LICENSE file at the root of the // Mumble source tree or at . -#include "mumble_plugin.h" +#define MUMBLE_ALLOW_DEPRECATED_LEGACY_PLUGIN_API +#include "mumble_legacy_plugin.h" #ifndef NULL_DESC # define NULL_DESC L"Retracted plugin" diff --git a/plugins/ql/ql.cpp b/plugins/ql/ql.cpp index 0905c2bd040..69c0441a36a 100644 --- a/plugins/ql/ql.cpp +++ b/plugins/ql/ql.cpp @@ -3,8 +3,11 @@ // that can be found in the LICENSE file at the root of the // Mumble source tree or at . -#include "../mumble_plugin_main.h" // Include standard plugin header. -#include "../mumble_plugin_utils.h" // Include plugin header for special functions, like "escape". +#define MUMBLE_ALLOW_DEPRECATED_LEGACY_PLUGIN_API +#include "../mumble_legacy_plugin.h" + +#include "../mumble_positional_audio_main.h" // Include standard positional audio header. +#include "../mumble_positional_audio_utils.h" // Include positional audio header for special functions, like "escape". static int fetch(float *avatar_pos, float *avatar_front, float *avatar_top, float *camera_pos, float *camera_front, float *camera_top, std::string &context, std::wstring &identity) { diff --git a/plugins/rl/rl.cpp b/plugins/rl/rl.cpp index eabe7707ce1..5d8ee9f0af8 100644 --- a/plugins/rl/rl.cpp +++ b/plugins/rl/rl.cpp @@ -3,7 +3,10 @@ // that can be found in the LICENSE file at the root of the // Mumble source tree or at . -#include "../mumble_plugin_main.h" +#define MUMBLE_ALLOW_DEPRECATED_LEGACY_PLUGIN_API +#include "../mumble_legacy_plugin.h" + +#include "../mumble_positional_audio_main.h" #ifdef WIN32 // Memory offsets diff --git a/plugins/se/se.cpp b/plugins/se/se.cpp index f048ea3add7..15bcd759fc7 100644 --- a/plugins/se/se.cpp +++ b/plugins/se/se.cpp @@ -3,8 +3,10 @@ // that can be found in the LICENSE file at the root of the // Mumble source tree or at . -#include "mumble_plugin.h" -#include "mumble_plugin_utils.h" +#define MUMBLE_ALLOW_DEPRECATED_LEGACY_PLUGIN_API +#include "../mumble_legacy_plugin.h" + +#include "../mumble_positional_audio_utils.h" #ifdef OS_LINUX # include "ProcessLinux.h" diff --git a/plugins/sr/sr.cpp b/plugins/sr/sr.cpp index 0482179d6aa..9ec80b8f7a4 100644 --- a/plugins/sr/sr.cpp +++ b/plugins/sr/sr.cpp @@ -35,7 +35,10 @@ */ -#include "../mumble_plugin_main.h" +#define MUMBLE_ALLOW_DEPRECATED_LEGACY_PLUGIN_API +#include "../mumble_legacy_plugin.h" + +#include "../mumble_positional_audio_main.h" static int fetch(float *avatar_pos, float *avatar_front, float *avatar_top, float *camera_pos, float *camera_front, float *camera_top, std::string & /*context*/, std::wstring & /*identity*/) { diff --git a/plugins/testPlugin/CMakeLists.txt b/plugins/testPlugin/CMakeLists.txt new file mode 100644 index 00000000000..6dea29e32b2 --- /dev/null +++ b/plugins/testPlugin/CMakeLists.txt @@ -0,0 +1,9 @@ +# Copyright 2021 The Mumble Developers. All rights reserved. +# Use of this source code is governed by a BSD-style license +# that can be found in the LICENSE file at the root of the +# Mumble source tree or at . + +if(${CMAKE_BUILD_TYPE} MATCHES Debug) + message("Including TestPlugin in debug mode") + add_library(testPlugin SHARED "testPlugin.cpp") +endif() diff --git a/plugins/testPlugin/testPlugin.cpp b/plugins/testPlugin/testPlugin.cpp new file mode 100644 index 00000000000..e18822fed17 --- /dev/null +++ b/plugins/testPlugin/testPlugin.cpp @@ -0,0 +1,480 @@ +// Copyright 2021 The Mumble Developers. All rights reserved. +// Use of this source code is governed by a BSD-style license +// that can be found in the LICENSE file at the root of the +// Mumble source tree or at . + +// Include the definitions of the plugin functions +// Not that this will also include ../PluginComponents.h +#include "../MumblePlugin_v_1_0_x.h" +#include "../MumbleAPI_v_1_0_x.h" + +#include +#include + +// These are just some utility functions facilitating writing logs and the like +// The actual implementation of the plugin is further down +std::ostream& pLog() { + std::cout << "TestPlugin: "; + return std::cout; +} + +template +void pluginLog(T log) { + pLog() << log << std::endl; +} + +std::ostream& operator<<(std::ostream& stream, const mumble_version_t version) { + stream << "v" << version.major << "." << version.minor << "." << version.patch; + return stream; +} + + +////////////////////////////////////////////////////////////// +////////////////////////////////////////////////////////////// +//////////////////// PLUGIN IMPLEMENTATION /////////////////// +////////////////////////////////////////////////////////////// +////////////////////////////////////////////////////////////// + +MumbleAPI_v_1_0_x mumAPI; +mumble_connection_t activeConnection; +mumble_plugin_id_t ownID; + +////////////////////////////////////////////////////////////// +//////////////////// OBLIGATORY FUNCTIONS //////////////////// +////////////////////////////////////////////////////////////// +// All of the following function must be implemented in order for Mumble to load the plugin + +mumble_error_t mumble_init(uint32_t id) { + pluginLog("Initialized plugin"); + + ownID = id; + + // Print the connection ID at initialization. If not connected to a server it should be -1. + pLog() << "Plugin ID is " << id << std::endl; + + mumAPI.log(ownID, "Intitialized"); + + // Little showcase for how to retrieve a setting from Mumble + int64_t voiceHold; + mumble_error_t error = mumAPI.getMumbleSetting_int(ownID, MSK_AUDIO_INPUT_VOICE_HOLD, &voiceHold); + if (error == STATUS_OK) { + pLog() << "The voice hold is set to " << voiceHold << std::endl; + } else { + pluginLog("Failed to retrieve voice hold"); + pLog() << errorMessage(error) << std::endl; + } + + // STATUS_OK is a macro set to the appropriate status flag (ErrorCode) + // If you need to return any other status have a look at the ErrorCode enum + // inside PluginComponents.h and use one of its values + return STATUS_OK; +} + +void mumble_shutdown() { + pluginLog("Shutdown plugin"); + + mumAPI.log(ownID, "Shutdown"); +} + +MumbleStringWrapper mumble_getName() { + static const char *name = "TestPlugin"; + + MumbleStringWrapper wrapper; + wrapper.data = name; + wrapper.size = strlen(name); + // It's a static String and therefore doesn't need releasing + wrapper.needsReleasing = false; + + return wrapper; +} + +mumble_version_t mumble_getAPIVersion() { + // MUMBLE_PLUGIN_API_VERSION will always contain the API version of the used header file (the one used to build + // this plugin against). Thus you should always return that here in order to no have to worry about it. + return MUMBLE_PLUGIN_API_VERSION; +} + +void mumble_registerAPIFunctions(void *api) { + // In this function the plugin is presented with a struct of function pointers that can be used + // to interact with Mumble. Thus you should store it somewhere safe for later usage. + + // The pointer has to be cast to the respective API struct. You always have to cast to the same API version + // as this plugin itself is using. Thus if this plugin is compiled using the API version 1.0.x (where x is an arbitrary version) + // the pointer has to be cast to MumbleAPI_v_1_0_x (where x is a literal "x"). + // Furthermore the struct HAS TO BE COPIED!!! Storing the pointer is not an option as it will become invalid quickly! + + // **If** you are using the same API version that is specified in the included header file (as you should), you + // can simply use the MUMBLE_API_CAST to cast the pointer to the correct type and automatically dereferencing it. + mumAPI = MUMBLE_API_CAST(api); + + pluginLog("Registered Mumble's API functions"); +} + +void mumble_releaseResource(const void *pointer) { + std::cerr << "[ERROR]: Unexpected call to mumble_releaseResources" << std::endl; + std::terminate(); + // This plugin doesn't use resources that are explicitly allocated (only static Strings are used). Therefore + // we don't have to implement this function. + // + // If you allocated resources using malloc(), you're implementation for releasing that would be + // free(const_cast(pointer)); + // + // If however you allocated a resource using the new operator (C++ only), you have figure out the pointer's + // original type and then invoke + // delete static_cast(pointer); + + // Mark as unused + (void) pointer; +} + + +////////////////////////////////////////////////////////////// +///////////////////// OPTIONAL FUNCTIONS ///////////////////// +////////////////////////////////////////////////////////////// +// The implementation of below functions is optional. If you don't need them, don't include them in your +// plugin + +void mumble_setMumbleInfo(mumble_version_t mumbleVersion, mumble_version_t mumbleAPIVersion, mumble_version_t minimumExpectedAPIVersion) { + // this function will always be the first one to be called. Even before init() + // In here you can get info about the Mumble version this plugin is about to run in. + pLog() << "Mumble version: " << mumbleVersion << "; Mumble API-Version: " << mumbleAPIVersion << "; Minimal expected API-Version: " + << minimumExpectedAPIVersion << std::endl; +} + +mumble_version_t mumble_getVersion() { + // Mumble uses semantic versioning (see https://semver.org/) + // { major, minor, patch } + return { 1, 0, 0 }; +} + +MumbleStringWrapper mumble_getAuthor() { + static const char *author = "MumbleDevelopers"; + + MumbleStringWrapper wrapper; + wrapper.data = author; + wrapper.size = strlen(author); + // It's a static String and therefore doesn't need releasing + wrapper.needsReleasing = false; + + return wrapper; +} + +MumbleStringWrapper mumble_getDescription() { + static const char *description = + "This plugin is merely a reference implementation without any real functionality. It shouldn't be included in the release build of Mumble."; + + MumbleStringWrapper wrapper; + wrapper.data = description; + wrapper.size = strlen(description); + // It's a static String and therefore doesn't need releasing + wrapper.needsReleasing = false; + + return wrapper; +} + +uint32_t mumble_getFeatures() { + // Tells Mumble whether this plugin delivers some known common functionality. See the PluginFeature enum in + // PluginComponents.h for what is available. + // If you want your plugin to deliver positional data, you'll want to return FEATURE_POSITIONAL + return FEATURE_NONE; +} + +uint32_t mumble_deactivateFeatures(uint32_t features) { + pLog() << "Asked to deactivate feature set " << features << std::endl; + + // All features that can't be deactivated should be returned + return features; +} + +uint8_t mumble_initPositionalData(const char *const*programNames, const uint64_t *programPIDs, size_t programCount) { + std::ostream& stream = pLog() << "Got " << programCount << " programs to init positional data."; + + if (programCount > 0) { + stream << " The first name is " << programNames[0] << " and has PID " << programPIDs[0]; + } + + stream << std::endl; + + // As this plugin doesn't provide PD, we return PDEC_ERROR_PERM to indicate that even in the future we won't do so + // If your plugin is indeed delivering positional data but is only temporarily unable to do so, return PDEC_ERROR_TEMP. + // and if you deliver PD and succeeded initializing return PDEC_OK. + return PDEC_ERROR_PERM; +} + +#define SET_TO_ZERO(name) name[0] = 0.0f; name[1] = 0.0f; name[2] = 0.0f +bool mumble_fetchPositionalData(float *avatarPos, float *avatarDir, float *avatarAxis, float *cameraPos, float *cameraDir, + float *cameraAxis, const char **context, const char **identity) { + pluginLog("Has been asked to deliver positional data"); + + // If unable to provide positional data, this function should return false and reset all given values to 0 / empty Strings + SET_TO_ZERO(avatarPos); + SET_TO_ZERO(avatarDir); + SET_TO_ZERO(avatarAxis); + SET_TO_ZERO(cameraPos); + SET_TO_ZERO(cameraDir); + SET_TO_ZERO(cameraAxis); + *context = ""; + *identity = ""; + + // This function returns whether it can continue to deliver positional data + return false; +} +#undef SET_TO_ZERO + +void mumble_shutdownPositionalData() { + pluginLog("Shutting down positional data"); +} + +void mumble_onServerConnected(mumble_connection_t connection) { + activeConnection = connection; + + pLog() << "Established server-connection with ID " << connection << std::endl; + + // Use API function that'll block + mumAPI.log(ownID, "Connected to a server"); +} + +void mumble_onServerDisconnected(mumble_connection_t connection) { + activeConnection = -1; + + const char *serverHash; + if (mumAPI.getServerHash(ownID, connection, &serverHash) == STATUS_OK) { + pLog() << "Disconnected from server-connection with ID " << connection << "(hash: " << serverHash << ")" << std::endl; + + mumAPI.freeMemory(ownID, serverHash); + } else { + pluginLog("[ERROR]: mumble_onServerDisconnected - Unable to fetch server-hash"); + } +} + +void mumble_onServerSynchronized(mumble_connection_t connection) { + // The client has finished synchronizing with the server. Thus we can now obtain a list of all users on this server + const char *serverHash; + if (mumAPI.getServerHash(ownID, connection, &serverHash) == STATUS_OK) { + pLog() << "Server has finished synchronizing (ServerConnection: " << connection << "; hash: " << serverHash << ")" << std::endl ; + + mumAPI.freeMemory(ownID, serverHash); + } else { + pluginLog("[ERROR]: mumble_onServerSynchronized - Unable to fetch server-hash"); + } + + size_t userCount; + mumble_userid_t *userIDs; + + if (mumAPI.getAllUsers(ownID, activeConnection, &userIDs, &userCount) != STATUS_OK) { + pluginLog("[ERROR]: Can't obtain user list"); + return; + } + + mumble_userid_t localUserID; + if (mumAPI.getLocalUserID(ownID, connection, &localUserID) != STATUS_OK) { + pluginLog("[ERROR]: Can't obtain ID of local user"); + return; + } + + pLog() << "There are " << userCount << " users on this server. Their names are:" << std::endl; + + for(size_t i=0; i" << std::endl; + continue; + } + + const char *userHash; + if (mumAPI.getUserHash(ownID, connection, userIDs[i], &userHash) != STATUS_OK) { + pluginLog(""); + } + + pLog() << "\t" << userName << " (" << userHash << ")" << std::endl; + + // Mute the user "MuteMe" if this is not the name of the local user (in which case it'd fail) + if (userIDs[i] != localUserID && std::strcmp(userName, "MuteMe") == 0) { + if (mumAPI.requestLocalMute(ownID, connection, userIDs[i], true) != STATUS_OK) { + pluginLog("[ERROR]: Failed at muting user \"MuteMe\"!"); + } + } + + mumAPI.freeMemory(ownID, userName); + mumAPI.freeMemory(ownID, userHash); + } + + mumAPI.freeMemory(ownID, userIDs); + + size_t channelCount; + mumble_channelid_t *channelIDs; + + if (mumAPI.getAllChannels(ownID, activeConnection, &channelIDs, &channelCount) != STATUS_OK) { + pluginLog("[ERROR]: Failed to fetch channel list!"); + return; + } + + pLog() << "There are " << channelCount << " channels on this server" << std::endl; + + mumAPI.freeMemory(ownID, channelIDs); + + mumble_userid_t localUser; + if (mumAPI.getLocalUserID(ownID, activeConnection, &localUser) != STATUS_OK) { + pluginLog("Failed to retrieve local user ID"); + return; + } + + if (mumAPI.sendData(ownID, activeConnection, &localUser, 1, reinterpret_cast("Just a test"), 12, "testMsg") == STATUS_OK) { + pluginLog("Successfully sent plugin message"); + + // Try break the rate-limiter for plugin messages + for (int i = 0; i < 40; i++) { + std::string data = "Rate-limit message #" + std::to_string(i); + + mumAPI.sendData(ownID, activeConnection, &localUser, 1, reinterpret_cast(data.c_str()), data.size(), "testMsg"); + } + } else { + pluginLog("Failed at sending message"); + } + + if (mumAPI.requestSetLocalUserComment(ownID, connection, "This user has the TestPlugin enabled - hand over a cookie!") != STATUS_OK) { + pluginLog("Failed at setting the local user's comment"); + } +} + +void mumble_onChannelEntered(mumble_connection_t connection, mumble_userid_t userID, mumble_channelid_t previousChannelID, mumble_channelid_t newChannelID) { + std::ostream& stream = pLog() << "User with ID " << userID << " entered channel with ID " << newChannelID << "."; + + // negative ID means that there was no previous channel (e.g. because the user just connected) + if (previousChannelID >= 0) { + stream << " Came from channel with ID " << previousChannelID << "."; + } + + stream << " (ServerConnection: " << connection << ")" << std::endl; +} + +void mumble_onChannelExited(mumble_connection_t connection, mumble_userid_t userID, mumble_channelid_t channelID) { + pLog() << "User with ID " << userID << " has left channel with ID " << channelID << ". (ServerConnection: " << connection << ")" << std::endl; +} + +void mumble_onUserTalkingStateChanged(mumble_connection_t connection, mumble_userid_t userID, mumble_talking_state_t talkingState) { + std::ostream& stream = pLog() << "User with ID " << userID << " changed his talking state to "; + + // The possible values are contained in the TalkingState enum inside PluginComponent.h + switch(talkingState) { + case INVALID: + stream << "Invalid"; + break; + case PASSIVE: + stream << "Passive"; + break; + case TALKING: + stream << "Talking"; + break; + case WHISPERING: + stream << "Whispering"; + break; + case SHOUTING: + stream << "Shouting"; + break; + default: + stream << "Unknown (" << talkingState << ")"; + } + + stream << ". (ServerConnection: " << connection << ")" << std::endl; +} + +bool mumble_onAudioInput(short *inputPCM, uint32_t sampleCount, uint16_t channelCount, uint32_t sampleRate, bool isSpeech) { + // pLog() << "Audio input with " << channelCount << " channels and " << sampleCount << " samples per channel encountered. IsSpeech: " + // << isSpeech << " Sample rate is " << sampleRate << "Hz" << std::endl; + + // mark variables as unused + (void) inputPCM; + (void) sampleCount; + (void) channelCount; + (void) sampleRate; + (void) isSpeech; + + // This function returns whether it has modified the audio stream + return false; +} + +bool mumble_onAudioSourceFetched(float *outputPCM, uint32_t sampleCount, uint16_t channelCount, uint32_t sampleRate, bool isSpeech, mumble_userid_t userID) { + std::ostream& stream = pLog() << "Audio output source with " << channelCount << " channels and " << sampleCount << " samples per channel " + << "(" << sampleRate << " Hz) fetched."; + + if (isSpeech) { + stream << " The output is speech from user with ID " << userID << "."; + } + + stream << std::endl; + + // Mark ouputPCM as unused + (void) outputPCM; + + // This function returns whether it has modified the audio stream + return false; +} + +bool mumble_onAudioOutputAboutToPlay(float *outputPCM, uint32_t sampleCount, uint16_t channelCount, uint32_t sampleRate) { + // pLog() << "The resulting audio output has " << channelCount << " channels with " << sampleCount << " samples per channel (" + // sampleRate << " Hz)" << std::endl; + + // mark variables as unused + (void) outputPCM; + (void) sampleCount; + (void) channelCount; + (void) sampleRate; + + // This function returns whether it has modified the audio stream + return false; +} + +bool mumble_onReceiveData(mumble_connection_t connection, mumble_userid_t sender, const uint8_t *data, size_t dataLength, const char *dataID) { + pLog() << "Received data with ID \"" << dataID << "\" from user with ID " << sender << ". Its length is " << dataLength + << ". (ServerConnection:" << connection << ")" << std::endl; + + if (std::strcmp(dataID, "testMsg") == 0) { + // We know that data is only a normal C-encoded String, so the reinterpret_cast is safe + pLog() << "The received data: " << reinterpret_cast(data) << std::endl; + } + + // This function returns whether it has processed the data (preventing further plugins from seeing it) + return false; +} + +void mumble_onUserAdded(mumble_connection_t connection, mumble_userid_t userID) { + pLog() << "Added user with ID " << userID << " (ServerConnection: " << connection << ")" << std::endl; +} + +void mumble_onUserRemoved(mumble_connection_t connection, mumble_userid_t userID) { + pLog() << "Removed user with ID " << userID << " (ServerConnection: " << connection << ")" << std::endl; +} + +void mumble_onChannelAdded(mumble_connection_t connection, mumble_channelid_t channelID) { + pLog() << "Added channel with ID " << channelID << " (ServerConnection: " << connection << ")" << std::endl; +} + +void mumble_onChannelRemoved(mumble_connection_t connection, mumble_channelid_t channelID) { + pLog() << "Removed channel with ID " << channelID << " (ServerConnection: " << connection << ")" << std::endl; +} + +void mumble_onChannelRenamed(mumble_connection_t connection, mumble_channelid_t channelID) { + pLog() << "Renamed channel with ID " << channelID << " (ServerConnection: " << connection << ")" << std::endl; +} + +void mumble_onKeyEvent(uint32_t keyCode, bool wasPress) { + pLog() << "Encountered key " << (wasPress ? "press" : "release") << " of key with code " << keyCode << std::endl; +} + +bool mumble_hasUpdate() { + // This plugin never has an update + return false; +} + +MumbleStringWrapper mumble_getUpdateDownloadURL() { + static const char *url = "https://i.dont.exist/testplugin.zip"; + + MumbleStringWrapper wrapper; + wrapper.data = url; + wrapper.size = strlen(url); + // It's a static String and therefore doesn't need releasing + wrapper.needsReleasing = false; + + return wrapper; +} diff --git a/plugins/ut2004/ut2004.cpp b/plugins/ut2004/ut2004.cpp index 07a490fe1db..1da0776ed0b 100644 --- a/plugins/ut2004/ut2004.cpp +++ b/plugins/ut2004/ut2004.cpp @@ -3,7 +3,10 @@ // that can be found in the LICENSE file at the root of the // Mumble source tree or at . -#include "../mumble_plugin_main.h" +#define MUMBLE_ALLOW_DEPRECATED_LEGACY_PLUGIN_API +#include "../mumble_legacy_plugin.h" + +#include "../mumble_positional_audio_main.h" using namespace std; diff --git a/plugins/ut3/ut3.cpp b/plugins/ut3/ut3.cpp index bcf6d50c26f..981ef29a368 100644 --- a/plugins/ut3/ut3.cpp +++ b/plugins/ut3/ut3.cpp @@ -3,7 +3,10 @@ // that can be found in the LICENSE file at the root of the // Mumble source tree or at . -#include "../mumble_plugin_main.h" +#define MUMBLE_ALLOW_DEPRECATED_LEGACY_PLUGIN_API +#include "../mumble_legacy_plugin.h" + +#include "../mumble_positional_audio_main.h" using namespace std; diff --git a/plugins/ut99/ut99.cpp b/plugins/ut99/ut99.cpp index 8831452ba97..7ee98151a32 100644 --- a/plugins/ut99/ut99.cpp +++ b/plugins/ut99/ut99.cpp @@ -34,8 +34,11 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -#include "../mumble_plugin_main.h" -#include "../mumble_plugin_utils.h" +#define MUMBLE_ALLOW_DEPRECATED_LEGACY_PLUGIN_API +#include "../mumble_legacy_plugin.h" + +#include "../mumble_positional_audio_main.h" +#include "../mumble_positional_audio_utils.h" procptr_t posptr; procptr_t frtptr; diff --git a/plugins/wolfet/wolfet.cpp b/plugins/wolfet/wolfet.cpp index a8827ae698c..2dda0e7433e 100644 --- a/plugins/wolfet/wolfet.cpp +++ b/plugins/wolfet/wolfet.cpp @@ -47,7 +47,10 @@ Increasing when turning left. */ -#include "../mumble_plugin_main.h" +#define MUMBLE_ALLOW_DEPRECATED_LEGACY_PLUGIN_API +#include "../mumble_legacy_plugin.h" + +#include "../mumble_positional_audio_main.h" static int fetch(float *avatar_pos, float *avatar_front, float *avatar_top, float *camera_pos, float *camera_front, float *camera_top, std::string &context, std::wstring &) { diff --git a/plugins/wow/wow.cpp b/plugins/wow/wow.cpp index 5a4ffed6d85..e64fe9b899a 100644 --- a/plugins/wow/wow.cpp +++ b/plugins/wow/wow.cpp @@ -3,8 +3,11 @@ // that can be found in the LICENSE file at the root of the // Mumble source tree or at . -#include "../mumble_plugin_main.h" // Include standard plugin header. -#include "../mumble_plugin_utils.h" // Include plugin header for special functions, like "escape". +#define MUMBLE_ALLOW_DEPRECATED_LEGACY_PLUGIN_API +#include "../mumble_legacy_plugin.h" + +#include "../mumble_positional_audio_main.h" // Include standard positional audio header. +#include "../mumble_positional_audio_utils.h" // Include positional audio header for special functions, like "escape". static int fetch(float *avatar_pos, float *avatar_front, float *avatar_top, float *camera_pos, float *camera_front, float *camera_top, std::string &context, std::wstring &identity) { diff --git a/plugins/wow_x64/wow_x64.cpp b/plugins/wow_x64/wow_x64.cpp index f6ea550ad4f..50b04f6ada1 100644 --- a/plugins/wow_x64/wow_x64.cpp +++ b/plugins/wow_x64/wow_x64.cpp @@ -3,8 +3,11 @@ // that can be found in the LICENSE file at the root of the // Mumble source tree or at . -#include "../mumble_plugin_main.h" // Include standard plugin header. -#include "../mumble_plugin_utils.h" // Include plugin header for special functions, like "escape". +#define MUMBLE_ALLOW_DEPRECATED_LEGACY_PLUGIN_API +#include "../mumble_legacy_plugin.h" + +#include "../mumble_positional_audio_main.h" // Include standard positional audio header. +#include "../mumble_positional_audio_utils.h" // Include positional audio header for special functions, like "escape". static int fetch(float *avatar_pos, float *avatar_front, float *avatar_top, float *camera_pos, float *camera_front, float *camera_top, std::string &context, std::wstring &identity) { diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 7affde98eb5..747473b70ec 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -62,6 +62,7 @@ set(SHARED_SOURCES "PasswordGenerator.cpp" "PlatformCheck.cpp" "QtUtils.cpp" + "ProcessResolver.cpp" "SelfSignedCertificate.cpp" "ServerAddress.cpp" "ServerResolver.cpp" @@ -96,6 +97,7 @@ set(SHARED_HEADERS "OSInfo.h" "PasswordGenerator.h" "PlatformCheck.h" + "ProcessResolver.h" "SelfSignedCertificate.h" "ServerAddress.h" "ServerResolver.h" diff --git a/src/Channel.cpp b/src/Channel.cpp index 749fa0865ac..2462b2edbe7 100644 --- a/src/Channel.cpp +++ b/src/Channel.cpp @@ -11,6 +11,9 @@ #include #ifdef MUMBLE +# include "PluginManager.h" +# include "Global.h" + QHash< int, Channel * > Channel::c_qhChannels; QReadWriteLock Channel::c_qrwlChannels; #endif @@ -66,6 +69,12 @@ Channel *Channel::add(int id, const QString &name) { Channel *c = new Channel(id, name, nullptr); c_qhChannels.insert(id, c); + + // We have to use a direct connection here in order to make sure that the user object that gets passed to the callback + // does not get invalidated or deleted while the callback is running. + QObject::connect(c, &Channel::channelEntered, Global::get().pluginManager, &PluginManager::on_channelEntered, Qt::DirectConnection); + QObject::connect(c, &Channel::channelExited, Global::get().pluginManager, &PluginManager::on_channelExited, Qt::DirectConnection); + return c; } @@ -159,14 +168,20 @@ void Channel::removeChannel(Channel *c) { } void Channel::addUser(User *p) { - if (p->cChannel) - p->cChannel->removeUser(p); + Channel *prevChannel = p->cChannel; + + if (prevChannel) + prevChannel->removeUser(p); p->cChannel = this; qlUsers << p; + + emit channelEntered(this, prevChannel, p); } void Channel::removeUser(User *p) { qlUsers.removeAll(p); + + emit channelExited(this, p); } Channel::operator QString() const { diff --git a/src/Channel.h b/src/Channel.h index 578e839193f..95e6bf97281 100644 --- a/src/Channel.h +++ b/src/Channel.h @@ -99,6 +99,20 @@ class Channel : public QObject { QSet< Channel * > allChildren(); operator QString() const; + + signals: + /// Signal emitted whenever a user enters a channel. + /// + /// @param newChannel A pointer to the Channel the user has just entered + /// @param prevChannel A pointer to the Channel the user is coming from or nullptr if + /// there is no such channel. + /// @param user A pointer to the User that has triggered this signal + void channelEntered(const Channel *newChannel, const Channel *prevChannel, const User *user); + /// Signal emitted whenever a user leaves a channel. + /// + /// @param channel A pointer to the Channel the user has left + /// @param user A pointer to the User that has triggered this signal + void channelExited(const Channel *channel, const User *user); }; #endif diff --git a/src/Message.h b/src/Message.h index 72ed4c00dac..32c8d5f5f8b 100644 --- a/src/Message.h +++ b/src/Message.h @@ -41,7 +41,8 @@ MUMBLE_MH_MSG(UserStats) \ MUMBLE_MH_MSG(RequestBlob) \ MUMBLE_MH_MSG(ServerConfig) \ - MUMBLE_MH_MSG(SuggestConfig) + MUMBLE_MH_MSG(SuggestConfig) \ + MUMBLE_MH_MSG(PluginDataTransmission) class MessageHandler { public: diff --git a/src/Mumble.proto b/src/Mumble.proto index 3075c6928fb..5132e701163 100644 --- a/src/Mumble.proto +++ b/src/Mumble.proto @@ -584,3 +584,16 @@ message SuggestConfig { // True if the administrator suggests push to talk to be used on this server. optional bool push_to_talk = 3; } + +// Used to send plugin messages between clients +message PluginDataTransmission { + // The session ID of the client this message was sent from + optional uint32 senderSession = 1; + // The session IDs of the clients that should receive this message + repeated uint32 receiverSessions = 2 [packed = true]; + // The data that is sent + optional bytes data = 3; + // The ID of the sent data. This will be used by plugins to check whether they will + // process it or not + optional string dataID = 4; +} diff --git a/src/MumbleConstants.h b/src/MumbleConstants.h new file mode 100644 index 00000000000..df4fe49e5cb --- /dev/null +++ b/src/MumbleConstants.h @@ -0,0 +1,20 @@ +// Copyright 2021 The Mumble Developers. All rights reserved. +// Use of this source code is governed by a BSD-style license +// that can be found in the LICENSE file at the root of the +// Mumble source tree or at . + +#ifndef MUMBLE_MUMBLECONSTANTS_H_ +#define MUMBLE_MUMBLECONSTANTS_H_ + +namespace Mumble { +namespace Plugins { + namespace PluginMessage { + + constexpr int MAX_DATA_LENGTH = 1000; + constexpr int MAX_DATA_ID_LENGTH = 100; + + }; // namespace PluginMessage +}; // namespace Plugins +}; // namespace Mumble + +#endif // MUMBLE_MUMBLECONSTANTS_H_ diff --git a/src/ProcessResolver.cpp b/src/ProcessResolver.cpp new file mode 100644 index 00000000000..39011d4ba9c --- /dev/null +++ b/src/ProcessResolver.cpp @@ -0,0 +1,307 @@ +// Copyright 2021 The Mumble Developers. All rights reserved. +// Use of this source code is governed by a BSD-style license +// that can be found in the LICENSE file at the root of the +// Mumble source tree or at . + +#include "ProcessResolver.h" +#include + +ProcessResolver::ProcessResolver(bool resolveImmediately) + : m_processNames(), + m_processPIDs() { + if (resolveImmediately) { + resolve(); + } +} + +ProcessResolver::~ProcessResolver() { + freeAndClearData(); +} + +void ProcessResolver::freeAndClearData() { + // delete all names + foreach(const char *currentName, m_processNames) { + delete currentName; + } + + m_processNames.clear(); + m_processPIDs.clear(); +} + +const QVector& ProcessResolver::getProcessNames() const { + return m_processNames; +} + +const QVector& ProcessResolver::getProcessPIDs() const { + return m_processPIDs; +} + +void ProcessResolver::resolve() { + // first clear the current lists + freeAndClearData(); + + doResolve(); +} + +size_t ProcessResolver::amountOfProcesses() const { + return m_processPIDs.size(); +} + + +/// Helper function to add a name stored as a stack-variable to the given vector +/// +/// @param stackName The pointer to the stack-variable +/// @param destVec The destination vector to add the pointer to +void addName(const char *stackName, QVector& destVec) { + // We can't store the pointer of a stack-variable (will be invalid as soon as we exit scope) + // so we'll have to allocate memory on the heap and copy the name there. + size_t nameLength = std::strlen(stackName) + 1; // +1 for terminating NULL-byte + char *name = new char[nameLength]; + + std::strcpy(name, stackName); + + destVec.append(name); +} + +// The implementation of the doResolve-function is platfrom-dependent +// The different implementations are heavily inspired by the ones given at https://github.com/davidebeatrici/list-processes +#ifdef Q_OS_WIN + // Implementation for Windows + #ifndef UNICODE + #define UNICODE + #endif + + #ifndef WIN32_LEAN_AND_MEAN + #define WIN32_LEAN_AND_MEAN + #endif + + #include + #include + #include + + bool utf16ToUtf8(const wchar_t *source, const int size, char *destination) { + if (!WideCharToMultiByte(CP_UTF8, 0, source, -1, destination, size, NULL, NULL)) { +#ifndef QT_NO_DEBUG + qCritical("ProcessResolver: WideCharToMultiByte() failed with error %d\n", GetLastError()); +#endif + return false; + } + + return true; + } + + void ProcessResolver::doResolve() { + HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); + if (hSnap == INVALID_HANDLE_VALUE) { +#ifndef QT_NO_DEBUG + qCritical("ProcessResolver: CreateToolhelp32Snapshot() failed with error %d", GetLastError()); +#endif + return; + } + + PROCESSENTRY32 pe; + pe.dwSize = sizeof(pe); + + BOOL ok = Process32First(hSnap, &pe); + if (!ok) { +#ifndef QT_NO_DEBUG + qCritical("ProcessResolver: Process32First() failed with error %d\n", GetLastError()); +#endif + return; + } + + char name[MAX_PATH]; + + while (ok) { + if (utf16ToUtf8(pe.szExeFile, sizeof(name), name)) { + // Store name + addName(name, m_processNames); + + // Store corresponding PID + m_processPIDs.append(pe.th32ProcessID); + } +#ifndef QT_NO_DEBUG + else { + qWarning("ProcessResolver: utf16ToUtf8() failed, skipping entry..."); + } +#endif + + ok = Process32Next(hSnap, &pe); + } + + CloseHandle(hSnap); + } +#elif defined(Q_OS_LINUX) + // Implementation for Linux + #include + #include + #include + #include + #include + #include + + + static constexpr const char *PROC_DIR = "/proc/"; + + void ProcessResolver::doResolve() { + QDir procDir(QString::fromLatin1(PROC_DIR)); + QStringList entries = procDir.entryList(); + + bool ok; + + foreach(const QString& currentEntry, entries) { + uint64_t pid = static_cast(currentEntry.toLongLong(&ok, 10)); + + if (!ok) { + continue; + } + + QString exe = QFile::symLinkTarget(QString::fromLatin1(PROC_DIR) + currentEntry + QString::fromLatin1("/exe")); + QFileInfo fi(exe); + QString firstPart = fi.baseName(); + QString completeSuffix = fi.completeSuffix(); + QString baseName; + if (completeSuffix.isEmpty()) { + baseName = firstPart; + } else { + baseName = firstPart + QLatin1String(".") + completeSuffix; + } + + if (baseName == QLatin1String("wine-preloader") || baseName == QLatin1String("wine64-preloader")) { + QFile f(QString::fromLatin1(PROC_DIR) + currentEntry + QString::fromLatin1("/cmdline")); + if (f.open(QIODevice::ReadOnly)) { + QByteArray cmdline = f.readAll(); + f.close(); + + int nul = cmdline.indexOf('\0'); + if (nul != -1) { + cmdline.truncate(nul); + } + + QString exe = QString::fromUtf8(cmdline); + if (exe.contains(QLatin1String("\\"))) { + int lastBackslash = exe.lastIndexOf(QLatin1String("\\")); + if (exe.count() > lastBackslash + 1) { + baseName = exe.mid(lastBackslash + 1); + } + } + } + } + + if (!baseName.isEmpty()) { + // add name + addName(baseName.toUtf8().data(), m_processNames); + + // add corresponding PID + m_processPIDs.append(pid); + } + } + } +#elif defined(Q_OS_MACOS) + // Implementation for MacOS + // Code taken from https://stackoverflow.com/questions/49506579/how-to-find-the-pid-of-any-process-in-mac-osx-c + #include + + void ProcessResolver::doResolve() { + pid_t pids[2048]; + int bytes = proc_listpids(PROC_ALL_PIDS, 0, pids, sizeof(pids)); + int n_proc = bytes / sizeof(pids[0]); + for (int i = 0; i < n_proc; i++) { + struct proc_bsdinfo proc; + int st = proc_pidinfo(pids[i], PROC_PIDTBSDINFO, 0, + &proc, PROC_PIDTBSDINFO_SIZE); + if (st == PROC_PIDTBSDINFO_SIZE) { + // add name + addName(proc.pbi_name, m_processNames); + + // add corresponding PID + m_processPIDs.append(pids[i]); + } + } + } +#elif defined(Q_OS_FREEBSD) + // Implementation for FreeBSD + #include + #include + #include + + void ProcessResolver::doResolve() { + int n_procs; + struct kinfo_proc *procs_info = kinfo_getallproc(&n_procs); + if (!procs_info) { +#ifndef QT_NO_DEBUG + qCritical("ProcessResolver: kinfo_getallproc() failed\n"); +#endif + return; + } + + for (int i = 0; i < n_procs; ++i) { + // Add name + addName(procs_info[i].ki_comm, m_processNames); + + // Add corresponding PID + m_processPIDs.append(procs_info[i].ki_pid); + } + + free(procs_info); + } +#elif defined(Q_OS_BSD4) + // Implementation of generic BSD other than FreeBSD + #include + + #include + #include + #include + #include + #include + + bool kvm_cleanup(kvm_t *kd) { + if (kvm_close(kd) == -1) { +#ifndef QT_NO_DEBUG + qCritical("ProcessResolver: kvm_close() failed with error %d\n", errno); +#endif + return false; + } + + return true; + } + + void ProcessResolver::doResolve() { + char error[_POSIX2_LINE_MAX]; +#ifdef KVM_NO_FILES + kvm_t *kd = kvm_openfiles(NULL, NULL, NULL, KVM_NO_FILES, error); +#else + kvm_t *kd = kvm_openfiles(NULL, _PATH_DEVNULL, NULL, O_RDONLY, error); +#endif + + if (!kd) { +#ifndef QT_NO_DEBUG + qCritical("ProcessResolver: kvm_open2() failed with error: %s\n", error); +#endif + return; + } + + int n_procs; + struct kinfo_proc *procs_info = kvm_getprocs(kd, KERN_PROC_PROC, 0, &n_procs); + if (!procs_info) { +#ifndef QT_NO_DEBUG + qCritical("ProcessResolver: kvm_getprocs() failed\n"); +#endif + kvm_cleanup(kd); + + return; + } + + for (int i = 0; i < n_procs; ++i) { + // Add name + addName(procs_info[i].ki_comm, m_processNames); + + // Add corresponding PIDs + m_processPIDs.append(procs_info[i].ki_pid); + } + + kvm_cleanup(kd); + } +#else + #error "No implementation of ProcessResolver::resolve() available for this operating system" +#endif diff --git a/src/ProcessResolver.h b/src/ProcessResolver.h new file mode 100644 index 00000000000..59ada3a1c4b --- /dev/null +++ b/src/ProcessResolver.h @@ -0,0 +1,40 @@ +// Copyright 2021 The Mumble Developers. All rights reserved. +// Use of this source code is governed by a BSD-style license +// that can be found in the LICENSE file at the root of the +// Mumble source tree or at . + +#ifndef MUMBLE_PROCESS_RESOLVER_H_ +#define MUMBLE_PROCESS_RESOLVER_H_ + +#include +#include + +/// This ProcessResolver can be used to get a QVector of running process names and associated PIDs on multiple platforms. +/// This object is by no means thread-safe! +class ProcessResolver { + protected: + /// The vector for the pointers to the process names + QVector m_processNames; + /// The vector for the process PIDs + QVector m_processPIDs; + + /// Deletes all names currently stored in processNames and clears processNames and processPIDs + void freeAndClearData(); + /// The OS specific implementation of filling in details about running process names and PIDs + void doResolve(); + public: + /// @param resolveImmediately Whether the constructor should directly invoke ProcesResolver::resolve() + ProcessResolver(bool resolveImmediately = true); + virtual ~ProcessResolver(); + + /// Resolves the namaes and PIDs of the running processes + void resolve(); + /// Gets a reference to the stored process names + const QVector& getProcessNames() const; + /// Gets a reference to the stored process PIDs (corresponding to the names returned by ProcessResolver::getProcessNames()) + const QVector& getProcessPIDs() const; + /// @returns The amount of processes that have been resolved by this object + size_t amountOfProcesses() const; +}; + +#endif // MUMBLE_PROCESS_RESOLVER_H_ diff --git a/src/mumble/API.h b/src/mumble/API.h new file mode 100644 index 00000000000..74d8196ae7b --- /dev/null +++ b/src/mumble/API.h @@ -0,0 +1,188 @@ +// Copyright 2021 The Mumble Developers. All rights reserved. +// Use of this source code is governed by a BSD-style license +// that can be found in the LICENSE file at the root of the +// Mumble source tree or at . + +#ifndef MUMBLE_MUMBLE_API_H_ +#define MUMBLE_MUMBLE_API_H_ + +// In here the MumbleAPI struct is defined +#include "MumbleAPI_v_1_0_x.h" + +#include +#include +#include +#include + +#include + +namespace API { + +using api_future_t = std::future< mumble_error_t >; +using api_promise_t = std::promise< mumble_error_t >; + +/// A "curator" that will keep track of allocated resources and how to delete them +struct MumbleAPICurator { + struct Entry { + /// The function used to delete the corresponding pointer + std::function< void(const void *) > m_deleter; + /// The ID of the plugin the resource pointed at was allocated for + mumble_plugin_id_t m_pluginID; + /// The name of the API function the resource pointed to was allocated in + /// NOTE: This must only ever be a pointer to a String literal. + const char *m_sourceFunctionName; + }; + + std::unordered_map< const void *, Entry > m_entries; + + ~MumbleAPICurator(); +}; + +/// This object contains the actual API implementation. It also takes care of synchronizing API calls +/// with Mumble's main thread so that plugins can call them from an arbitrary thread without causing +/// issues. +/// This class is a singleton as a way to be able to write C function wrappers for the member functions +/// that are needed for passing to the plugins. +class MumbleAPI : public QObject { + Q_OBJECT; + Q_DISABLE_COPY(MumbleAPI); + +public: + static MumbleAPI &get(); + +public slots: + // The description of the functions is provided in MumbleAPI.h + + // Note that every slot is synchronized and is therefore guaranteed to be executed in the main + // thread. For the synchronization strategy see below. + void freeMemory_v_1_0_x(mumble_plugin_id_t callerID, const void *ptr, api_promise_t *promise); + void getActiveServerConnection_v_1_0_x(mumble_plugin_id_t callerID, mumble_connection_t *connection, + api_promise_t *promise); + void isConnectionSynchronized_v_1_0_x(mumble_plugin_id_t callerID, mumble_connection_t connection, + bool *synchronized, api_promise_t *promise); + void getLocalUserID_v_1_0_x(mumble_plugin_id_t callerID, mumble_connection_t connection, mumble_userid_t *userID, + api_promise_t *promise); + void getUserName_v_1_0_x(mumble_plugin_id_t callerID, mumble_connection_t connection, mumble_userid_t userID, + const char **name, api_promise_t *promise); + void getChannelName_v_1_0_x(mumble_plugin_id_t callerID, mumble_connection_t connection, + mumble_channelid_t channelID, const char **name, api_promise_t *promise); + void getAllUsers_v_1_0_x(mumble_plugin_id_t callerID, mumble_connection_t connection, mumble_userid_t **users, + size_t *userCount, api_promise_t *promise); + void getAllChannels_v_1_0_x(mumble_plugin_id_t callerID, mumble_connection_t connection, + mumble_channelid_t **channels, size_t *channelCount, api_promise_t *promise); + void getChannelOfUser_v_1_0_x(mumble_plugin_id_t callerID, mumble_connection_t connection, mumble_userid_t userID, + mumble_channelid_t *channelID, api_promise_t *promise); + void getUsersInChannel_v_1_0_x(mumble_plugin_id_t callerID, mumble_connection_t connection, + mumble_channelid_t channelID, mumble_userid_t **users, size_t *userCount, + api_promise_t *promise); + void getLocalUserTransmissionMode_v_1_0_x(mumble_plugin_id_t callerID, mumble_transmission_mode_t *transmissionMode, + api_promise_t *promise); + void isUserLocallyMuted_v_1_0_x(mumble_plugin_id_t callerID, mumble_connection_t connection, mumble_userid_t userID, + bool *muted, api_promise_t *promise); + void isLocalUserMuted_v_1_0_x(mumble_plugin_id_t callerID, bool *muted, api_promise_t *promise); + void isLocalUserDeafened_v_1_0_x(mumble_plugin_id_t callerID, bool *deafened, api_promise_t *promise); + void getUserHash_v_1_0_x(mumble_plugin_id_t callerID, mumble_connection_t connection, mumble_userid_t userID, + const char **hash, api_promise_t *promise); + void getServerHash_v_1_0_x(mumble_plugin_id_t callerID, mumble_connection_t connection, const char **hash, + api_promise_t *promise); + void requestLocalUserTransmissionMode_v_1_0_x(mumble_plugin_id_t callerID, + mumble_transmission_mode_t transmissionMode, api_promise_t *promise); + void getUserComment_v_1_0_x(mumble_plugin_id_t callerID, mumble_connection_t connection, mumble_userid_t userID, + const char **comment, api_promise_t *promise); + void getChannelDescription_v_1_0_x(mumble_plugin_id_t callerID, mumble_connection_t connection, + mumble_channelid_t channelID, const char **description, api_promise_t *promise); + void requestUserMove_v_1_0_x(mumble_plugin_id_t callerID, mumble_connection_t connection, mumble_userid_t userID, + mumble_channelid_t channelID, const char *password, api_promise_t *promise); + void requestMicrophoneActivationOverwrite_v_1_0_x(mumble_plugin_id_t callerID, bool activate, + api_promise_t *promise); + void requestLocalMute_v_1_0_x(mumble_plugin_id_t callerID, mumble_connection_t connection, mumble_userid_t userID, + bool muted, api_promise_t *promise); + void requestLocalUserMute_v_1_0_x(mumble_plugin_id_t callerID, bool muted, api_promise_t *promise); + void requestLocalUserDeaf_v_1_0_x(mumble_plugin_id_t callerID, bool deafened, api_promise_t *promise); + void requestSetLocalUserComment_v_1_0_x(mumble_plugin_id_t callerID, mumble_connection_t connection, + const char *comment, api_promise_t *promise); + void findUserByName_v_1_0_x(mumble_plugin_id_t callerID, mumble_connection_t connection, const char *userName, + mumble_userid_t *userID, api_promise_t *promise); + void findChannelByName_v_1_0_x(mumble_plugin_id_t callerID, mumble_connection_t connection, const char *channelName, + mumble_channelid_t *channelID, api_promise_t *promise); + void getMumbleSetting_bool_v_1_0_x(mumble_plugin_id_t callerID, mumble_settings_key_t key, bool *outValue, + api_promise_t *promise); + void getMumbleSetting_int_v_1_0_x(mumble_plugin_id_t callerID, mumble_settings_key_t key, int64_t *outValue, + api_promise_t *promise); + void getMumbleSetting_double_v_1_0_x(mumble_plugin_id_t callerID, mumble_settings_key_t key, double *outValue, + api_promise_t *promise); + void getMumbleSetting_string_v_1_0_x(mumble_plugin_id_t callerID, mumble_settings_key_t key, const char **outValue, + api_promise_t *promise); + void setMumbleSetting_bool_v_1_0_x(mumble_plugin_id_t callerID, mumble_settings_key_t key, bool value, + api_promise_t *promise); + void setMumbleSetting_int_v_1_0_x(mumble_plugin_id_t callerID, mumble_settings_key_t key, int64_t value, + api_promise_t *promise); + void setMumbleSetting_double_v_1_0_x(mumble_plugin_id_t callerID, mumble_settings_key_t key, double value, + api_promise_t *promise); + void setMumbleSetting_string_v_1_0_x(mumble_plugin_id_t callerID, mumble_settings_key_t key, const char *value, + api_promise_t *promise); + void sendData_v_1_0_x(mumble_plugin_id_t callerID, mumble_connection_t connection, const mumble_userid_t *users, + size_t userCount, const uint8_t *data, size_t dataLength, const char *dataID, + api_promise_t *promise); + void log_v_1_0_x(mumble_plugin_id_t callerID, const char *message, api_promise_t *promise); + void playSample_v_1_0_x(mumble_plugin_id_t callerID, const char *samplePath, api_promise_t *promise); + + +private: + MumbleAPI(); + + MumbleAPICurator m_curator; +}; + +/// @returns The Mumble API struct (v1.0.x) +MumbleAPI_v_1_0_x getMumbleAPI_v_1_0_x(); + +/// Converts from the Qt key-encoding to the API's key encoding. +/// +/// @param keyCode The Qt key-code that shall be converted +/// @returns The converted key code or KC_INVALID if conversion failed +mumble_keycode_t qtKeyCodeToAPIKeyCode(unsigned int keyCode); + +/// A class holding non-permanent data set by plugins. Non-permanent means that this data +/// will not be stored between restarts. +/// All member field should be atomic in order to be thread-safe +class PluginData { +public: + /// Constructor + PluginData(); + /// Destructor + ~PluginData(); + + /// A flag indicating whether a plugin has requested the microphone to be permanently on (mirroring the + /// behaviour of the continous transmission mode. + std::atomic_bool overwriteMicrophoneActivation; + + /// @returns A reference to the PluginData singleton + static PluginData &get(); +}; // class PluginData +}; // namespace API + + +// Declare the meta-types that we require in order for the API to work +Q_DECLARE_METATYPE(mumble_settings_key_t); +Q_DECLARE_METATYPE(mumble_settings_key_t *); +Q_DECLARE_METATYPE(mumble_transmission_mode_t); +Q_DECLARE_METATYPE(mumble_transmission_mode_t *); +Q_DECLARE_METATYPE(API::api_promise_t *); + +////////////////////////////////////////////////////////////// +///////////// SYNCHRONIZATION STRATEGY /////////////////////// +////////////////////////////////////////////////////////////// + +/** + * Every API function call checks whether it is being called from the main thread. If it is, + * it continues executing as usual. If it is not however, it uses Qt's signal/slot mechanism + * to schedule the respective function to be run in the main thread in the next iteration of + * the event loop. + * In order to synchronize with the calling thread, the return value (error code) of these + * functions is "returned" as a promise. Thus by accessing the exit code via the corresponding + * future, the calling thread is blocked until the function has been executed in the main thread + * (and thereby set the exit code once it is done allowing the calling thread to unblock). + */ + +#endif diff --git a/src/mumble/API_v_1_0_x.cpp b/src/mumble/API_v_1_0_x.cpp new file mode 100644 index 00000000000..ac8adf6f121 --- /dev/null +++ b/src/mumble/API_v_1_0_x.cpp @@ -0,0 +1,1980 @@ +// Copyright 2021 The Mumble Developers. All rights reserved. +// Use of this source code is governed by a BSD-style license +// that can be found in the LICENSE file at the root of the +// Mumble source tree or at . + +#include "API.h" +#include "AudioOutput.h" +#include "Channel.h" +#include "ClientUser.h" +#include "Database.h" +#include "Log.h" +#include "MainWindow.h" +#include "PluginComponents_v_1_0_x.h" +#include "PluginManager.h" +#include "ServerHandler.h" +#include "Settings.h" +#include "UserModel.h" +#include "MumbleConstants.h" +#include "Global.h" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#define EXIT_WITH(code) \ + if (promise) { \ + promise->set_value(code); \ + } \ + return; + +#define VERIFY_PLUGIN_ID(id) \ + if (!Global::get().pluginManager->pluginExists(id)) { \ + EXIT_WITH(EC_INVALID_PLUGIN_ID); \ + } + +// Right now there can only be one connection managed by the current ServerHandler +#define VERIFY_CONNECTION(connection) \ + if (!Global::get().sh || Global::get().sh->getConnectionID() != connection) { \ + EXIT_WITH(EC_CONNECTION_NOT_FOUND); \ + } + +// Right now whether or not a connection has finished synchronizing is indicated by Global::get().uiSession. If it is zero, +// synchronization is not done yet (or there is no connection to begin with). The connection parameter in the macro is +// only present in case it will be needed in the future +#define ENSURE_CONNECTION_SYNCHRONIZED(connection) \ + if (Global::get().uiSession == 0) { \ + EXIT_WITH(EC_CONNECTION_UNSYNCHRONIZED); \ + } + +#define UNUSED(var) (void) var; + +namespace API { + +MumbleAPICurator::~MumbleAPICurator() { + // free all remaining resources using the stored deleters + for (const auto ¤t : m_entries) { + const Entry &entry = current.second; + + // Delete leaked resource + entry.m_deleter(current.first); + + // Print an error about the leaked resource + printf("[ERROR]: Plugin with ID %d leaked memory from a call to API function \"%s\"\n", entry.m_pluginID, entry.m_sourceFunctionName); + } +} +// Some common delete-functions +void defaultDeleter(const void *ptr) { + // We use const-cast in order to circumvent the shortcoming of the free() signature only taking + // in void * and not const void *. Delete on the other hand is allowed on const pointers which is + // why this is an okay thing to do. + // See also https://stackoverflow.com/questions/2819535/unable-to-free-const-pointers-in-c + free(const_cast< void * >(ptr)); +} + + +///////////////////////////////////////////////////////////////////////////////////////// +/////////////////////////////////// API IMPLEMENTATION ////////////////////////////////// +///////////////////////////////////////////////////////////////////////////////////////// + +// This macro registers type, type * and type ** to Qt's metatype system +// and also their const variants (except const value as that doesn't make sense) +#define REGISTER_METATYPE(type) \ + qRegisterMetaType< type >(#type); \ + qRegisterMetaType< type * >(#type " *"); \ + qRegisterMetaType< type ** >(#type " **"); \ + qRegisterMetaType< const type * >("const " #type " *"); \ + qRegisterMetaType< const type ** >("const " #type " **"); \ + +MumbleAPI::MumbleAPI() { + // Move this object to the main thread + moveToThread(qApp->thread()); + + // Register all API types to Qt's metatype system + REGISTER_METATYPE(bool); + REGISTER_METATYPE(char); + REGISTER_METATYPE(double); + REGISTER_METATYPE(int); + REGISTER_METATYPE(int64_t); + REGISTER_METATYPE(mumble_channelid_t); + REGISTER_METATYPE(mumble_connection_t); + REGISTER_METATYPE(mumble_plugin_id_t); + REGISTER_METATYPE(mumble_settings_key_t); + REGISTER_METATYPE(mumble_transmission_mode_t); + REGISTER_METATYPE(mumble_userid_t); + REGISTER_METATYPE(mumble_userid_t); + REGISTER_METATYPE(size_t); + REGISTER_METATYPE(uint8_t); + + // Define additional types that can't be defined using macro REGISTER_METATYPE + qRegisterMetaType< api_promise_t * >("api_promise_t *"); + qRegisterMetaType< API::api_promise_t * >("API::api_promise_t *"); + qRegisterMetaType< const void * >("const void *"); + qRegisterMetaType< const void ** >("const void **"); + qRegisterMetaType< void * >("void *"); + qRegisterMetaType< void ** >("void **"); +} + +#undef REFGISTER_METATYPE + +MumbleAPI &MumbleAPI::get() { + static MumbleAPI api; + + return api; +} + +void MumbleAPI::freeMemory_v_1_0_x(mumble_plugin_id_t callerID, const void *ptr, api_promise_t *promise) { + if (QThread::currentThread() != thread()) { + // Invoke in main thread + QMetaObject::invokeMethod(this, "freeMemory_v_1_0_x", Qt::QueuedConnection, Q_ARG(mumble_plugin_id_t, callerID), + Q_ARG(const void *, ptr), Q_ARG(api_promise_t *, promise)); + + return; + } + + // Don't verify plugin ID here to avoid memory leaks + UNUSED(callerID); + + auto it = m_curator.m_entries.find(ptr); + if (it != m_curator.m_entries.cend()) { + MumbleAPICurator::Entry &entry = (*it).second; + + // call the deleter to delete the resource + entry.m_deleter(ptr); + + // Remove pointer from curator + m_curator.m_entries.erase(it); + + EXIT_WITH(STATUS_OK); + } else { + EXIT_WITH(EC_POINTER_NOT_FOUND); + } +} + +void MumbleAPI::getActiveServerConnection_v_1_0_x(mumble_plugin_id_t callerID, mumble_connection_t *connection, + api_promise_t *promise) { + if (QThread::currentThread() != thread()) { + // Invoke in main thread + QMetaObject::invokeMethod(this, "getActiveServerConnection_v_1_0_x", Qt::QueuedConnection, + Q_ARG(mumble_plugin_id_t, callerID), Q_ARG(mumble_connection_t *, connection), + Q_ARG(api_promise_t *, promise)); + + return; + } + + VERIFY_PLUGIN_ID(callerID); + + if (Global::get().sh) { + *connection = Global::get().sh->getConnectionID(); + + EXIT_WITH(STATUS_OK); + } else { + EXIT_WITH(EC_NO_ACTIVE_CONNECTION); + } +} + +void MumbleAPI::isConnectionSynchronized_v_1_0_x(mumble_plugin_id_t callerID, mumble_connection_t connection, + bool *synchronized, api_promise_t *promise) { + if (QThread::currentThread() != thread()) { + // Invoke in main thread + QMetaObject::invokeMethod(this, "isConnectionSynchronized_v_1_0_x", Qt::QueuedConnection, + Q_ARG(mumble_plugin_id_t, callerID), Q_ARG(mumble_connection_t, connection), + Q_ARG(bool *, synchronized), Q_ARG(api_promise_t *, promise)); + + return; + } + + VERIFY_PLUGIN_ID(callerID); + VERIFY_CONNECTION(connection); + + // Right now there can only be one connection and if Global::get().uiSession is zero, then the synchronization has not finished + // yet (or there is no connection to begin with) + *synchronized = Global::get().uiSession != 0; + + EXIT_WITH(STATUS_OK); +} + +void MumbleAPI::getLocalUserID_v_1_0_x(mumble_plugin_id_t callerID, mumble_connection_t connection, + mumble_userid_t *userID, api_promise_t *promise) { + if (QThread::currentThread() != thread()) { + // Invoke in main thread + QMetaObject::invokeMethod(this, "getLocalUserID_v_1_0_x", Qt::QueuedConnection, + Q_ARG(mumble_plugin_id_t, callerID), Q_ARG(mumble_connection_t, connection), + Q_ARG(mumble_userid_t *, userID), Q_ARG(api_promise_t *, promise)); + + return; + } + + VERIFY_PLUGIN_ID(callerID); + + VERIFY_CONNECTION(connection); + ENSURE_CONNECTION_SYNCHRONIZED(connection); + + *userID = Global::get().uiSession; + + EXIT_WITH(STATUS_OK); +} + +void MumbleAPI::getUserName_v_1_0_x(mumble_plugin_id_t callerID, mumble_connection_t connection, mumble_userid_t userID, + const char **name, api_promise_t *promise) { + if (QThread::currentThread() != thread()) { + // Invoke in main thread + QMetaObject::invokeMethod(this, "getUserName_v_1_0_x", Qt::QueuedConnection, + Q_ARG(mumble_plugin_id_t, callerID), Q_ARG(mumble_connection_t, connection), + Q_ARG(mumble_userid_t, userID), Q_ARG(const char **, name), + Q_ARG(api_promise_t *, promise)); + + return; + } + + VERIFY_PLUGIN_ID(callerID); + + VERIFY_CONNECTION(connection); + ENSURE_CONNECTION_SYNCHRONIZED(connection); + + const ClientUser *user = ClientUser::get(userID); + + if (user) { + // +1 for NULL terminator + size_t size = user->qsName.toUtf8().size() + 1; + + char *nameArray = reinterpret_cast< char * >(malloc(size * sizeof(char))); + + std::strcpy(nameArray, user->qsName.toUtf8().data()); + + // save the allocated pointer and how to delete it + m_curator.m_entries.insert({ nameArray, { defaultDeleter, callerID, "getUserName" } }); + + *name = nameArray; + + EXIT_WITH(STATUS_OK); + } else { + EXIT_WITH(EC_USER_NOT_FOUND); + } +} + +void MumbleAPI::getChannelName_v_1_0_x(mumble_plugin_id_t callerID, mumble_connection_t connection, + mumble_channelid_t channelID, const char **name, api_promise_t *promise) { + if (QThread::currentThread() != thread()) { + // Invoke in main thread + QMetaObject::invokeMethod(this, "getChannelName_v_1_0_x", Qt::QueuedConnection, + Q_ARG(mumble_plugin_id_t, callerID), Q_ARG(mumble_connection_t, connection), + Q_ARG(mumble_channelid_t, channelID), Q_ARG(const char **, name), + Q_ARG(api_promise_t *, promise)); + + return; + } + + VERIFY_PLUGIN_ID(callerID); + + VERIFY_CONNECTION(connection); + ENSURE_CONNECTION_SYNCHRONIZED(connection); + + const Channel *channel = Channel::get(channelID); + + if (channel) { + // +1 for NULL terminator + size_t size = channel->qsName.toUtf8().size() + 1; + + char *nameArray = reinterpret_cast< char * >(malloc(size * sizeof(char))); + + std::strcpy(nameArray, channel->qsName.toUtf8().data()); + + // save the allocated pointer and how to delete it + m_curator.m_entries.insert({ nameArray, { defaultDeleter, callerID, "getChannelName" } }); + + *name = nameArray; + + EXIT_WITH(STATUS_OK); + } else { + EXIT_WITH(EC_CHANNEL_NOT_FOUND); + } +} + +void MumbleAPI::getAllUsers_v_1_0_x(mumble_plugin_id_t callerID, mumble_connection_t connection, + mumble_userid_t **users, size_t *userCount, api_promise_t *promise) { + if (QThread::currentThread() != thread()) { + // Invoke in main thread + QMetaObject::invokeMethod(this, "getAllUsers_v_1_0_x", Qt::QueuedConnection, + Q_ARG(mumble_plugin_id_t, callerID), Q_ARG(mumble_connection_t, connection), + Q_ARG(mumble_userid_t **, users), Q_ARG(size_t *, userCount), + Q_ARG(api_promise_t *, promise)); + + return; + } + + VERIFY_PLUGIN_ID(callerID); + + VERIFY_CONNECTION(connection); + ENSURE_CONNECTION_SYNCHRONIZED(connection); + + QReadLocker userLock(&ClientUser::c_qrwlUsers); + + size_t amount = ClientUser::c_qmUsers.size(); + + auto it = ClientUser::c_qmUsers.constBegin(); + + mumble_userid_t *userIDs = reinterpret_cast< mumble_userid_t * >(malloc(sizeof(mumble_userid_t) * amount)); + + unsigned int index = 0; + while (it != ClientUser::c_qmUsers.constEnd()) { + userIDs[index] = it.key(); + + it++; + index++; + } + + m_curator.m_entries.insert({ userIDs, { defaultDeleter, callerID, "getAllUsers" } }); + + *users = userIDs; + *userCount = amount; + + EXIT_WITH(STATUS_OK); +} + +void MumbleAPI::getAllChannels_v_1_0_x(mumble_plugin_id_t callerID, mumble_connection_t connection, + mumble_channelid_t **channels, size_t *channelCount, api_promise_t *promise) { + if (QThread::currentThread() != thread()) { + // Invoke in main thread + QMetaObject::invokeMethod(this, "getAllChannels_v_1_0_x", Qt::QueuedConnection, + Q_ARG(mumble_plugin_id_t, callerID), Q_ARG(mumble_connection_t, connection), + Q_ARG(mumble_channelid_t **, channels), Q_ARG(size_t *, channelCount), + Q_ARG(api_promise_t *, promise)); + + return; + } + + VERIFY_PLUGIN_ID(callerID); + + VERIFY_CONNECTION(connection); + ENSURE_CONNECTION_SYNCHRONIZED(connection); + + QReadLocker channelLock(&Channel::c_qrwlChannels); + + size_t amount = Channel::c_qhChannels.size(); + + auto it = Channel::c_qhChannels.constBegin(); + + mumble_channelid_t *channelIDs = + reinterpret_cast< mumble_channelid_t * >(malloc(sizeof(mumble_channelid_t) * amount)); + + unsigned int index = 0; + while (it != Channel::c_qhChannels.constEnd()) { + channelIDs[index] = it.key(); + + it++; + index++; + } + + m_curator.m_entries.insert({ channelIDs, { defaultDeleter, callerID, "getAllChannels" } }); + + *channels = channelIDs; + *channelCount = amount; + + EXIT_WITH(STATUS_OK); +} + +void MumbleAPI::getChannelOfUser_v_1_0_x(mumble_plugin_id_t callerID, mumble_connection_t connection, + mumble_userid_t userID, mumble_channelid_t *channelID, + api_promise_t *promise) { + if (QThread::currentThread() != thread()) { + // Invoke in main thread + QMetaObject::invokeMethod(this, "getChannelOfUser_v_1_0_x", Qt::QueuedConnection, + Q_ARG(mumble_plugin_id_t, callerID), Q_ARG(mumble_connection_t, connection), + Q_ARG(mumble_userid_t, userID), Q_ARG(mumble_channelid_t *, channelID), + Q_ARG(api_promise_t *, promise)); + + return; + } + + VERIFY_PLUGIN_ID(callerID); + + VERIFY_CONNECTION(connection); + ENSURE_CONNECTION_SYNCHRONIZED(connection); + + const ClientUser *user = ClientUser::get(userID); + + if (!user) { + EXIT_WITH(EC_USER_NOT_FOUND); + } + + if (user->cChannel) { + *channelID = user->cChannel->iId; + + EXIT_WITH(STATUS_OK); + } else { + EXIT_WITH(EC_GENERIC_ERROR); + } +} + +void MumbleAPI::getUsersInChannel_v_1_0_x(mumble_plugin_id_t callerID, mumble_connection_t connection, + mumble_channelid_t channelID, mumble_userid_t **users, size_t *userCount, + api_promise_t *promise) { + if (QThread::currentThread() != thread()) { + // Invoke in main thread + QMetaObject::invokeMethod(this, "getUsersInChannel_v_1_0_x", Qt::QueuedConnection, + Q_ARG(mumble_plugin_id_t, callerID), Q_ARG(mumble_connection_t, connection), + Q_ARG(mumble_channelid_t, channelID), Q_ARG(mumble_userid_t **, users), + Q_ARG(size_t *, userCount), Q_ARG(api_promise_t *, promise)); + + return; + } + + VERIFY_PLUGIN_ID(callerID); + + VERIFY_CONNECTION(connection); + ENSURE_CONNECTION_SYNCHRONIZED(connection); + + const Channel *channel = Channel::get(channelID); + + if (!channel) { + EXIT_WITH(EC_CHANNEL_NOT_FOUND); + } + + size_t amount = channel->qlUsers.size(); + + mumble_userid_t *userIDs = reinterpret_cast< mumble_userid_t * >(malloc(sizeof(mumble_userid_t) * amount)); + + int index = 0; + foreach (const User *currentUser, channel->qlUsers) { + userIDs[index] = currentUser->uiSession; + + index++; + } + + m_curator.m_entries.insert({ userIDs, { defaultDeleter, callerID, "getUsersInChannel" } }); + + *users = userIDs; + *userCount = amount; + + EXIT_WITH(STATUS_OK); +} + +void MumbleAPI::getLocalUserTransmissionMode_v_1_0_x(mumble_plugin_id_t callerID, + mumble_transmission_mode_t *transmissionMode, + api_promise_t *promise) { + if (QThread::currentThread() != thread()) { + // Invoke in main thread + QMetaObject::invokeMethod( + this, "getLocalUserTransmissionMode_v_1_0_x", Qt::QueuedConnection, Q_ARG(mumble_plugin_id_t, callerID), + Q_ARG(mumble_transmission_mode_t *, transmissionMode), Q_ARG(api_promise_t *, promise)); + + return; + } + + VERIFY_PLUGIN_ID(callerID); + + switch (Global::get().s.atTransmit) { + case Settings::AudioTransmit::Continuous: + *transmissionMode = TM_CONTINOUS; + EXIT_WITH(STATUS_OK); + case Settings::AudioTransmit::VAD: + *transmissionMode = TM_VOICE_ACTIVATION; + EXIT_WITH(STATUS_OK); + case Settings::AudioTransmit::PushToTalk: + *transmissionMode = TM_PUSH_TO_TALK; + EXIT_WITH(STATUS_OK); + } + + // Unable to resolve transmission mode + EXIT_WITH(EC_GENERIC_ERROR); +} + +void MumbleAPI::isUserLocallyMuted_v_1_0_x(mumble_plugin_id_t callerID, mumble_connection_t connection, + mumble_userid_t userID, bool *muted, api_promise_t *promise) { + if (QThread::currentThread() != thread()) { + // Invoke in main thread + QMetaObject::invokeMethod(this, "isUserLocallyMuted_v_1_0_x", Qt::QueuedConnection, + Q_ARG(mumble_plugin_id_t, callerID), Q_ARG(mumble_connection_t, connection), + Q_ARG(mumble_userid_t, userID), Q_ARG(bool *, muted), + Q_ARG(api_promise_t *, promise)); + + return; + } + + VERIFY_PLUGIN_ID(callerID); + + VERIFY_CONNECTION(connection); + ENSURE_CONNECTION_SYNCHRONIZED(connection); + + const ClientUser *user = ClientUser::get(userID); + + if (!user) { + EXIT_WITH(EC_USER_NOT_FOUND); + } + + *muted = user->bLocalMute; + + EXIT_WITH(STATUS_OK); +} + +void MumbleAPI::isLocalUserMuted_v_1_0_x(mumble_plugin_id_t callerID, bool *muted, api_promise_t *promise) { + if (QThread::currentThread() != thread()) { + // Invoke in main thread + QMetaObject::invokeMethod(this, "isLocalUserMuted_v_1_0_x", Qt::QueuedConnection, + Q_ARG(mumble_plugin_id_t, callerID), Q_ARG(bool *, muted), + Q_ARG(api_promise_t *, promise)); + + return; + } + + VERIFY_PLUGIN_ID(callerID); + + *muted = Global::get().s.bMute; + + EXIT_WITH(STATUS_OK); +} + +void MumbleAPI::isLocalUserDeafened_v_1_0_x(mumble_plugin_id_t callerID, bool *deafened, api_promise_t *promise) { + if (QThread::currentThread() != thread()) { + // Invoke in main thread + QMetaObject::invokeMethod(this, "isLocalUserDeafened_v_1_0_x", Qt::QueuedConnection, + Q_ARG(mumble_plugin_id_t, callerID), Q_ARG(bool *, deafened), + Q_ARG(api_promise_t *, promise)); + + return; + } + + VERIFY_PLUGIN_ID(callerID); + + *deafened = Global::get().s.bDeaf; + + EXIT_WITH(STATUS_OK); +} + +void MumbleAPI::getUserHash_v_1_0_x(mumble_plugin_id_t callerID, mumble_connection_t connection, mumble_userid_t userID, + const char **hash, api_promise_t *promise) { + if (QThread::currentThread() != thread()) { + // Invoke in main thread + QMetaObject::invokeMethod(this, "getUserHash_v_1_0_x", Qt::QueuedConnection, + Q_ARG(mumble_plugin_id_t, callerID), Q_ARG(mumble_connection_t, connection), + Q_ARG(mumble_userid_t, userID), Q_ARG(const char **, hash), + Q_ARG(api_promise_t *, promise)); + + return; + } + + VERIFY_PLUGIN_ID(callerID); + + VERIFY_CONNECTION(connection); + ENSURE_CONNECTION_SYNCHRONIZED(connection); + + const ClientUser *user = ClientUser::get(userID); + + if (!user) { + EXIT_WITH(EC_USER_NOT_FOUND); + } + + // The user's hash is already in hexadecimal representation, so we don't have to worry about null-bytes in it + // +1 for NULL terminator + size_t size = user->qsHash.toUtf8().size() + 1; + + char *hashArray = reinterpret_cast< char * >(malloc(size * sizeof(char))); + + std::strcpy(hashArray, user->qsHash.toUtf8().data()); + + m_curator.m_entries.insert({ hashArray, { defaultDeleter, callerID, "getUserHash" } }); + + *hash = hashArray; + + EXIT_WITH(STATUS_OK); +} + +void MumbleAPI::getServerHash_v_1_0_x(mumble_plugin_id_t callerID, mumble_connection_t connection, const char **hash, + api_promise_t *promise) { + if (QThread::currentThread() != thread()) { + // Invoke in main thread + QMetaObject::invokeMethod(this, "getServerHash_v_1_0_x", Qt::QueuedConnection, + Q_ARG(mumble_plugin_id_t, callerID), Q_ARG(mumble_connection_t, connection), + Q_ARG(const char **, hash), Q_ARG(api_promise_t *, promise)); + + return; + } + + VERIFY_PLUGIN_ID(callerID); + + VERIFY_CONNECTION(connection); + ENSURE_CONNECTION_SYNCHRONIZED(connection); + + // Use hexadecimal representation in order for the String to be properly printable and for it to be C-encodable + QByteArray hashHex = Global::get().sh->qbaDigest.toHex(); + QString strHash = QString::fromLatin1(hashHex); + + // +1 for NULL terminator + size_t size = strHash.toUtf8().size() + 1; + + char *hashArray = reinterpret_cast< char * >(malloc(size * sizeof(char))); + + std::strcpy(hashArray, strHash.toUtf8().data()); + + m_curator.m_entries.insert({ hashArray, { defaultDeleter, callerID, "getServerHash" } }); + + *hash = hashArray; + + EXIT_WITH(STATUS_OK); +} + +void MumbleAPI::requestLocalUserTransmissionMode_v_1_0_x(mumble_plugin_id_t callerID, + mumble_transmission_mode_t transmissionMode, + api_promise_t *promise) { + if (QThread::currentThread() != thread()) { + // Invoke in main thread + QMetaObject::invokeMethod(this, "requestLocalUserTransmissionMode_v_1_0_x", Qt::QueuedConnection, + Q_ARG(mumble_plugin_id_t, callerID), + Q_ARG(mumble_transmission_mode_t, transmissionMode), Q_ARG(api_promise_t *, promise)); + + return; + } + + VERIFY_PLUGIN_ID(callerID); + + switch (transmissionMode) { + case TM_CONTINOUS: + Global::get().s.atTransmit = Settings::AudioTransmit::Continuous; + EXIT_WITH(STATUS_OK); + case TM_VOICE_ACTIVATION: + Global::get().s.atTransmit = Settings::AudioTransmit::VAD; + EXIT_WITH(STATUS_OK); + case TM_PUSH_TO_TALK: + Global::get().s.atTransmit = Settings::AudioTransmit::PushToTalk; + EXIT_WITH(STATUS_OK); + } + + EXIT_WITH(EC_UNKNOWN_TRANSMISSION_MODE); +} + +void MumbleAPI::getUserComment_v_1_0_x(mumble_plugin_id_t callerID, mumble_connection_t connection, + mumble_userid_t userID, const char **comment, api_promise_t *promise) { + if (QThread::currentThread() != thread()) { + // Invoke in main thread + QMetaObject::invokeMethod(this, "getUserComment_v_1_0_x", Qt::QueuedConnection, + Q_ARG(mumble_plugin_id_t, callerID), Q_ARG(mumble_connection_t, connection), + Q_ARG(mumble_userid_t, userID), Q_ARG(const char **, comment), + Q_ARG(api_promise_t *, promise)); + + return; + } + + VERIFY_PLUGIN_ID(callerID); + + VERIFY_CONNECTION(connection); + ENSURE_CONNECTION_SYNCHRONIZED(connection); + + ClientUser *user = ClientUser::get(userID); + + if (!user) { + EXIT_WITH(EC_USER_NOT_FOUND); + } + + if (user->qsComment.isEmpty() && !user->qbaCommentHash.isEmpty()) { + user->qsComment = QString::fromUtf8(Global::get().db->blob(user->qbaCommentHash)); + + if (user->qsComment.isEmpty()) { + // The user's comment hasn't been synchronized to this client yet + EXIT_WITH(EC_UNSYNCHRONIZED_BLOB); + } + } + + // +1 for NULL terminator + size_t size = user->qsComment.toUtf8().size() + 1; + + char *nameArray = reinterpret_cast< char * >(malloc(size * sizeof(char))); + + std::strcpy(nameArray, user->qsComment.toUtf8().data()); + + m_curator.m_entries.insert({ nameArray, { defaultDeleter, callerID, "getUserComment" } }); + + *comment = nameArray; + + EXIT_WITH(STATUS_OK); +} + +void MumbleAPI::getChannelDescription_v_1_0_x(mumble_plugin_id_t callerID, mumble_connection_t connection, + mumble_channelid_t channelID, const char **description, + api_promise_t *promise) { + if (QThread::currentThread() != thread()) { + // Invoke in main thread + QMetaObject::invokeMethod(this, "getChannelDescription_v_1_0_x", Qt::QueuedConnection, + Q_ARG(mumble_plugin_id_t, callerID), Q_ARG(mumble_connection_t, connection), + Q_ARG(mumble_channelid_t, channelID), Q_ARG(const char **, description), + Q_ARG(api_promise_t *, promise)); + + return; + } + + VERIFY_PLUGIN_ID(callerID); + + VERIFY_CONNECTION(connection); + ENSURE_CONNECTION_SYNCHRONIZED(connection); + + Channel *channel = Channel::get(channelID); + + if (!channel) { + EXIT_WITH(EC_CHANNEL_NOT_FOUND); + } + + if (channel->qsDesc.isEmpty() && !channel->qbaDescHash.isEmpty()) { + channel->qsDesc = QString::fromUtf8(Global::get().db->blob(channel->qbaDescHash)); + + if (channel->qsDesc.isEmpty()) { + // The channel's description hasn't been synchronized to this client yet + EXIT_WITH(EC_UNSYNCHRONIZED_BLOB); + } + } + + // +1 for NULL terminator + size_t size = channel->qsDesc.toUtf8().size() + 1; + + char *nameArray = reinterpret_cast< char * >(malloc(size * sizeof(char))); + + std::strcpy(nameArray, channel->qsDesc.toUtf8().data()); + + m_curator.m_entries.insert({ nameArray, { defaultDeleter, callerID, "getChannelDescription" } }); + + *description = nameArray; + + EXIT_WITH(STATUS_OK); +} + +void MumbleAPI::requestUserMove_v_1_0_x(mumble_plugin_id_t callerID, mumble_connection_t connection, + mumble_userid_t userID, mumble_channelid_t channelID, const char *password, + api_promise_t *promise) { + if (QThread::currentThread() != thread()) { + // Invoke in main thread + QMetaObject::invokeMethod(this, "requestUserMove_v_1_0_x", Qt::QueuedConnection, + Q_ARG(mumble_plugin_id_t, callerID), Q_ARG(mumble_connection_t, connection), + Q_ARG(mumble_userid_t, userID), Q_ARG(mumble_channelid_t, channelID), + Q_ARG(const char *, password), Q_ARG(api_promise_t *, promise)); + + return; + } + + VERIFY_PLUGIN_ID(callerID); + + VERIFY_CONNECTION(connection); + ENSURE_CONNECTION_SYNCHRONIZED(connection); + + const ClientUser *user = ClientUser::get(userID); + + if (!user) { + EXIT_WITH(EC_USER_NOT_FOUND); + } + + const Channel *channel = Channel::get(channelID); + + if (!channel) { + EXIT_WITH(EC_CHANNEL_NOT_FOUND); + } + + if (channel != user->cChannel) { + // send move-request to the server only if the user is not in the channel already + QStringList passwordList; + if (password) { + passwordList << QString::fromUtf8(password); + } + + Global::get().sh->joinChannel(user->uiSession, channel->iId, passwordList); + } + + EXIT_WITH(STATUS_OK); +} + +void MumbleAPI::requestMicrophoneActivationOverwrite_v_1_0_x(mumble_plugin_id_t callerID, bool activate, + api_promise_t *promise) { + if (QThread::currentThread() != thread()) { + // Invoke in main thread + QMetaObject::invokeMethod(this, "requestMicrophoneActivationOverwrite_v_1_0_x", Qt::QueuedConnection, + Q_ARG(mumble_plugin_id_t, callerID), Q_ARG(bool, activate), + Q_ARG(api_promise_t *, promise)); + + return; + } + + VERIFY_PLUGIN_ID(callerID); + + PluginData::get().overwriteMicrophoneActivation.store(activate); + + EXIT_WITH(STATUS_OK); +} + +void MumbleAPI::requestLocalMute_v_1_0_x(mumble_plugin_id_t callerID, mumble_connection_t connection, + mumble_userid_t userID, bool muted, api_promise_t *promise) { + if (QThread::currentThread() != thread()) { + // Invoke in main thread + QMetaObject::invokeMethod(this, "requestLocalMute_v_1_0_x", Qt::QueuedConnection, + Q_ARG(mumble_plugin_id_t, callerID), Q_ARG(mumble_connection_t, connection), + Q_ARG(mumble_userid_t, userID), Q_ARG(bool, muted), Q_ARG(api_promise_t *, promise)); + + return; + } + + VERIFY_PLUGIN_ID(callerID); + + VERIFY_CONNECTION(connection); + ENSURE_CONNECTION_SYNCHRONIZED(connection); + + if (userID == Global::get().uiSession) { + // Can't locally mute the local user + EXIT_WITH(EC_INVALID_MUTE_TARGET); + } + + ClientUser *user = ClientUser::get(userID); + + if (!user) { + EXIT_WITH(EC_USER_NOT_FOUND); + } + + user->setLocalMute(muted); + + EXIT_WITH(STATUS_OK); +} + +void MumbleAPI::requestLocalUserMute_v_1_0_x(mumble_plugin_id_t callerID, bool muted, api_promise_t *promise) { + if (QThread::currentThread() != thread()) { + // Invoke in main thread + QMetaObject::invokeMethod(this, "requestLocalUserMute_v_1_0_x", Qt::QueuedConnection, + Q_ARG(mumble_plugin_id_t, callerID), Q_ARG(bool, muted), + Q_ARG(api_promise_t *, promise)); + + return; + } + + VERIFY_PLUGIN_ID(callerID); + + if (!Global::get().mw) { + EXIT_WITH(EC_INTERNAL_ERROR); + } + + Global::get().mw->setAudioMute(muted); + + EXIT_WITH(STATUS_OK); +} + +void MumbleAPI::requestLocalUserDeaf_v_1_0_x(mumble_plugin_id_t callerID, bool deafened, api_promise_t *promise) { + if (QThread::currentThread() != thread()) { + // Invoke in main thread + QMetaObject::invokeMethod(this, "requestLocalUserDeaf_v_1_0_x", Qt::QueuedConnection, + Q_ARG(mumble_plugin_id_t, callerID), Q_ARG(bool, deafened), + Q_ARG(api_promise_t *, promise)); + + return; + } + + VERIFY_PLUGIN_ID(callerID); + + if (!Global::get().mw) { + EXIT_WITH(EC_INTERNAL_ERROR); + } + + Global::get().mw->setAudioDeaf(deafened); + + EXIT_WITH(STATUS_OK); +} + +void MumbleAPI::requestSetLocalUserComment_v_1_0_x(mumble_plugin_id_t callerID, mumble_connection_t connection, + const char *comment, api_promise_t *promise) { + if (QThread::currentThread() != thread()) { + // Invoke in main thread + QMetaObject::invokeMethod(this, "requestSetLocalUserComment_v_1_0_x", Qt::QueuedConnection, + Q_ARG(mumble_plugin_id_t, callerID), Q_ARG(mumble_connection_t, connection), + Q_ARG(const char *, comment), Q_ARG(api_promise_t *, promise)); + + return; + } + + VERIFY_PLUGIN_ID(callerID); + + VERIFY_CONNECTION(connection); + ENSURE_CONNECTION_SYNCHRONIZED(connection); + + ClientUser *localUser = ClientUser::get(Global::get().uiSession); + + if (!localUser) { + EXIT_WITH(EC_USER_NOT_FOUND); + } + + if (!Global::get().mw || !Global::get().mw->pmModel) { + EXIT_WITH(EC_INTERNAL_ERROR); + } + + Global::get().mw->pmModel->setComment(localUser, QString::fromUtf8(comment)); + + EXIT_WITH(STATUS_OK); +} + +void MumbleAPI::findUserByName_v_1_0_x(mumble_plugin_id_t callerID, mumble_connection_t connection, + const char *userName, mumble_userid_t *userID, api_promise_t *promise) { + if (QThread::currentThread() != thread()) { + // Invoke in main thread + QMetaObject::invokeMethod(this, "findUserByName_v_1_0_x", Qt::QueuedConnection, + Q_ARG(mumble_plugin_id_t, callerID), Q_ARG(mumble_connection_t, connection), + Q_ARG(const char *, userName), Q_ARG(mumble_userid_t *, userID), + Q_ARG(api_promise_t *, promise)); + + return; + } + + VERIFY_PLUGIN_ID(callerID); + + VERIFY_CONNECTION(connection); + ENSURE_CONNECTION_SYNCHRONIZED(connection); + + const QString qsUserName = QString::fromUtf8(userName); + + QReadLocker userLock(&ClientUser::c_qrwlUsers); + + auto it = ClientUser::c_qmUsers.constBegin(); + while (it != ClientUser::c_qmUsers.constEnd()) { + if (it.value()->qsName == qsUserName) { + *userID = it.key(); + + EXIT_WITH(STATUS_OK); + } + + it++; + } + + EXIT_WITH(EC_USER_NOT_FOUND); +} + +void MumbleAPI::findChannelByName_v_1_0_x(mumble_plugin_id_t callerID, mumble_connection_t connection, + const char *channelName, mumble_channelid_t *channelID, + api_promise_t *promise) { + if (QThread::currentThread() != thread()) { + // Invoke in main thread + QMetaObject::invokeMethod(this, "findChannelByName_v_1_0_x", Qt::QueuedConnection, + Q_ARG(mumble_plugin_id_t, callerID), Q_ARG(mumble_connection_t, connection), + Q_ARG(const char *, channelName), Q_ARG(mumble_channelid_t *, channelID), + Q_ARG(api_promise_t *, promise)); + + return; + } + + VERIFY_PLUGIN_ID(callerID); + + VERIFY_CONNECTION(connection); + ENSURE_CONNECTION_SYNCHRONIZED(connection); + + const QString qsChannelName = QString::fromUtf8(channelName); + + QReadLocker channelLock(&Channel::c_qrwlChannels); + + auto it = Channel::c_qhChannels.constBegin(); + while (it != Channel::c_qhChannels.constEnd()) { + if (it.value()->qsName == qsChannelName) { + *channelID = it.key(); + + EXIT_WITH(STATUS_OK); + } + + it++; + } + + EXIT_WITH(EC_CHANNEL_NOT_FOUND); +} + +QVariant getMumbleSettingHelper(mumble_settings_key_t key) { + QVariant value; + + // All values are explicitly cast to the target type of their associated API. For instance there is not API to + // get float values but there is one for doubles. Therefore floats have to be cast to doubles in order for the + // type checking to work out. + switch (key) { + case MSK_AUDIO_INPUT_VOICE_HOLD: + value = static_cast< int >(Global::get().s.iVoiceHold); + break; + case MSK_AUDIO_INPUT_VAD_SILENCE_THRESHOLD: + value = static_cast< double >(Global::get().s.fVADmin); + break; + case MSK_AUDIO_INPUT_VAD_SPEECH_THRESHOLD: + value = static_cast< double >(Global::get().s.fVADmax); + break; + case MSK_AUDIO_OUTPUT_PA_MINIMUM_DISTANCE: + value = static_cast< double >(Global::get().s.fAudioMinDistance); + break; + case MSK_AUDIO_OUTPUT_PA_MAXIMUM_DISTANCE: + value = static_cast< double >(Global::get().s.fAudioMaxDistance); + break; + case MSK_AUDIO_OUTPUT_PA_BLOOM: + value = static_cast< double >(Global::get().s.fAudioBloom); + break; + case MSK_AUDIO_OUTPUT_PA_MINIMUM_VOLUME: + value = static_cast< double >(Global::get().s.fAudioMaxDistVolume); + break; + case MSK_INVALID: + // There is no setting associated with this key + break; + } + + return value; +} + +// IS_TYPE actually only checks if the QVariant can be converted to the needed type since that's all that we really care +// about at the end of the day. +#define IS_TYPE(var, varType) static_cast< QMetaType::Type >(var.type()) == varType +#define IS_NOT_TYPE(var, varType) static_cast< QMetaType::Type >(var.type()) != varType + +void MumbleAPI::getMumbleSetting_bool_v_1_0_x(mumble_plugin_id_t callerID, mumble_settings_key_t key, bool *outValue, + api_promise_t *promise) { + if (QThread::currentThread() != thread()) { + // Invoke in main thread + QMetaObject::invokeMethod(this, "getMumbleSetting_bool_v_1_0_x", Qt::QueuedConnection, + Q_ARG(mumble_plugin_id_t, callerID), Q_ARG(mumble_settings_key_t, key), + Q_ARG(bool *, outValue), Q_ARG(api_promise_t *, promise)); + + return; + } + + VERIFY_PLUGIN_ID(callerID); + + QVariant value = getMumbleSettingHelper(key); + + if (!value.isValid()) { + // We also return that for MSK_INVALID + EXIT_WITH(EC_UNKNOWN_SETTINGS_KEY); + } + + if (IS_NOT_TYPE(value, QMetaType::Bool)) { + EXIT_WITH(EC_WRONG_SETTINGS_TYPE); + } + + *outValue = value.toBool(); + + EXIT_WITH(STATUS_OK); +} + +void MumbleAPI::getMumbleSetting_int_v_1_0_x(mumble_plugin_id_t callerID, mumble_settings_key_t key, int64_t *outValue, + api_promise_t *promise) { + if (QThread::currentThread() != thread()) { + // Invoke in main thread + QMetaObject::invokeMethod(this, "getMumbleSetting_int_v_1_0_x", Qt::QueuedConnection, + Q_ARG(mumble_plugin_id_t, callerID), Q_ARG(mumble_settings_key_t, key), + Q_ARG(int64_t *, outValue), Q_ARG(api_promise_t *, promise)); + + return; + } + + VERIFY_PLUGIN_ID(callerID); + + QVariant value = getMumbleSettingHelper(key); + + if (!value.isValid()) { + // We also return that for MSK_INVALID + EXIT_WITH(EC_UNKNOWN_SETTINGS_KEY); + } + + if (IS_NOT_TYPE(value, QMetaType::Int)) { + EXIT_WITH(EC_WRONG_SETTINGS_TYPE); + } + + *outValue = value.toInt(); + + EXIT_WITH(STATUS_OK); +} + +void MumbleAPI::getMumbleSetting_double_v_1_0_x(mumble_plugin_id_t callerID, mumble_settings_key_t key, + double *outValue, api_promise_t *promise) { + if (QThread::currentThread() != thread()) { + // Invoke in main thread + QMetaObject::invokeMethod(this, "getMumbleSetting_double_v_1_0_x", Qt::QueuedConnection, + Q_ARG(mumble_plugin_id_t, callerID), Q_ARG(mumble_settings_key_t, key), + Q_ARG(double *, outValue), Q_ARG(api_promise_t *, promise)); + + return; + } + + VERIFY_PLUGIN_ID(callerID); + + QVariant value = getMumbleSettingHelper(key); + + if (!value.isValid()) { + // We also return that for MSK_INVALID + EXIT_WITH(EC_UNKNOWN_SETTINGS_KEY); + } + + if (IS_NOT_TYPE(value, QMetaType::Double)) { + EXIT_WITH(EC_WRONG_SETTINGS_TYPE); + } + + *outValue = value.toDouble(); + + EXIT_WITH(STATUS_OK); +} + +void MumbleAPI::getMumbleSetting_string_v_1_0_x(mumble_plugin_id_t callerID, mumble_settings_key_t key, + const char **outValue, api_promise_t *promise) { + if (QThread::currentThread() != thread()) { + // Invoke in main thread + QMetaObject::invokeMethod(this, "getMumbleSetting_string_v_1_0_x", Qt::QueuedConnection, + Q_ARG(mumble_plugin_id_t, callerID), Q_ARG(mumble_settings_key_t, key), + Q_ARG(const char **, outValue), Q_ARG(api_promise_t *, promise)); + + return; + } + + VERIFY_PLUGIN_ID(callerID); + + QVariant value = getMumbleSettingHelper(key); + + if (!value.isValid()) { + // We also return that for MSK_INVALID + EXIT_WITH(EC_UNKNOWN_SETTINGS_KEY); + } + + if (IS_NOT_TYPE(value, QMetaType::QString)) { + EXIT_WITH(EC_WRONG_SETTINGS_TYPE); + } + + const QString stringValue = value.toString(); + + // +1 for NULL terminator + size_t size = stringValue.toUtf8().size() + 1; + + char *valueArray = reinterpret_cast< char * >(malloc(size * sizeof(char))); + + std::strcpy(valueArray, stringValue.toUtf8().data()); + + m_curator.m_entries.insert({ valueArray, { defaultDeleter, callerID, "getMumbleSetting_string" } }); + + *outValue = valueArray; + + EXIT_WITH(STATUS_OK); +} + +mumble_error_t setMumbleSettingHelper(mumble_settings_key_t key, QVariant value) { + switch (key) { + case MSK_AUDIO_INPUT_VOICE_HOLD: + if (IS_TYPE(value, QMetaType::Int)) { + Global::get().s.iVoiceHold = value.toInt(); + + return STATUS_OK; + } else { + return EC_WRONG_SETTINGS_TYPE; + } + case MSK_AUDIO_INPUT_VAD_SILENCE_THRESHOLD: + if (IS_TYPE(value, QMetaType::Double)) { + Global::get().s.fVADmin = static_cast< float >(value.toDouble()); + + return STATUS_OK; + } else { + return EC_WRONG_SETTINGS_TYPE; + } + case MSK_AUDIO_INPUT_VAD_SPEECH_THRESHOLD: + if (IS_TYPE(value, QMetaType::Double)) { + Global::get().s.fVADmax = static_cast< float >(value.toDouble()); + + return STATUS_OK; + } else { + return EC_WRONG_SETTINGS_TYPE; + } + case MSK_AUDIO_OUTPUT_PA_MINIMUM_DISTANCE: + if (IS_TYPE(value, QMetaType::Double)) { + Global::get().s.fAudioMinDistance = static_cast< float >(value.toDouble()); + + return STATUS_OK; + } else { + return EC_WRONG_SETTINGS_TYPE; + } + case MSK_AUDIO_OUTPUT_PA_MAXIMUM_DISTANCE: + if (IS_TYPE(value, QMetaType::Double)) { + Global::get().s.fAudioMaxDistance = static_cast< float >(value.toDouble()); + + return STATUS_OK; + } else { + return EC_WRONG_SETTINGS_TYPE; + } + case MSK_AUDIO_OUTPUT_PA_BLOOM: + if (IS_TYPE(value, QMetaType::Double)) { + Global::get().s.fAudioBloom = static_cast< float >(value.toDouble()); + + return STATUS_OK; + } else { + return EC_WRONG_SETTINGS_TYPE; + } + case MSK_AUDIO_OUTPUT_PA_MINIMUM_VOLUME: + if (IS_TYPE(value, QMetaType::Double)) { + Global::get().s.fAudioMaxDistVolume = static_cast< float >(value.toDouble()); + + return STATUS_OK; + } else { + return EC_WRONG_SETTINGS_TYPE; + } + case MSK_INVALID: + // Do nothing + break; + } + + return EC_UNKNOWN_SETTINGS_KEY; +} + +void MumbleAPI::setMumbleSetting_bool_v_1_0_x(mumble_plugin_id_t callerID, mumble_settings_key_t key, bool value, + api_promise_t *promise) { + if (QThread::currentThread() != thread()) { + // Invoke in main thread + QMetaObject::invokeMethod(this, "setMumbleSetting_bool_v_1_0_x", Qt::QueuedConnection, + Q_ARG(mumble_plugin_id_t, callerID), Q_ARG(mumble_settings_key_t, key), + Q_ARG(bool, value), Q_ARG(api_promise_t *, promise)); + + return; + } + + VERIFY_PLUGIN_ID(callerID); + + mumble_error_t exitCode = setMumbleSettingHelper(key, value); + EXIT_WITH(exitCode); +} + +void MumbleAPI::setMumbleSetting_int_v_1_0_x(mumble_plugin_id_t callerID, mumble_settings_key_t key, int64_t value, + api_promise_t *promise) { + if (QThread::currentThread() != thread()) { + // Invoke in main thread + QMetaObject::invokeMethod(this, "setMumbleSetting_int_v_1_0_x", Qt::QueuedConnection, + Q_ARG(mumble_plugin_id_t, callerID), Q_ARG(mumble_settings_key_t, key), + Q_ARG(int64_t, value), Q_ARG(api_promise_t *, promise)); + + return; + } + + VERIFY_PLUGIN_ID(callerID); + + mumble_error_t exitCode = setMumbleSettingHelper(key, QVariant::fromValue(value)); + EXIT_WITH(exitCode); +} + +void MumbleAPI::setMumbleSetting_double_v_1_0_x(mumble_plugin_id_t callerID, mumble_settings_key_t key, double value, + api_promise_t *promise) { + if (QThread::currentThread() != thread()) { + // Invoke in main thread + QMetaObject::invokeMethod(this, "setMumbleSetting_double_v_1_0_x", Qt::QueuedConnection, + Q_ARG(mumble_plugin_id_t, callerID), Q_ARG(mumble_settings_key_t, key), + Q_ARG(double, value), Q_ARG(api_promise_t *, promise)); + + return; + } + + VERIFY_PLUGIN_ID(callerID); + + mumble_error_t exitCode = setMumbleSettingHelper(key, value); + EXIT_WITH(exitCode); +} + +void MumbleAPI::setMumbleSetting_string_v_1_0_x(mumble_plugin_id_t callerID, mumble_settings_key_t key, + const char *value, api_promise_t *promise) { + if (QThread::currentThread() != thread()) { + // Invoke in main thread + QMetaObject::invokeMethod(this, "setMumbleSetting_string_v_1_0_x", Qt::QueuedConnection, + Q_ARG(mumble_plugin_id_t, callerID), Q_ARG(mumble_settings_key_t, key), + Q_ARG(const char *, value), Q_ARG(api_promise_t *, promise)); + + return; + } + + VERIFY_PLUGIN_ID(callerID); + + mumble_error_t exitCode = setMumbleSettingHelper(key, QString::fromUtf8(value)); + EXIT_WITH(exitCode); +} +#undef IS_TYPE +#undef IS_NOT_TYPE + +void MumbleAPI::sendData_v_1_0_x(mumble_plugin_id_t callerID, mumble_connection_t connection, + const mumble_userid_t *users, size_t userCount, const uint8_t *data, size_t dataLength, + const char *dataID, api_promise_t *promise) { + if (QThread::currentThread() != thread()) { + // Invoke in main thread + QMetaObject::invokeMethod(this, "sendData_v_1_0_x", Qt::QueuedConnection, Q_ARG(mumble_plugin_id_t, callerID), + Q_ARG(mumble_connection_t, connection), Q_ARG(const mumble_userid_t *, users), + Q_ARG(size_t, userCount), Q_ARG(const uint8_t *, data), Q_ARG(size_t, dataLength), + Q_ARG(const char *, dataID), Q_ARG(api_promise_t *, promise)); + + return; + } + + VERIFY_PLUGIN_ID(callerID); + + VERIFY_CONNECTION(connection); + ENSURE_CONNECTION_SYNCHRONIZED(connection); + + if (dataLength > Mumble::Plugins::PluginMessage::MAX_DATA_LENGTH) { + EXIT_WITH(EC_DATA_TOO_BIG); + } + if (std::strlen(dataID) > Mumble::Plugins::PluginMessage::MAX_DATA_ID_LENGTH) { + EXIT_WITH(EC_DATA_ID_TOO_LONG); + } + + MumbleProto::PluginDataTransmission mpdt; + mpdt.set_sendersession(Global::get().uiSession); + + for (size_t i = 0; i < userCount; i++) { + const ClientUser *user = ClientUser::get(users[i]); + + if (user) { + mpdt.add_receiversessions(users[i]); + } else { + EXIT_WITH(EC_USER_NOT_FOUND); + } + } + + mpdt.set_data(data, dataLength); + mpdt.set_dataid(dataID); + + if (Global::get().sh) { + Global::get().sh->sendMessage(mpdt); + + EXIT_WITH(STATUS_OK); + } else { + EXIT_WITH(EC_CONNECTION_NOT_FOUND); + } +} + +void MumbleAPI::log_v_1_0_x(mumble_plugin_id_t callerID, const char *message, api_promise_t *promise) { + if (QThread::currentThread() != thread()) { + // Invoke in main thread + QMetaObject::invokeMethod(this, "log_v_1_0_x", Qt::QueuedConnection, Q_ARG(mumble_plugin_id_t, callerID), + Q_ARG(const char *, message), Q_ARG(api_promise_t *, promise)); + + return; + } + + // We verify the plugin manually as we need a handle to it later + const_plugin_ptr_t plugin = Global::get().pluginManager->getPlugin(callerID); + if (!plugin) { + EXIT_WITH(EC_INVALID_PLUGIN_ID); + } + + QString msg = QString::fromLatin1("%1: %2") + .arg(plugin->getName().toHtmlEscaped()) + .arg(QString::fromUtf8(message).toHtmlEscaped()); + + // Use static method that handles the case in which the Log object doesn't exist yet + Log::logOrDefer(Log::PluginMessage, msg); + + EXIT_WITH(STATUS_OK); +} + +void MumbleAPI::playSample_v_1_0_x(mumble_plugin_id_t callerID, const char *samplePath, api_promise_t *promise) { + if (QThread::currentThread() != thread()) { + // Invoke in main thread + QMetaObject::invokeMethod(this, "playSample_v_1_0_x", Qt::QueuedConnection, Q_ARG(mumble_plugin_id_t, callerID), + Q_ARG(const char *, samplePath), Q_ARG(api_promise_t *, promise)); + + return; + } + + VERIFY_PLUGIN_ID(callerID); + + if (!Global::get().ao) { + EXIT_WITH(EC_AUDIO_NOT_AVAILABLE); + } + + if (Global::get().ao->playSample(QString::fromUtf8(samplePath), false)) { + EXIT_WITH(STATUS_OK); + } else { + EXIT_WITH(EC_INVALID_SAMPLE); + } +} + +///////////////////////////////////////////////////////////////////////////////////////// +/////////////////// C FUNCTION WRAPPERS FOR USE IN API STRUCT /////////////////////////// +///////////////////////////////////////////////////////////////////////////////////////// + +mumble_error_t PLUGIN_CALLING_CONVENTION freeMemory_v_1_0_x(mumble_plugin_id_t callerID, const void *ptr) { + api_promise_t promise; + api_future_t future = promise.get_future(); + + MumbleAPI::get().freeMemory_v_1_0_x(callerID, ptr, &promise); + + return future.get(); +} + +mumble_error_t PLUGIN_CALLING_CONVENTION getActiveServerConnection_v_1_0_x(mumble_plugin_id_t callerID, + mumble_connection_t *connection) { + api_promise_t promise; + api_future_t future = promise.get_future(); + + MumbleAPI::get().getActiveServerConnection_v_1_0_x(callerID, connection, &promise); + + return future.get(); +} + +mumble_error_t PLUGIN_CALLING_CONVENTION isConnectionSynchronized_v_1_0_x(mumble_plugin_id_t callerID, + mumble_connection_t connection, + bool *synchronized) { + api_promise_t promise; + api_future_t future = promise.get_future(); + + MumbleAPI::get().isConnectionSynchronized_v_1_0_x(callerID, connection, synchronized, &promise); + + return future.get(); +} + +mumble_error_t PLUGIN_CALLING_CONVENTION getLocalUserID_v_1_0_x(mumble_plugin_id_t callerID, + mumble_connection_t connection, + mumble_userid_t *userID) { + api_promise_t promise; + api_future_t future = promise.get_future(); + + MumbleAPI::get().getLocalUserID_v_1_0_x(callerID, connection, userID, &promise); + + return future.get(); +} + +mumble_error_t PLUGIN_CALLING_CONVENTION getUserName_v_1_0_x(mumble_plugin_id_t callerID, + mumble_connection_t connection, mumble_userid_t userID, + const char **name) { + api_promise_t promise; + api_future_t future = promise.get_future(); + + MumbleAPI::get().getUserName_v_1_0_x(callerID, connection, userID, name, &promise); + + return future.get(); +} + +mumble_error_t PLUGIN_CALLING_CONVENTION getChannelName_v_1_0_x(mumble_plugin_id_t callerID, + mumble_connection_t connection, + mumble_channelid_t channelID, const char **name) { + api_promise_t promise; + api_future_t future = promise.get_future(); + + MumbleAPI::get().getChannelName_v_1_0_x(callerID, connection, channelID, name, &promise); + + return future.get(); +} + +mumble_error_t PLUGIN_CALLING_CONVENTION getAllUsers_v_1_0_x(mumble_plugin_id_t callerID, + mumble_connection_t connection, mumble_userid_t **users, + size_t *userCount) { + api_promise_t promise; + api_future_t future = promise.get_future(); + + MumbleAPI::get().getAllUsers_v_1_0_x(callerID, connection, users, userCount, &promise); + + return future.get(); +} + +mumble_error_t PLUGIN_CALLING_CONVENTION getAllChannels_v_1_0_x(mumble_plugin_id_t callerID, + mumble_connection_t connection, + mumble_channelid_t **channels, size_t *channelCount) { + api_promise_t promise; + api_future_t future = promise.get_future(); + + MumbleAPI::get().getAllChannels_v_1_0_x(callerID, connection, channels, channelCount, &promise); + + return future.get(); +} + +mumble_error_t PLUGIN_CALLING_CONVENTION getChannelOfUser_v_1_0_x(mumble_plugin_id_t callerID, + mumble_connection_t connection, + mumble_userid_t userID, mumble_channelid_t *channel) { + api_promise_t promise; + api_future_t future = promise.get_future(); + + MumbleAPI::get().getChannelOfUser_v_1_0_x(callerID, connection, userID, channel, &promise); + + return future.get(); +} + +mumble_error_t PLUGIN_CALLING_CONVENTION getUsersInChannel_v_1_0_x(mumble_plugin_id_t callerID, + mumble_connection_t connection, + mumble_channelid_t channelID, + mumble_userid_t **userList, size_t *userCount) { + api_promise_t promise; + api_future_t future = promise.get_future(); + + MumbleAPI::get().getUsersInChannel_v_1_0_x(callerID, connection, channelID, userList, userCount, &promise); + + return future.get(); +} + + +mumble_error_t PLUGIN_CALLING_CONVENTION + getLocalUserTransmissionMode_v_1_0_x(mumble_plugin_id_t callerID, mumble_transmission_mode_t *transmissionMode) { + api_promise_t promise; + api_future_t future = promise.get_future(); + + MumbleAPI::get().getLocalUserTransmissionMode_v_1_0_x(callerID, transmissionMode, &promise); + + return future.get(); +} + +mumble_error_t PLUGIN_CALLING_CONVENTION isUserLocallyMuted_v_1_0_x(mumble_plugin_id_t callerID, + mumble_connection_t connection, + mumble_userid_t userID, bool *muted) { + api_promise_t promise; + api_future_t future = promise.get_future(); + + MumbleAPI::get().isUserLocallyMuted_v_1_0_x(callerID, connection, userID, muted, &promise); + + return future.get(); +} + +mumble_error_t PLUGIN_CALLING_CONVENTION isLocalUserMuted_v_1_0_x(mumble_plugin_id_t callerID, bool *muted) { + api_promise_t promise; + api_future_t future = promise.get_future(); + + MumbleAPI::get().isLocalUserMuted_v_1_0_x(callerID, muted, &promise); + + return future.get(); +} + +mumble_error_t PLUGIN_CALLING_CONVENTION isLocalUserDeafened_v_1_0_x(mumble_plugin_id_t callerID, bool *deafened) { + api_promise_t promise; + api_future_t future = promise.get_future(); + + MumbleAPI::get().isLocalUserDeafened_v_1_0_x(callerID, deafened, &promise); + + return future.get(); +} + +mumble_error_t PLUGIN_CALLING_CONVENTION getUserHash_v_1_0_x(mumble_plugin_id_t callerID, + mumble_connection_t connection, mumble_userid_t userID, + const char **hash) { + api_promise_t promise; + api_future_t future = promise.get_future(); + + MumbleAPI::get().getUserHash_v_1_0_x(callerID, connection, userID, hash, &promise); + + return future.get(); +} + +mumble_error_t PLUGIN_CALLING_CONVENTION getServerHash_v_1_0_x(mumble_plugin_id_t callerID, + mumble_connection_t connection, const char **hash) { + api_promise_t promise; + api_future_t future = promise.get_future(); + + MumbleAPI::get().getServerHash_v_1_0_x(callerID, connection, hash, &promise); + + return future.get(); +} + + +mumble_error_t PLUGIN_CALLING_CONVENTION + requestLocalUserTransmissionMode_v_1_0_x(mumble_plugin_id_t callerID, mumble_transmission_mode_t transmissionMode) { + api_promise_t promise; + api_future_t future = promise.get_future(); + + MumbleAPI::get().requestLocalUserTransmissionMode_v_1_0_x(callerID, transmissionMode, &promise); + + return future.get(); +} + +mumble_error_t PLUGIN_CALLING_CONVENTION getUserComment_v_1_0_x(mumble_plugin_id_t callerID, + mumble_connection_t connection, mumble_userid_t userID, + const char **comment) { + api_promise_t promise; + api_future_t future = promise.get_future(); + + MumbleAPI::get().getUserComment_v_1_0_x(callerID, connection, userID, comment, &promise); + + return future.get(); +} + +mumble_error_t PLUGIN_CALLING_CONVENTION getChannelDescription_v_1_0_x(mumble_plugin_id_t callerID, + mumble_connection_t connection, + mumble_channelid_t channelID, + const char **description) { + api_promise_t promise; + api_future_t future = promise.get_future(); + + MumbleAPI::get().getChannelDescription_v_1_0_x(callerID, connection, channelID, description, &promise); + + return future.get(); +} + +mumble_error_t PLUGIN_CALLING_CONVENTION requestUserMove_v_1_0_x(mumble_plugin_id_t callerID, + mumble_connection_t connection, mumble_userid_t userID, + mumble_channelid_t channelID, const char *password) { + api_promise_t promise; + api_future_t future = promise.get_future(); + + MumbleAPI::get().requestUserMove_v_1_0_x(callerID, connection, userID, channelID, password, &promise); + + return future.get(); +} + +mumble_error_t PLUGIN_CALLING_CONVENTION requestMicrophoneActivationOverwrite_v_1_0_x(mumble_plugin_id_t callerID, + bool activate) { + api_promise_t promise; + api_future_t future = promise.get_future(); + + MumbleAPI::get().requestMicrophoneActivationOverwrite_v_1_0_x(callerID, activate, &promise); + + return future.get(); +} + +mumble_error_t PLUGIN_CALLING_CONVENTION requestLocalMute_v_1_0_x(mumble_plugin_id_t callerID, + mumble_connection_t connection, + mumble_userid_t userID, bool muted) { + api_promise_t promise; + api_future_t future = promise.get_future(); + + MumbleAPI::get().requestLocalMute_v_1_0_x(callerID, connection, userID, muted, &promise); + + return future.get(); +} + +mumble_error_t PLUGIN_CALLING_CONVENTION requestLocalUserMute_v_1_0_x(mumble_plugin_id_t callerID, bool muted) { + api_promise_t promise; + api_future_t future = promise.get_future(); + + MumbleAPI::get().requestLocalUserMute_v_1_0_x(callerID, muted, &promise); + + return future.get(); +} + +mumble_error_t PLUGIN_CALLING_CONVENTION requestLocalUserDeaf_v_1_0_x(mumble_plugin_id_t callerID, bool deafened) { + api_promise_t promise; + api_future_t future = promise.get_future(); + + MumbleAPI::get().requestLocalUserDeaf_v_1_0_x(callerID, deafened, &promise); + + return future.get(); +} + +mumble_error_t PLUGIN_CALLING_CONVENTION requestSetLocalUserComment_v_1_0_x(mumble_plugin_id_t callerID, + mumble_connection_t connection, + const char *comment) { + api_promise_t promise; + api_future_t future = promise.get_future(); + + MumbleAPI::get().requestSetLocalUserComment_v_1_0_x(callerID, connection, comment, &promise); + + return future.get(); +} + +mumble_error_t PLUGIN_CALLING_CONVENTION findUserByName_v_1_0_x(mumble_plugin_id_t callerID, + mumble_connection_t connection, const char *userName, + mumble_userid_t *userID) { + api_promise_t promise; + api_future_t future = promise.get_future(); + + MumbleAPI::get().findUserByName_v_1_0_x(callerID, connection, userName, userID, &promise); + + return future.get(); +} + +mumble_error_t PLUGIN_CALLING_CONVENTION findChannelByName_v_1_0_x(mumble_plugin_id_t callerID, + mumble_connection_t connection, + const char *channelName, + mumble_channelid_t *channelID) { + api_promise_t promise; + api_future_t future = promise.get_future(); + + MumbleAPI::get().findChannelByName_v_1_0_x(callerID, connection, channelName, channelID, &promise); + + return future.get(); +} + +mumble_error_t PLUGIN_CALLING_CONVENTION getMumbleSetting_bool_v_1_0_x(mumble_plugin_id_t callerID, + mumble_settings_key_t key, bool *outValue) { + api_promise_t promise; + api_future_t future = promise.get_future(); + + MumbleAPI::get().getMumbleSetting_bool_v_1_0_x(callerID, key, outValue, &promise); + + return future.get(); +} + +mumble_error_t PLUGIN_CALLING_CONVENTION getMumbleSetting_int_v_1_0_x(mumble_plugin_id_t callerID, + mumble_settings_key_t key, int64_t *outValue) { + api_promise_t promise; + api_future_t future = promise.get_future(); + + MumbleAPI::get().getMumbleSetting_int_v_1_0_x(callerID, key, outValue, &promise); + + return future.get(); +} + +mumble_error_t PLUGIN_CALLING_CONVENTION getMumbleSetting_double_v_1_0_x(mumble_plugin_id_t callerID, + mumble_settings_key_t key, double *outValue) { + api_promise_t promise; + api_future_t future = promise.get_future(); + + MumbleAPI::get().getMumbleSetting_double_v_1_0_x(callerID, key, outValue, &promise); + + return future.get(); +} + +mumble_error_t PLUGIN_CALLING_CONVENTION getMumbleSetting_string_v_1_0_x(mumble_plugin_id_t callerID, + mumble_settings_key_t key, + const char **outValue) { + api_promise_t promise; + api_future_t future = promise.get_future(); + + MumbleAPI::get().getMumbleSetting_string_v_1_0_x(callerID, key, outValue, &promise); + + return future.get(); +} + +mumble_error_t PLUGIN_CALLING_CONVENTION setMumbleSetting_bool_v_1_0_x(mumble_plugin_id_t callerID, + mumble_settings_key_t key, bool value) { + api_promise_t promise; + api_future_t future = promise.get_future(); + + MumbleAPI::get().setMumbleSetting_bool_v_1_0_x(callerID, key, value, &promise); + + return future.get(); +} + +mumble_error_t PLUGIN_CALLING_CONVENTION setMumbleSetting_int_v_1_0_x(mumble_plugin_id_t callerID, + mumble_settings_key_t key, int64_t value) { + api_promise_t promise; + api_future_t future = promise.get_future(); + + MumbleAPI::get().setMumbleSetting_int_v_1_0_x(callerID, key, value, &promise); + + return future.get(); +} + +mumble_error_t PLUGIN_CALLING_CONVENTION setMumbleSetting_double_v_1_0_x(mumble_plugin_id_t callerID, + mumble_settings_key_t key, double value) { + api_promise_t promise; + api_future_t future = promise.get_future(); + + MumbleAPI::get().setMumbleSetting_double_v_1_0_x(callerID, key, value, &promise); + + return future.get(); +} + +mumble_error_t PLUGIN_CALLING_CONVENTION setMumbleSetting_string_v_1_0_x(mumble_plugin_id_t callerID, + mumble_settings_key_t key, const char *value) { + api_promise_t promise; + api_future_t future = promise.get_future(); + + MumbleAPI::get().setMumbleSetting_string_v_1_0_x(callerID, key, value, &promise); + + return future.get(); +} + +mumble_error_t PLUGIN_CALLING_CONVENTION sendData_v_1_0_x(mumble_plugin_id_t callerID, mumble_connection_t connection, + const mumble_userid_t *users, size_t userCount, + const uint8_t *data, size_t dataLength, const char *dataID) { + api_promise_t promise; + api_future_t future = promise.get_future(); + + MumbleAPI::get().sendData_v_1_0_x(callerID, connection, users, userCount, data, dataLength, dataID, &promise); + + return future.get(); +} + +mumble_error_t PLUGIN_CALLING_CONVENTION log_v_1_0_x(mumble_plugin_id_t callerID, const char *message) { + api_promise_t promise; + api_future_t future = promise.get_future(); + + MumbleAPI::get().log_v_1_0_x(callerID, message, &promise); + + return future.get(); +} + +mumble_error_t PLUGIN_CALLING_CONVENTION playSample_v_1_0_x(mumble_plugin_id_t callerID, const char *samplePath) { + api_promise_t promise; + api_future_t future = promise.get_future(); + + MumbleAPI::get().playSample_v_1_0_x(callerID, samplePath, &promise); + + return future.get(); +} + + +///////////////////////////////////////////////////////////////////////////////////////// +//////////////////////////// GETTER FOR API STRUCTS ///////////////////////////////////// +///////////////////////////////////////////////////////////////////////////////////////// + +MumbleAPI_v_1_0_x getMumbleAPI_v_1_0_x() { + return { freeMemory_v_1_0_x, + getActiveServerConnection_v_1_0_x, + isConnectionSynchronized_v_1_0_x, + getLocalUserID_v_1_0_x, + getUserName_v_1_0_x, + getChannelName_v_1_0_x, + getAllUsers_v_1_0_x, + getAllChannels_v_1_0_x, + getChannelOfUser_v_1_0_x, + getUsersInChannel_v_1_0_x, + getLocalUserTransmissionMode_v_1_0_x, + isUserLocallyMuted_v_1_0_x, + isLocalUserMuted_v_1_0_x, + isLocalUserDeafened_v_1_0_x, + getUserHash_v_1_0_x, + getServerHash_v_1_0_x, + getUserComment_v_1_0_x, + getChannelDescription_v_1_0_x, + requestLocalUserTransmissionMode_v_1_0_x, + requestUserMove_v_1_0_x, + requestMicrophoneActivationOverwrite_v_1_0_x, + requestLocalMute_v_1_0_x, + requestLocalUserMute_v_1_0_x, + requestLocalUserDeaf_v_1_0_x, + requestSetLocalUserComment_v_1_0_x, + findUserByName_v_1_0_x, + findChannelByName_v_1_0_x, + getMumbleSetting_bool_v_1_0_x, + getMumbleSetting_int_v_1_0_x, + getMumbleSetting_double_v_1_0_x, + getMumbleSetting_string_v_1_0_x, + setMumbleSetting_bool_v_1_0_x, + setMumbleSetting_int_v_1_0_x, + setMumbleSetting_double_v_1_0_x, + setMumbleSetting_string_v_1_0_x, + sendData_v_1_0_x, + log_v_1_0_x, + playSample_v_1_0_x }; +} + +#define MAP(qtName, apiName) \ + case Qt::Key_##qtName: \ + return KC_##apiName + +mumble_keycode_t qtKeyCodeToAPIKeyCode(unsigned int keyCode) { + switch (keyCode) { + MAP(Escape, ESCAPE); + MAP(Tab, TAB); + MAP(Backspace, BACKSPACE); + case Qt::Key_Return: + // Fallthrough + case Qt::Key_Enter: + return KC_ENTER; + MAP(Delete, DELETE); + MAP(Print, PRINT); + MAP(Home, HOME); + MAP(End, END); + MAP(Up, UP); + MAP(Down, DOWN); + MAP(Left, LEFT); + MAP(Right, RIGHT); + MAP(PageUp, PAGE_UP); + MAP(PageDown, PAGE_DOWN); + MAP(Shift, SHIFT); + MAP(Control, CONTROL); + MAP(Meta, META); + MAP(Alt, ALT); + MAP(AltGr, ALT_GR); + MAP(CapsLock, CAPSLOCK); + MAP(NumLock, NUMLOCK); + MAP(ScrollLock, SCROLLLOCK); + MAP(F1, F1); + MAP(F2, F2); + MAP(F3, F3); + MAP(F4, F4); + MAP(F5, F5); + MAP(F6, F6); + MAP(F7, F7); + MAP(F8, F8); + MAP(F9, F9); + MAP(F10, F10); + MAP(F11, F11); + MAP(F12, F12); + MAP(F13, F13); + MAP(F14, F14); + MAP(F15, F15); + MAP(F16, F16); + MAP(F17, F17); + MAP(F18, F18); + MAP(F19, F19); + case Qt::Key_Super_L: + // Fallthrough + case Qt::Key_Super_R: + return KC_SUPER; + MAP(Space, SPACE); + MAP(Exclam, EXCLAMATION_MARK); + MAP(QuoteDbl, DOUBLE_QUOTE); + MAP(NumberSign, HASHTAG); + MAP(Dollar, DOLLAR); + MAP(Percent, PERCENT); + MAP(Ampersand, AMPERSAND); + MAP(Apostrophe, SINGLE_QUOTE); + MAP(ParenLeft, OPEN_PARENTHESIS); + MAP(ParenRight, CLOSE_PARENTHESIS); + MAP(Asterisk, ASTERISK); + MAP(Plus, PLUS); + MAP(Comma, COMMA); + MAP(Minus, MINUS); + MAP(Period, PERIOD); + MAP(Slash, SLASH); + MAP(0, 0); + MAP(1, 1); + MAP(2, 2); + MAP(3, 3); + MAP(4, 4); + MAP(5, 5); + MAP(6, 6); + MAP(7, 7); + MAP(8, 8); + MAP(9, 9); + MAP(Colon, COLON); + MAP(Semicolon, SEMICOLON); + MAP(Less, LESS_THAN); + MAP(Equal, EQUALS); + MAP(Greater, GREATER_THAN); + MAP(Question, QUESTION_MARK); + MAP(At, AT_SYMBOL); + MAP(A, A); + MAP(B, B); + MAP(C, C); + MAP(D, D); + MAP(E, E); + MAP(F, F); + MAP(G, G); + MAP(H, H); + MAP(I, I); + MAP(J, J); + MAP(K, K); + MAP(L, L); + MAP(M, M); + MAP(N, N); + MAP(O, O); + MAP(P, P); + MAP(Q, Q); + MAP(R, R); + MAP(S, S); + MAP(T, T); + MAP(U, U); + MAP(V, V); + MAP(W, W); + MAP(X, X); + MAP(Y, Y); + MAP(Z, Z); + MAP(BracketLeft, OPEN_BRACKET); + MAP(BracketRight, CLOSE_BRACKET); + MAP(Backslash, BACKSLASH); + MAP(AsciiCircum, CIRCUMFLEX); + MAP(Underscore, UNDERSCORE); + MAP(BraceLeft, OPEN_BRACE); + MAP(BraceRight, CLOSE_BRACE); + MAP(Bar, VERTICAL_BAR); + MAP(AsciiTilde, TILDE); + MAP(degree, DEGREE_SIGN); + } + + return KC_INVALID; +} + +#undef MAP + + +// Implementation of PluginData +PluginData::PluginData() : overwriteMicrophoneActivation(false) { +} + +PluginData::~PluginData() { +} + +PluginData &PluginData::get() { + static PluginData *instance = new PluginData(); + + return *instance; +} +}; // namespace API + +#undef EXIT_WITH +#undef VERIFY_PLUGIN_ID +#undef VERIFY_CONNECTION +#undef ENSURE_CONNECTION_SYNCHRONIZED +#undef UNUSED diff --git a/src/mumble/Audio.cpp b/src/mumble/Audio.cpp index 9d383ffe798..b2d61025d8e 100644 --- a/src/mumble/Audio.cpp +++ b/src/mumble/Audio.cpp @@ -13,6 +13,7 @@ #endif #include "Log.h" #include "PacketDataStream.h" +#include "PluginManager.h" #include "Global.h" #include @@ -269,6 +270,15 @@ void Audio::stopInput() { void Audio::start(const QString &input, const QString &output) { startInput(input); startOutput(output); + + // Now that the audio input and output is created, we connect them to the PluginManager + // As these callbacks might want to change the audio before it gets further processed, all these connections have to be direct + QObject::connect(Global::get().ai.get(), &AudioInput::audioInputEncountered, Global::get().pluginManager, + &PluginManager::on_audioInput, Qt::DirectConnection); + QObject::connect(Global::get().ao.get(), &AudioOutput::audioSourceFetched, Global::get().pluginManager, + &PluginManager::on_audioSourceFetched, Qt::DirectConnection); + QObject::connect(Global::get().ao.get(), &AudioOutput::audioOutputAboutToPlay, Global::get().pluginManager, + &PluginManager::on_audioOutputAboutToPlay, Qt::DirectConnection); } void Audio::stop() { diff --git a/src/mumble/AudioInput.cpp b/src/mumble/AudioInput.cpp index d898ca0ccdb..cf1ca382068 100644 --- a/src/mumble/AudioInput.cpp +++ b/src/mumble/AudioInput.cpp @@ -11,14 +11,18 @@ # include "OpusCodec.h" #endif #include "MainWindow.h" +#include "User.h" +#include "PacketDataStream.h" +#include "PluginManager.h" #include "Message.h" #include "NetworkConfig.h" #include "PacketDataStream.h" -#include "Plugins.h" #include "ServerHandler.h" #include "User.h" #include "Utils.h" #include "VoiceRecorder.h" +#include "API.h" + #include "Global.h" #ifdef USE_RNNOISE @@ -1058,7 +1062,7 @@ void AudioInput::encodeAudioFrame(AudioChunk chunk) { iHoldFrames = 0; } - if (Global::get().s.atTransmit == Settings::Continuous) { + if (Global::get().s.atTransmit == Settings::Continuous || API::PluginData::get().overwriteMicrophoneActivation.load()) { // Continous transmission is enabled bIsSpeech = true; } else if (Global::get().s.atTransmit == Settings::PushToTalk) { @@ -1143,6 +1147,8 @@ void AudioInput::encodeAudioFrame(AudioChunk chunk) { EncodingOutputBuffer buffer; Q_ASSERT(buffer.size() >= static_cast< size_t >(iAudioQuality / 100 * iAudioFrames / 8)); + emit audioInputEncountered(psSource, iFrameSize, iMicChannels, SAMPLE_RATE, bIsSpeech); + int len = 0; bool encoded = true; @@ -1274,10 +1280,13 @@ void AudioInput::flushCheck(const QByteArray &frame, bool terminator, int voiceT } } - if (Global::get().s.bTransmitPosition && Global::get().p && !Global::get().bCenterPosition && Global::get().p->fetch()) { - pds << Global::get().p->fPosition[0]; - pds << Global::get().p->fPosition[1]; - pds << Global::get().p->fPosition[2]; + if (Global::get().s.bTransmitPosition && Global::get().pluginManager && !Global::get().bCenterPosition + && Global::get().pluginManager->fetchPositionalData()) { + Position3D currentPos = Global::get().pluginManager->getPositionalData().getPlayerPos(); + + pds << currentPos.x; + pds << currentPos.y; + pds << currentPos.z; } sendAudioFrame(data, pds); diff --git a/src/mumble/AudioInput.h b/src/mumble/AudioInput.h index b9dbb0c4eea..0410db4213e 100644 --- a/src/mumble/AudioInput.h +++ b/src/mumble/AudioInput.h @@ -254,6 +254,14 @@ class AudioInput : public QThread { signals: void doDeaf(); void doMute(); + /// A signal emitted if audio input is being encountered + /// + /// @param inputPCM The encountered input PCM + /// @param sampleCount The amount of samples in the input + /// @param channelCount The amount of channels in the input + /// @param sampleRate The used sample rate in Hz + /// @param isSpeech Whether Mumble considers the inpu to be speech + void audioInputEncountered(short *inputPCM, unsigned int sampleCount, unsigned int channelCount, unsigned int sampleRate, bool isSpeech); public: typedef enum { ActivityStateIdle, ActivityStateReturnedFromIdle, ActivityStateActive } ActivityState; diff --git a/src/mumble/AudioOutput.cpp b/src/mumble/AudioOutput.cpp index 15a01600efe..784a2924565 100644 --- a/src/mumble/AudioOutput.cpp +++ b/src/mumble/AudioOutput.cpp @@ -12,7 +12,7 @@ #include "ChannelListener.h" #include "Message.h" #include "PacketDataStream.h" -#include "Plugins.h" +#include "PluginManager.h" #include "ServerHandler.h" #include "SpeechFlags.h" #include "Timer.h" @@ -395,20 +395,19 @@ bool AudioOutput::mix(void *outbuff, unsigned int frameCount) { prioritySpeakerActive = true; } - if (!qlMix.isEmpty()) { + // If the audio backend uses a float-array we can sample and mix the audio sources directly into the output. Otherwise we'll have to + // use an intermediate buffer which we will convert to an array of shorts later + STACKVAR(float, fOutput, iChannels * frameCount); + float *output = (eSampleFormat == SampleFloat) ? reinterpret_cast(outbuff) : fOutput; + memset(output, 0, sizeof(float) * frameCount * iChannels); + + if (! qlMix.isEmpty()) { // There are audio sources available -> mix those sources together and feed them into the audio backend STACKVAR(float, speaker, iChannels * 3); STACKVAR(float, svol, iChannels); - STACKVAR(float, fOutput, iChannels *frameCount); - - // If the audio backend uses a float-array we can sample and mix the audio sources directly into the output. - // Otherwise we'll have to use an intermediate buffer which we will convert to an array of shorts later - float *output = (eSampleFormat == SampleFloat) ? reinterpret_cast< float * >(outbuff) : fOutput; bool validListener = false; - memset(output, 0, sizeof(float) * frameCount * iChannels); - // Initialize recorder if recording is enabled boost::shared_array< float > recbuff; if (recorder) { @@ -420,75 +419,56 @@ bool AudioOutput::mix(void *outbuff, unsigned int frameCount) { for (unsigned int i = 0; i < iChannels; ++i) svol[i] = mul * fSpeakerVolume[i]; - if (Global::get().s.bPositionalAudio && (iChannels > 1) && Global::get().p->fetch() - && (Global::get().bPosTest || Global::get().p->fCameraPosition[0] != 0 || Global::get().p->fCameraPosition[1] != 0 - || Global::get().p->fCameraPosition[2] != 0)) { + if (Global::get().s.bPositionalAudio && (iChannels > 1) && Global::get().pluginManager->fetchPositionalData()) { // Calculate the positional audio effects if it is enabled - float front[3] = { Global::get().p->fCameraFront[0], Global::get().p->fCameraFront[1], Global::get().p->fCameraFront[2] }; - float top[3] = { Global::get().p->fCameraTop[0], Global::get().p->fCameraTop[1], Global::get().p->fCameraTop[2] }; - - // Front vector is dominant; if it's zero we presume all is zero. + Vector3D cameraDir = Global::get().pluginManager->getPositionalData().getCameraDir(); - float flen = sqrtf(front[0] * front[0] + front[1] * front[1] + front[2] * front[2]); + Vector3D cameraAxis = Global::get().pluginManager->getPositionalData().getCameraAxis(); - if (flen > 0.0f) { - front[0] *= (1.0f / flen); - front[1] *= (1.0f / flen); - front[2] *= (1.0f / flen); + // Direction vector is dominant; if it's zero we presume all is zero. - float tlen = sqrtf(top[0] * top[0] + top[1] * top[1] + top[2] * top[2]); + if (!cameraDir.isZero()) { + cameraDir.normalize(); - if (tlen > 0.0f) { - top[0] *= (1.0f / tlen); - top[1] *= (1.0f / tlen); - top[2] *= (1.0f / tlen); + if (!cameraAxis.isZero()) { + cameraAxis.normalize(); } else { - top[0] = 0.0f; - top[1] = 1.0f; - top[2] = 0.0f; + cameraAxis = { 0.0f, 1.0f, 0.0f }; } - const float dotproduct = front[0] * top[0] + front[1] * top[1] + front[2] * top[2]; + const float dotproduct = cameraDir.dotProduct(cameraAxis); const float error = std::abs(dotproduct); if (error > 0.5f) { // Not perpendicular by a large margin. Assume Y up and rotate 90 degrees. float azimuth = 0.0f; - if ((front[0] != 0.0f) || (front[2] != 0.0f)) - azimuth = atan2f(front[2], front[0]); - float inclination = acosf(front[1]) - static_cast< float >(M_PI) / 2.0f; + if (cameraDir.x != 0.0f || cameraDir.z != 0.0f) { + azimuth = atan2f(cameraDir.z, cameraDir.x); + } + + float inclination = acosf(cameraDir.y) - static_cast< float >(M_PI) / 2.0f; - top[0] = sinf(inclination) * cosf(azimuth); - top[1] = cosf(inclination); - top[2] = sinf(inclination) * sinf(azimuth); + cameraAxis.x = sinf(inclination) * cosf(azimuth); + cameraAxis.y = cosf(inclination); + cameraAxis.z = sinf(inclination) * sinf(azimuth); } else if (error > 0.01f) { // Not perpendicular by a small margin. Find the nearest perpendicular vector. + cameraAxis = cameraAxis - cameraDir * dotproduct; - top[0] -= front[0] * dotproduct; - top[1] -= front[1] * dotproduct; - top[2] -= front[2] * dotproduct; - - // normalize top again - tlen = sqrtf(top[0] * top[0] + top[1] * top[1] + top[2] * top[2]); - // tlen is guaranteed to be non-zero, otherwise error would have been larger than 0.5 - top[0] *= (1.0f / tlen); - top[1] *= (1.0f / tlen); - top[2] *= (1.0f / tlen); + // normalize axis again (the orthogonalized vector us guaranteed to be non-zero + // as the error (dotproduct) was only 0.5 (and not 1 in which case above operation + // would create the zero-vector). + cameraAxis.normalize(); } } else { - front[0] = 0.0f; - front[1] = 0.0f; - front[2] = 1.0f; + cameraDir = { 0.0f, 0.0f, 1.0f }; - top[0] = 0.0f; - top[1] = 1.0f; - top[2] = 0.0f; + cameraAxis = { 0.0f, 1.0f, 0.0f }; } // Calculate right vector as front X top - float right[3] = { top[1] * front[2] - top[2] * front[1], top[2] * front[0] - top[0] * front[2], - top[0] * front[1] - top[1] * front[0] }; + Vector3D right = cameraAxis.crossProduct(cameraDir); /* qWarning("Front: %f %f %f", front[0], front[1], front[2]); @@ -497,26 +477,27 @@ bool AudioOutput::mix(void *outbuff, unsigned int frameCount) { */ // Rotate speakers to match orientation for (unsigned int i = 0; i < iChannels; ++i) { - speaker[3 * i + 0] = - fSpeakers[3 * i + 0] * right[0] + fSpeakers[3 * i + 1] * top[0] + fSpeakers[3 * i + 2] * front[0]; - speaker[3 * i + 1] = - fSpeakers[3 * i + 0] * right[1] + fSpeakers[3 * i + 1] * top[1] + fSpeakers[3 * i + 2] * front[1]; - speaker[3 * i + 2] = - fSpeakers[3 * i + 0] * right[2] + fSpeakers[3 * i + 1] * top[2] + fSpeakers[3 * i + 2] * front[2]; + speaker[3 * i + 0] = fSpeakers[3 * i + 0] * right.x + fSpeakers[3 * i + 1] * cameraAxis.x + + fSpeakers[3 * i + 2] * cameraDir.x; + speaker[3 * i + 1] = fSpeakers[3 * i + 0] * right.y + fSpeakers[3 * i + 1] * cameraAxis.y + + fSpeakers[3 * i + 2] * cameraDir.y; + speaker[3 * i + 2] = fSpeakers[3 * i + 0] * right.z + fSpeakers[3 * i + 1] * cameraAxis.z + + fSpeakers[3 * i + 2] * cameraDir.z; } validListener = true; } foreach (AudioOutputUser *aop, qlMix) { // Iterate through all audio sources and mix them together into the output (or the intermediate array) - const float *RESTRICT pfBuffer = aop->pfBuffer; - float volumeAdjustment = 1; + float *RESTRICT pfBuffer = aop->pfBuffer; + float volumeAdjustment = 1; // Check if the audio source is a user speaking (instead of a sample playback) and apply potential volume // adjustments AudioOutputSpeech *speech = qobject_cast< AudioOutputSpeech * >(aop); + const ClientUser *user = nullptr; if (speech) { - const ClientUser *user = speech->p; + user = speech->p; volumeAdjustment *= user->getLocalVolumeAdjustments(); if (user->cChannel && ChannelListener::isListening(Global::get().uiSession, user->cChannel->iId) @@ -534,6 +515,11 @@ bool AudioOutput::mix(void *outbuff, unsigned int frameCount) { } } + // As the events may cause the output PCM to change, the connection has to be direct in any case + const int channels = (speech && speech->bStereo) ? 2 : 1; + // If user != nullptr, then the current audio is considered speech + emit audioSourceFetched(pfBuffer, frameCount, channels, SAMPLE_RATE, static_cast< bool >(user), user); + // If recording is enabled add the current audio source to the recording buffer if (recorder) { if (speech) { @@ -574,27 +560,33 @@ bool AudioOutput::mix(void *outbuff, unsigned int frameCount) { #endif // If positional audio is enabled, calculate the respective audio effect here - float dir[3] = { aop->fPos[0] - Global::get().p->fCameraPosition[0], aop->fPos[1] - Global::get().p->fCameraPosition[1], - aop->fPos[2] - Global::get().p->fCameraPosition[2] }; - float len = sqrtf(dir[0] * dir[0] + dir[1] * dir[1] + dir[2] * dir[2]); + Position3D outputPos = { aop->fPos[0], aop->fPos[1], aop->fPos[2] }; + Position3D ownPos = Global::get().pluginManager->getPositionalData().getCameraPos(); + + Vector3D connectionVec = outputPos - ownPos; + float len = connectionVec.norm(); + if (len > 0.0f) { - dir[0] /= len; - dir[1] /= len; - dir[2] /= len; + // Don't use normalize-func in order to save the re-computation of the vector's length + connectionVec.x /= len; + connectionVec.y /= len; + connectionVec.z /= len; } /* qWarning("Voice pos: %f %f %f", aop->fPos[0], aop->fPos[1], aop->fPos[2]); - qWarning("Voice dir: %f %f %f", dir[0], dir[1], dir[2]); + qWarning("Voice dir: %f %f %f", connectionVec.x, connectionVec.y, connectionVec.z); */ if (!aop->pfVolume) { aop->pfVolume = new float[nchan]; for (unsigned int s = 0; s < nchan; ++s) aop->pfVolume[s] = -1.0; } + for (unsigned int s = 0; s < nchan; ++s) { - const float dot = bSpeakerPositional[s] ? dir[0] * speaker[s * 3 + 0] + dir[1] * speaker[s * 3 + 1] - + dir[2] * speaker[s * 3 + 2] - : 1.0f; + const float dot = bSpeakerPositional[s] + ? connectionVec.x * speaker[s * 3 + 0] + connectionVec.y * speaker[s * 3 + 1] + + connectionVec.z * speaker[s * 3 + 2] + : 1.0f; const float str = svol[s] * calcGain(dot, len) * volumeAdjustment; float *RESTRICT o = output + s; const float old = (aop->pfVolume[s] >= 0.0f) ? aop->pfVolume[s] : str; @@ -642,7 +634,12 @@ bool AudioOutput::mix(void *outbuff, unsigned int frameCount) { if (recorder && recorder->isInMixDownMode()) { recorder->addBuffer(nullptr, recbuff, frameCount); } + } + + bool pluginModifiedAudio = false; + emit audioOutputAboutToPlay(output, frameCount, nchan, SAMPLE_RATE, &pluginModifiedAudio); + if (pluginModifiedAudio || (! qlMix.isEmpty())) { // Clip the output audio if (eSampleFormat == SampleFloat) for (unsigned int i = 0; i < frameCount * iChannels; i++) @@ -665,7 +662,7 @@ bool AudioOutput::mix(void *outbuff, unsigned int frameCount) { #endif // Return whether data has been written to the outbuff - return (!qlMix.isEmpty()); + return (pluginModifiedAudio || (! qlMix.isEmpty())); } bool AudioOutput::isAlive() const { diff --git a/src/mumble/AudioOutput.h b/src/mumble/AudioOutput.h index 299736b9d38..3d5c5b6da08 100644 --- a/src/mumble/AudioOutput.h +++ b/src/mumble/AudioOutput.h @@ -127,6 +127,25 @@ class AudioOutput : public QThread { static float calcGain(float dotproduct, float distance); unsigned int getMixerFreq() const; void setBufferSize(unsigned int bufferSize); + +signals: + /// Signal emitted whenever an audio source has been fetched + /// + /// @param outputPCM The fetched output PCM + /// @param sampleCount The amount of samples in the output + /// @param channelCount The amount of channels in the output + /// @param sampleRate The used sample rate in Hz + /// @param isSpeech Whether the fetched output is considered to be speech + /// @param A pointer to the user that this speech belongs to or nullptr if this isn't speech + void audioSourceFetched(float *outputPCM, unsigned int sampleCount, unsigned int channelCount, unsigned int sampleRate, bool isSpeech, const ClientUser *user); + /// Signal emitted whenever an audio is about to be played to the user + /// + /// @param outputPCM The output PCM that is to be played + /// @param sampleCount The amount of samples in the output + /// @param channelCount The amount of channels in the output + /// @param sampleRate The used sample rate in Hz + /// @param modifiedAudio Pointer to bool if audio has been modified or not and should be played + void audioOutputAboutToPlay(float *outputPCM, unsigned int sampleCount, unsigned int channelCount, unsigned int sampleRate, bool *modifiedAudio); }; #endif diff --git a/src/mumble/CMakeLists.txt b/src/mumble/CMakeLists.txt index d904be2b1f6..cf28c1dd582 100644 --- a/src/mumble/CMakeLists.txt +++ b/src/mumble/CMakeLists.txt @@ -35,6 +35,9 @@ option(qtspeech "Use Qt's text-to-speech system (part of the Qt Speech module) i option(jackaudio "Build support for JackAudio." ON) option(portaudio "Build support for PortAudio" ON) +option(plugin-debug "Build Mumble with debug output for plugin developers." OFF) +option(plugin-callback-debug "Build Mumble with debug output for plugin callbacks inside of Mumble." OFF) + if(WIN32) option(asio "Build support for ASIO audio input." OFF) option(wasapi "Build support for WASAPI." ON) @@ -81,6 +84,8 @@ set(MUMBLE_SOURCES "ACLEditor.cpp" "ACLEditor.h" "ACLEditor.ui" + "API_v_1_0_x.cpp" + "API.h" "ApplicationPalette.h" "AudioConfigDialog.cpp" "AudioConfigDialog.h" @@ -142,6 +147,8 @@ set(MUMBLE_SOURCES "LCD.cpp" "LCD.h" "LCD.ui" + "LegacyPlugin.cpp" + "LegacyPlugin.h" "ListenerLocalVolumeDialog.cpp" "Log.cpp" "Log.h" @@ -162,9 +169,21 @@ set(MUMBLE_SOURCES "NetworkConfig.ui" "OpusCodec.cpp" "OpusCodec.h" - "Plugins.cpp" - "Plugins.h" - "Plugins.ui" + "PluginConfig.cpp" + "PluginConfig.h" + "PluginConfig.ui" + "Plugin.cpp" + "Plugin.h" + "PluginInstaller.cpp" + "PluginInstaller.h" + "PluginInstaller.ui" + "PluginManager.cpp" + "PluginManager.h" + "PluginUpdater.cpp" + "PluginUpdater.h" + "PluginUpdater.ui" + "PositionalData.cpp" + "PositionalData.h" "PTTButtonWidget.cpp" "PTTButtonWidget.h" "PTTButtonWidget.ui" @@ -347,8 +366,72 @@ target_include_directories(mumble "widgets" ${SHARED_SOURCE_DIR} "${3RDPARTY_DIR}/smallft" + "${PLUGINS_DIR}" ) +find_pkg(Poco COMPONENTS Zip) + +if(TARGET Poco::Zip) + target_link_libraries(mumble + PRIVATE + Poco::Zip + ) +else() + message(STATUS "Regular Poco search failed - looking for Poco include dir manually...") + + if(MINGW) + # These are the paths for our MXE environment + if(32_BIT) + set(POCO_INCLUDE_DIR_HINT "/usr/lib/mxe/usr/i686-w64-mingw32.static/include/") + else() + set(POCO_INCLUDE_DIR_HINT "/usr/lib/mxe/usr/x86_64-w64-mingw32.static/include/") + endif() + else() + set(POCO_INCLUDE_DIR_HINT "/usr/include") + endif() + + find_path(POCO_INCLUDE_DIR "Poco/Poco.h" HINTS ${POCO_INCLUDE_DIR_HINT}) + + if(POCO_INCLUDE_DIR) + message(STATUS "Found Poco include dir at \"${POCO_INCLUDE_DIR}\"") + else() + message(FATAL_ERROR "Unable to locate Poco include directory") + endif() + + find_library(POCO_LIB_FOUNDATION NAMES PocoFoundation PocoFoundationmd REQUIRED) + find_library(POCO_LIB_UTIL NAMES PocoUtil PocoUtilmd REQUIRED) + find_library(POCO_LIB_XML NAMES PocoXML PocoXMLmd REQUIRED) + find_library(POCO_LIB_ZIP NAMES PocoZip PocoZipmd REQUIRED) + + if(POCO_LIB_ZIP) + message(STATUS "Found Poco Zip library at \"${POCO_LIB_ZIP}\"") + else() + message(FATAL_ERROR "Unable to find Poco Zip library") + endif() + + + # Now use the found include dir and libraries by linking it to the target + target_include_directories(mumble + PRIVATE + ${POCO_INCLUDE_DIR} + ) + + target_link_libraries(mumble + PRIVATE + ${POCO_LIB_ZIP} + ${POCO_LIB_XML} + ${POCO_LIB_UTIL} + ${POCO_LIB_FOUNDATION} + ) + + if(static) + target_compile_definitions(mumble + PUBLIC + POCO_STATIC + ) + endif() +endif() + find_pkg("SndFile;LibSndFile;sndfile" REQUIRED) # Look for various targets as they are named differently on different platforms @@ -1020,3 +1103,21 @@ if(${CMAKE_VERSION} VERSION_GREATER_EQUAL "3.16.0") ) endif() endif() + +if(plugin-debug) + target_compile_definitions(mumble PRIVATE "MUMBLE_PLUGIN_DEBUG") +endif() + +if(plugin-callback-debug) + target_compile_definitions(mumble PRIVATE "MUMBLE_PLUGIN_CALLBACK_DEBUG") +endif() + +if(UNIX) + if(${CMAKE_SYSTEM_NAME} STREQUAL "FreeBSD") + # On FreeBSD we need the util library for src/ProcessResolver.cpp to work + target_link_libraries(mumble PRIVATE util) + elseif(${CMAKE_SYSTEM_NAME} MATCHES ".*BSD") + # On any other BSD we need the kvm library for src/ProcessResolver.cpp to work + target_link_libraries(mumble PRIVATE kvm) + endif() +endif() diff --git a/src/mumble/ClientUser.cpp b/src/mumble/ClientUser.cpp index 3e7c7152c7c..8a66cf49656 100644 --- a/src/mumble/ClientUser.cpp +++ b/src/mumble/ClientUser.cpp @@ -7,6 +7,7 @@ #include "AudioOutput.h" #include "Channel.h" +#include "PluginManager.h" #include "Global.h" QHash< unsigned int, ClientUser * > ClientUser::c_qmUsers; @@ -61,6 +62,9 @@ ClientUser *ClientUser::add(unsigned int uiSession, QObject *po) { ClientUser *p = new ClientUser(po); p->uiSession = uiSession; c_qmUsers[uiSession] = p; + + QObject::connect(p, &ClientUser::talkingStateChanged, Global::get().pluginManager, &PluginManager::on_userTalkingStateChanged); + return p; } diff --git a/src/mumble/Global.cpp b/src/mumble/Global.cpp index acbbec7e155..b8c001ad152 100644 --- a/src/mumble/Global.cpp +++ b/src/mumble/Global.cpp @@ -73,7 +73,7 @@ static void migrateDataDir() { Global::Global(const QString &qsConfigPath) { mw = 0; db = 0; - p = 0; + pluginManager = 0; nam = 0; c = 0; talkingUI = 0; diff --git a/src/mumble/Global.h b/src/mumble/Global.h index 5aee1abf588..d8748bb51d0 100644 --- a/src/mumble/Global.h +++ b/src/mumble/Global.h @@ -22,7 +22,7 @@ class AudioInput; class AudioOutput; class Database; class Log; -class Plugins; +class PluginManager; class QSettings; class Overlay; class LCD; @@ -53,7 +53,8 @@ struct Global Q_DECL_FINAL { */ Database *db; Log *l; - Plugins *p; + /// A pointer to the PluginManager that is used in this session + PluginManager *pluginManager; QSettings *qs; #ifdef USE_OVERLAY Overlay *o; diff --git a/src/mumble/LegacyPlugin.cpp b/src/mumble/LegacyPlugin.cpp new file mode 100644 index 00000000000..420f3cf73fe --- /dev/null +++ b/src/mumble/LegacyPlugin.cpp @@ -0,0 +1,267 @@ +// Copyright 2021 The Mumble Developers. All rights reserved. +// Use of this source code is governed by a BSD-style license +// that can be found in the LICENSE file at the root of the +// Mumble source tree or at . + +#include "LegacyPlugin.h" +#include "MumblePlugin_v_1_0_x.h" + +#include +#include +#include +#include +#include +#include + +#include + + +/// A regular expression used to extract the version from the legacy plugin's description +static const QRegularExpression versionRegEx(QString::fromLatin1("(?:v)?(?:ersion)?[ \\t]*(\\d+)\\.(\\d+)(?:\\.(\\d+))?"), QRegularExpression::CaseInsensitiveOption); + + +LegacyPlugin::LegacyPlugin(QString path, bool isBuiltIn, QObject *p) + : Plugin(path, isBuiltIn, p), + m_name(), + m_description(), + m_version(VERSION_UNKNOWN), + m_mumPlug(0), + m_mumPlug2(0), + m_mumPlugQt(0) { +} + +LegacyPlugin::~LegacyPlugin() { +} + +bool LegacyPlugin::doInitialize() { + if (Plugin::doInitialize()) { + // initialization seems to have succeeded so far + // This means that mumPlug is initialized + + m_name = QString::fromStdWString(m_mumPlug->shortname); + // Although the MumblePlugin struct has a member called "description", the actual description seems to + // always only be returned by the longdesc function (The description member is actually just the name with some version + // info) + m_description = QString::fromStdWString(m_mumPlug->longdesc()); + // The version field in the MumblePlugin2 struct is the positional-audio-plugin-API version and not the version + // of the plugin itself. This information is not provided for legacy plugins. + // Most of them however provide information about the version of the game they support. Thus we will try to parse the + // description and extract this version using it for the plugin's version as well. + // Some plugins have the version in the actual description field of the old API (see above comment why these aren't the same) + // so we will use a combination of both to search for the version. If multiple version(-like) strings are found, the last one + // will be used. + QString matchContent = m_description + QChar::Null + QString::fromStdWString(m_mumPlug->description); + QRegularExpressionMatchIterator matchIt = versionRegEx.globalMatch(matchContent); + + // Only consider the last match + QRegularExpressionMatch match; + while (matchIt.hasNext()) { + match = matchIt.next(); + } + + if (match.hasMatch()) { + // Store version + m_version = { match.captured(1).toInt(), match.captured(2).toInt(), match.captured(3).toInt() }; + } + + return true; + } else { + // initialization has failed + // pass on info about failed init + return false; + } +} + +void LegacyPlugin::resolveFunctionPointers() { + // We don't set any functions inside the apiFnc struct variable in order for the default + // implementations in the Plugin class to mimic empty default implementations for all functions + // not explicitly overwritten by this class + + if (isValid()) { + // The corresponding library was loaded -> try to locate all API functions of the legacy plugin's spec + // (for positional audio) and set defaults for the other ones in order to maintain compatibility with + // the new plugin system + + QWriteLocker lock(&m_pluginLock); + + mumblePluginFunc pluginFunc = reinterpret_cast(m_lib.resolve("getMumblePlugin")); + mumblePlugin2Func plugin2Func = reinterpret_cast(m_lib.resolve("getMumblePlugin2")); + mumblePluginQtFunc pluginQtFunc = reinterpret_cast(m_lib.resolve("getMumblePluginQt")); + + if (pluginFunc) { + m_mumPlug = pluginFunc(); + } + if (plugin2Func) { + m_mumPlug2 = plugin2Func(); + } + if (pluginQtFunc) { + m_mumPlugQt = pluginQtFunc(); + } + + // A legacy plugin is valid as long as there is a function to get the MumblePlugin struct from it + // and the plugin has been compiled by the same compiler as this client (determined by the plugin's + // "magic") and it isn't retracted + bool suitableMagic = m_mumPlug && m_mumPlug->magic == MUMBLE_PLUGIN_MAGIC; + bool retracted = m_mumPlug && m_mumPlug->shortname == L"Retracted"; + m_pluginIsValid = pluginFunc && suitableMagic && !retracted; + +#ifdef MUMBLE_PLUGIN_DEBUG + if (!m_pluginIsValid) { + if (!pluginFunc) { + qDebug("Plugin \"%s\" is missing the getMumblePlugin() function", qPrintable(m_pluginPath)); + } else if (!suitableMagic) { + qDebug("Plugin \"%s\" was compiled with a different compiler (magic differs)", qPrintable(m_pluginPath)); + } else { + qDebug("Plugin \"%s\" is retracted", qPrintable(m_pluginPath)); + } + } +#endif + } +} + +mumble_error_t LegacyPlugin::init() { + { + QWriteLocker lock(&m_pluginLock); + + m_pluginIsLoaded = true; + } + + // No-op as legacy plugins never have anything to initialize + // The only init function they care about is the one that inits positional audio + return STATUS_OK; +} + +QString LegacyPlugin::getName() const { + PluginReadLocker lock(&m_pluginLock); + + if (!m_name.isEmpty()) { + return m_name; + } else { + return QString::fromLatin1(""); + } +} + +QString LegacyPlugin::getDescription() const { + PluginReadLocker lock(&m_pluginLock); + + if (!m_description.isEmpty()) { + return m_description; + } else { + return QString::fromLatin1(""); + } +} + +bool LegacyPlugin::showAboutDialog(QWidget *parent) const { + if (m_mumPlugQt && m_mumPlugQt->about) { + m_mumPlugQt->about(parent); + + return true; + } + if (m_mumPlug->about) { + // the original implementation in Mumble would pass nullptr to the about-function in the mumPlug struct + // so we'll mimic that behaviour for compatibility + m_mumPlug->about(nullptr); + + return true; + } + + return false; +} + +bool LegacyPlugin::showConfigDialog(QWidget *parent) const { + if (m_mumPlugQt && m_mumPlugQt->config) { + m_mumPlugQt->config(parent); + + return true; + } + if (m_mumPlug->config) { + // the original implementation in Mumble would pass nullptr to the about-function in the mumPlug struct + // so we'll mimic that behaviour for compatibility + m_mumPlug->config(nullptr); + + return true; + } + + return false; +} + +uint8_t LegacyPlugin::initPositionalData(const char *const*programNames, const uint64_t *programPIDs, size_t programCount) { + int retCode; + + if (m_mumPlug2) { + // Create and populate a multimap holding the names and PIDs to pass to the tryLock-function + std::multimap pidMap; + + for (size_t i=0; i>().from_bytes(currentName); + + pidMap.insert(std::pair(currentNameWstr, programPIDs[i])); + } + + retCode = m_mumPlug2->trylock(pidMap); + } else { + // The default MumblePlugin doesn't take the name and PID arguments + retCode = m_mumPlug->trylock(); + } + + // ensure that only expected return codes are being returned from this function + // the legacy plugins return 1 on successfull locking and 0 on failure + if (retCode) { + QWriteLocker wLock(&m_pluginLock); + + m_positionalDataIsActive = true; + + return PDEC_OK; + } else { + // legacy plugins don't have the concept of indicating a permanent error + // so we'll return a temporary error for them + return PDEC_ERROR_TEMP; + } +} + +bool LegacyPlugin::fetchPositionalData(Position3D& avatarPos, Vector3D& avatarDir, Vector3D& avatarAxis, Position3D& cameraPos, Vector3D& cameraDir, + Vector3D& cameraAxis, QString& context, QString& identity) const { + std::wstring identityWstr; + std::string contextStr; + + int retCode = m_mumPlug->fetch(static_cast(avatarPos), static_cast(avatarDir), static_cast(avatarAxis), + static_cast(cameraPos), static_cast(cameraDir), static_cast(cameraAxis), contextStr, identityWstr); + + context = QString::fromStdString(contextStr); + identity = QString::fromStdWString(identityWstr); + + // The fetch-function should return if it is "still locked on" meaning that it can continue providing + // positional audio + return retCode == 1; +} + +void LegacyPlugin::shutdownPositionalData() { + QWriteLocker lock(&m_pluginLock); + + m_positionalDataIsActive = false; + + m_mumPlug->unlock(); +} + +uint32_t LegacyPlugin::getFeatures() const { + return FEATURE_POSITIONAL; +} + +mumble_version_t LegacyPlugin::getVersion() const { + return m_version; +} + +bool LegacyPlugin::providesAboutDialog() const { + return m_mumPlug->about || (m_mumPlugQt && m_mumPlugQt->about); +} + +bool LegacyPlugin::providesConfigDialog() const { + return m_mumPlug->config || (m_mumPlugQt && m_mumPlugQt->config); +} + +mumble_version_t LegacyPlugin::getAPIVersion() const { + // Legacy plugins are always on most recent API as they don't use it in any case -> no need to perform + // backwards compatibility stuff + return MUMBLE_PLUGIN_API_VERSION; +} diff --git a/src/mumble/LegacyPlugin.h b/src/mumble/LegacyPlugin.h new file mode 100644 index 00000000000..250b624a058 --- /dev/null +++ b/src/mumble/LegacyPlugin.h @@ -0,0 +1,82 @@ +// Copyright 2021 The Mumble Developers. All rights reserved. +// Use of this source code is governed by a BSD-style license +// that can be found in the LICENSE file at the root of the +// Mumble source tree or at . + +#ifndef MUMBLE_MUMBLE_LEGACY_PLUGIN_H_ +#define MUMBLE_MUMBLE_LEGACY_PLUGIN_H_ + +#include "Plugin.h" + +#include + +#include +#include + +#define MUMBLE_ALLOW_DEPRECATED_LEGACY_PLUGIN_API +#include "mumble_legacy_plugin.h" + +class LegacyPlugin; + +/// Typedef for a LegacyPlugin pointer +typedef std::shared_ptr legacy_plugin_ptr_t; +/// Typedef for a const LegacyPlugin pointer +typedef std::shared_ptr const_legacy_plugin_ptr_t; + + +/// This class is meant for compatibility for old Mumble "plugins" that stem from before the plugin framework has been +/// introduced. Thus the "plugins" represented by this class are for positional data gathering only. +class LegacyPlugin : public Plugin { + friend class Plugin; // needed in order for Plugin::createNew to access LegacyPlugin::doInitialize() + private: + Q_OBJECT + Q_DISABLE_COPY(LegacyPlugin) + + protected: + /// The name of the "plugin" + QString m_name; + /// The description of the "plugin" + QString m_description; + /// The Version of the "plugin" + mumble_version_t m_version; + /// A pointer to the PluginStruct in its initial version. After initialization this + /// field is effectively const and therefore it is not needed to protect read-access by a lock. + MumblePlugin *m_mumPlug; + /// A pointer to the PluginStruct in its second, enhanced version. After initialization this + /// field is effectively const and therefore it is not needed to protect read-access by a lock. + MumblePlugin2 *m_mumPlug2; + /// A pointer to the PluginStruct that encorporates Qt functionality. After initialization this + /// field is effectively const and therefore it is not needed to protect read-access by a lock. + MumblePluginQt *m_mumPlugQt; + + virtual void resolveFunctionPointers() override; + virtual bool doInitialize() override; + + LegacyPlugin(QString path, bool isBuiltIn = false, QObject *p = 0); + + virtual bool showAboutDialog(QWidget *parent) const override; + virtual bool showConfigDialog(QWidget *parent) const override; + virtual uint8_t initPositionalData(const char *const*programNames, const uint64_t *programPIDs, size_t programCount) override; + virtual bool fetchPositionalData(Position3D& avatarPos, Vector3D& avatarDir, Vector3D& avatarAxis, Position3D& cameraPos, Vector3D& cameraDir, + Vector3D& cameraAxis, QString& context, QString& identity) const override; + virtual void shutdownPositionalData() override; + public: + virtual ~LegacyPlugin() override; + + virtual mumble_error_t init() override; + + // functions for direct plugin-interaction + virtual QString getName() const override; + + virtual QString getDescription() const override; + virtual uint32_t getFeatures() const override; + virtual mumble_version_t getAPIVersion() const override; + + virtual mumble_version_t getVersion() const override; + + // functions for checking which underlying plugin functions are implemented + virtual bool providesAboutDialog() const override; + virtual bool providesConfigDialog() const override; +}; + +#endif diff --git a/src/mumble/Log.cpp b/src/mumble/Log.cpp index d4c0111973a..5a2b14edc97 100644 --- a/src/mumble/Log.cpp +++ b/src/mumble/Log.cpp @@ -334,6 +334,8 @@ QVector< LogMessage > Log::qvDeferredLogs; Log::Log(QObject *p) : QObject(p) { + qRegisterMetaType(); + #ifndef USE_NO_TTS tts = new TextToSpeech(this); tts->setVolume(Global::get().s.iTTSVolume); @@ -374,7 +376,8 @@ const Log::MsgType Log::msgOrder[] = { DebugInfo, ChannelLeaveDisconnect, PermissionDenied, TextMessage, - PrivateTextMessage }; + PrivateTextMessage, + PluginMessage }; const char *Log::msgNames[] = { QT_TRANSLATE_NOOP("Log", "Debug"), QT_TRANSLATE_NOOP("Log", "Critical"), @@ -406,7 +409,8 @@ const char *Log::msgNames[] = { QT_TRANSLATE_NOOP("Log", "Debug"), QT_TRANSLATE_NOOP("Log", "User left channel and disconnected"), QT_TRANSLATE_NOOP("Log", "Private text message"), QT_TRANSLATE_NOOP("Log", "User started listening to channel"), - QT_TRANSLATE_NOOP("Log", "User stopped listening to channel") }; + QT_TRANSLATE_NOOP("Log", "User stopped listening to channel"), + QT_TRANSLATE_NOOP("Log", "Plugin message") }; QString Log::msgName(MsgType t) const { return tr(msgNames[t]); diff --git a/src/mumble/Log.h b/src/mumble/Log.h index 6958f2be9a6..d584f2a490a 100644 --- a/src/mumble/Log.h +++ b/src/mumble/Log.h @@ -90,8 +90,10 @@ class Log : public QObject { ChannelLeaveDisconnect, PrivateTextMessage, ChannelListeningAdd, - ChannelListeningRemove + ChannelListeningRemove, + PluginMessage }; + enum LogColorType { Time, Server, Privilege, Source, Target }; static const MsgType firstMsgType = DebugInfo; static const MsgType lastMsgType = ChannelListeningRemove; @@ -134,7 +136,9 @@ class Log : public QObject { static void logOrDefer(Log::MsgType mt, const QString &console, const QString &terse = QString(), bool ownMessage = false, const QString &overrideTTS = QString(), bool ignoreTTS = false); public slots: - void log(MsgType mt, const QString &console, const QString &terse = QString(), bool ownMessage = false, + // We have to explicitly use Log::MsgType and not only MsgType in order to be able to use QMetaObject::invokeMethod + // with this function. + void log(Log::MsgType mt, const QString &console, const QString &terse = QString(), bool ownMessage = false, const QString &overrideTTS = QString(), bool ignoreTTS = false); /// Logs LogMessages that have been deferred so far void processDeferredLogs(); @@ -172,4 +176,6 @@ class LogDocumentResourceAddedEvent : public QEvent { LogDocumentResourceAddedEvent(); }; +Q_DECLARE_METATYPE(Log::MsgType); + #endif diff --git a/src/mumble/MainWindow.cpp b/src/mumble/MainWindow.cpp index e7ae820d169..f2718f0a312 100644 --- a/src/mumble/MainWindow.cpp +++ b/src/mumble/MainWindow.cpp @@ -33,8 +33,8 @@ #include "ChannelListener.h" #include "ListenerLocalVolumeDialog.h" #include "Markdown.h" +#include "PluginManager.h" #include "PTTButtonWidget.h" -#include "Plugins.h" #include "RichTextEditor.h" #include "SSLCipherInfo.h" #include "Screen.h" @@ -185,6 +185,8 @@ MainWindow::MainWindow(QWidget *p) : QMainWindow(p) { setOnTop(Global::get().s.aotbAlwaysOnTop == Settings::OnTopAlways || (Global::get().s.bMinimalView && Global::get().s.aotbAlwaysOnTop == Settings::OnTopInMinimal) || (!Global::get().s.bMinimalView && Global::get().s.aotbAlwaysOnTop == Settings::OnTopInNormal)); + + QObject::connect(this, &MainWindow::serverSynchronized, Global::get().pluginManager, &PluginManager::on_serverSynchronized); } void MainWindow::createActions() { @@ -318,6 +320,13 @@ void MainWindow::setupGui() { QObject::connect(&ChannelListener::get(), &ChannelListener::localVolumeAdjustmentsChanged, pmModel, &UserModel::on_channelListenerLocalVolumeAdjustmentChanged); + // connect slots to PluginManager + QObject::connect(pmModel, &UserModel::userAdded, Global::get().pluginManager, &PluginManager::on_userAdded); + QObject::connect(pmModel, &UserModel::userRemoved, Global::get().pluginManager, &PluginManager::on_userRemoved); + QObject::connect(pmModel, &UserModel::channelAdded, Global::get().pluginManager, &PluginManager::on_channelAdded); + QObject::connect(pmModel, &UserModel::channelRemoved, Global::get().pluginManager, &PluginManager::on_channelRemoved); + QObject::connect(pmModel, &UserModel::channelRenamed, Global::get().pluginManager, &PluginManager::on_channelRenamed); + qaAudioMute->setChecked(Global::get().s.bMute); qaAudioDeaf->setChecked(Global::get().s.bDeaf); #ifdef USE_NO_TTS @@ -902,6 +911,16 @@ static void recreateServerHandler() { SLOT(resolverError(QAbstractSocket::SocketError, QString))); QObject::connect(sh.get(), &ServerHandler::disconnected, Global::get().talkingUI, &TalkingUI::on_serverDisconnected); + + // We have to use direct connections for these here as the PluginManager must be able to access the connection's ID + // and in order for that to be possible the (dis)connection process must not proceed in the background. + Global::get().pluginManager->connect(sh.get(), &ServerHandler::connected, Global::get().pluginManager, + &PluginManager::on_serverConnected, Qt::DirectConnection); + // We connect the plugin manager to "aboutToDisconnect" instead of "disconnect" in order for the slot to be + // guaranteed to be completed *before* the acutal disconnect logic (e.g. MainWindow::serverDisconnected) kicks in. + // In order for that to work it is ESSENTIAL to use a DIRECT CONNECTION! + Global::get().pluginManager->connect(sh.get(), &ServerHandler::aboutToDisconnect, Global::get().pluginManager, + &PluginManager::on_serverDisconnected, Qt::DirectConnection); } void MainWindow::openUrl(const QUrl &url) { @@ -2491,6 +2510,12 @@ void MainWindow::on_qaAudioMute_triggered() { updateTrayIcon(); } +void MainWindow::setAudioMute(bool mute) { + // Pretend the user pushed the button manually + qaAudioMute->setChecked(mute); + qaAudioMute->triggered(mute); +} + void MainWindow::on_qaAudioDeaf_triggered() { if (Global::get().bInAudioWizard) { qaAudioDeaf->setChecked(!qaAudioDeaf->isChecked()); @@ -2503,11 +2528,13 @@ void MainWindow::on_qaAudioDeaf_triggered() { on_qaAudioMute_triggered(); return; } + AudioInputPtr ai = Global::get().ai; if (ai) ai->tIdle.restart(); Global::get().s.bDeaf = qaAudioDeaf->isChecked(); + if (Global::get().s.bDeaf && !Global::get().s.bMute) { bAutoUnmute = true; Global::get().s.bMute = true; @@ -2527,6 +2554,12 @@ void MainWindow::on_qaAudioDeaf_triggered() { updateTrayIcon(); } +void MainWindow::setAudioDeaf(bool deaf) { + // Pretend the user pushed the button manually + qaAudioDeaf->setChecked(deaf); + qaAudioDeaf->triggered(deaf); +} + void MainWindow::on_qaRecording_triggered() { if (voiceRecorderDialog) { voiceRecorderDialog->show(); @@ -2549,7 +2582,7 @@ void MainWindow::on_qaAudioStats_triggered() { } void MainWindow::on_qaAudioUnlink_triggered() { - Global::get().p->bUnlink = true; + Global::get().pluginManager->unlinkPositionalData(); } void MainWindow::on_qaConfigDialog_triggered() { diff --git a/src/mumble/MainWindow.h b/src/mumble/MainWindow.h index f600ede146c..20c8a30006c 100644 --- a/src/mumble/MainWindow.h +++ b/src/mumble/MainWindow.h @@ -311,6 +311,14 @@ public slots: /// Updates the user's image directory to the given path (any included /// filename is discarded). void updateImagePath(QString filepath) const; + /// Sets the local user's mute state + /// + /// @param mute Whether to mute the user + void setAudioMute(bool mute); + /// Sets the local user's deaf state + /// + /// @param deaf Whether to deafen the user + void setAudioDeaf(bool deaf); signals: /// Signal emitted when the server and the client have finished /// synchronizing (after a new connection). diff --git a/src/mumble/ManualPlugin.cpp b/src/mumble/ManualPlugin.cpp index 981273c5f0c..e4ea84336f8 100644 --- a/src/mumble/ManualPlugin.cpp +++ b/src/mumble/ManualPlugin.cpp @@ -9,13 +9,15 @@ #include "ManualPlugin.h" #include "ui_ManualPlugin.h" +#include "Global.h" + #include #include #include -#include "../../plugins/mumble_plugin.h" -#include "Global.h" +#define MUMBLE_ALLOW_DEPRECATED_LEGACY_PLUGIN_API +#include "../../plugins/mumble_legacy_plugin.h" static QPointer< Manual > mDlg = nullptr; static bool bLinkable = false; @@ -43,7 +45,7 @@ Manual::Manual(QWidget *p) : QDialog(p) { qgvPosition->viewport()->installEventFilter(this); qgvPosition->scale(1.0f, 1.0f); - qgsScene = new QGraphicsScene(QRectF(-5.0f, -5.0f, 10.0f, 10.0f), this); + m_qgsScene = new QGraphicsScene(QRectF(-5.0f, -5.0f, 10.0f, 10.0f), this); const float indicatorDiameter = 4.0f; QPainterPath indicator; @@ -53,9 +55,9 @@ Manual::Manual(QWidget *p) : QDialog(p) { indicator.moveTo(0, indicatorDiameter / 2); indicator.lineTo(0, indicatorDiameter); - qgiPosition = qgsScene->addPath(indicator); + m_qgiPosition = m_qgsScene->addPath(indicator); - qgvPosition->setScene(qgsScene); + qgvPosition->setScene(m_qgsScene); qgvPosition->fitInView(-5.0f, -5.0f, 10.0f, 10.0f, Qt::KeepAspectRatio); qdsbX->setRange(-FLT_MAX, FLT_MAX); @@ -101,7 +103,7 @@ bool Manual::eventFilter(QObject *obj, QEvent *evt) { QPointF qpf = qgvPosition->mapToScene(qme->pos()); qdsbX->setValue(qpf.x()); qdsbZ->setValue(-qpf.y()); - qgiPosition->setPos(qpf); + m_qgiPosition->setPos(qpf); } } } @@ -134,8 +136,8 @@ void Manual::on_qpbActivated_clicked(bool b) { } void Manual::on_qdsbX_valueChanged(double d) { - my.avatar_pos[0] = my.camera_pos[0] = static_cast< float >(d); - qgiPosition->setPos(my.avatar_pos[0], -my.avatar_pos[2]); + my.avatar_pos[0] = my.camera_pos[0] = static_cast(d); + m_qgiPosition->setPos(my.avatar_pos[0], -my.avatar_pos[2]); } void Manual::on_qdsbY_valueChanged(double d) { @@ -143,8 +145,8 @@ void Manual::on_qdsbY_valueChanged(double d) { } void Manual::on_qdsbZ_valueChanged(double d) { - my.avatar_pos[2] = my.camera_pos[2] = static_cast< float >(d); - qgiPosition->setPos(my.avatar_pos[0], -my.avatar_pos[2]); + my.avatar_pos[2] = my.camera_pos[2] = static_cast(d); + m_qgiPosition->setPos(my.avatar_pos[0], -my.avatar_pos[2]); } void Manual::on_qsbAzimuth_valueChanged(int i) { @@ -262,7 +264,7 @@ void Manual::on_speakerPositionUpdate(QHash< unsigned int, Position2D > position remainingIt.next(); const float speakerRadius = 1.2; - QGraphicsItem *speakerItem = qgsScene->addEllipse(-speakerRadius, -speakerRadius, 2 * speakerRadius, + QGraphicsItem *speakerItem = m_qgsScene->addEllipse(-speakerRadius, -speakerRadius, 2 * speakerRadius, 2 * speakerRadius, QPen(), QBrush(Qt::red)); Position2D pos = remainingIt.value(); @@ -317,7 +319,7 @@ void Manual::updateTopAndFront(int azimuth, int elevation) { iAzimuth = azimuth; iElevation = elevation; - qgiPosition->setRotation(azimuth); + m_qgiPosition->setRotation(azimuth); double azim = azimuth * M_PI / 180.; double elev = elevation * M_PI / 180.; @@ -415,3 +417,16 @@ MumblePlugin *ManualPlugin_getMumblePlugin() { MumblePluginQt *ManualPlugin_getMumblePluginQt() { return &manualqt; } + + +/////////// Implementation of the ManualPlugin class ////////////// +ManualPlugin::ManualPlugin(QObject *p) : LegacyPlugin(QString::fromLatin1("manual.builtin"), true, p) { +} + +ManualPlugin::~ManualPlugin() { +} + +void ManualPlugin::resolveFunctionPointers() { + m_mumPlug = &manual; + m_mumPlugQt = &manualqt; +} diff --git a/src/mumble/ManualPlugin.h b/src/mumble/ManualPlugin.h index fa76efbc889..dbeee0d31fb 100644 --- a/src/mumble/ManualPlugin.h +++ b/src/mumble/ManualPlugin.h @@ -12,8 +12,7 @@ #include #include "ui_ManualPlugin.h" - -#include "../../plugins/mumble_plugin.h" +#include "LegacyPlugin.h" #include #include @@ -67,8 +66,8 @@ public slots: void on_updateStaleSpeakers(); protected: - QGraphicsScene *qgsScene; - QGraphicsItem *qgiPosition; + QGraphicsScene *m_qgsScene; + QGraphicsItem *m_qgiPosition; std::atomic< bool > updateLoopRunning; @@ -83,4 +82,20 @@ public slots: MumblePlugin *ManualPlugin_getMumblePlugin(); MumblePluginQt *ManualPlugin_getMumblePluginQt(); + +/// A built-in "plugin" for positional data gatherig allowing for manually placing the "players" in a UI +class ManualPlugin : public LegacyPlugin { + friend class Plugin; // needed in order for Plugin::createNew to access LegacyPlugin::doInitialize() + private: + Q_OBJECT + Q_DISABLE_COPY(ManualPlugin) + + protected: + virtual void resolveFunctionPointers() Q_DECL_OVERRIDE; + ManualPlugin(QObject *p = nullptr); + + public: + virtual ~ManualPlugin() Q_DECL_OVERRIDE; +}; + #endif diff --git a/src/mumble/Messages.cpp b/src/mumble/Messages.cpp index 62887c34f6c..729085f5efe 100644 --- a/src/mumble/Messages.cpp +++ b/src/mumble/Messages.cpp @@ -24,7 +24,6 @@ # include "Overlay.h" #endif #include "ChannelListener.h" -#include "Plugins.h" #include "ServerHandler.h" #include "TalkingUI.h" #include "User.h" @@ -35,6 +34,7 @@ #include "VersionCheck.h" #include "ViewCert.h" #include "crypto/CryptState.h" +#include "PluginManager.h" #include "Global.h" #include @@ -1303,6 +1303,25 @@ void MainWindow::msgSuggestConfig(const MumbleProto::SuggestConfig &msg) { } } +void MainWindow::msgPluginDataTransmission(const MumbleProto::PluginDataTransmission &msg) { + // Another client's plugin has sent us some data. Verify the necessary parts are there and delegate it to the + // PluginManager + + if (!msg.has_sendersession() || !msg.has_data() || !msg.has_dataid()) { + // if the message contains no sender session, no data or no ID for the data, it is of no use to us and we discard it + return; + } + + const ClientUser *sender = ClientUser::get(msg.sendersession()); + const std::string &data = msg.data(); + + if (sender) { + static_assert(sizeof(unsigned char) == sizeof(uint8_t), "Unsigned char does not have expected 8bit size"); + // As long as above assertion is true, we are only casting away the sign, which is fine + Global::get().pluginManager->on_receiveData(sender, reinterpret_cast< const uint8_t * >(data.c_str()), data.size(), msg.dataid().c_str()); + } +} + #undef ACTOR_INIT #undef VICTIM_INIT #undef SELF_INIT diff --git a/src/mumble/NetworkConfig.cpp b/src/mumble/NetworkConfig.cpp index 82cd5c9f4dd..6b75bd64446 100644 --- a/src/mumble/NetworkConfig.cpp +++ b/src/mumble/NetworkConfig.cpp @@ -28,6 +28,7 @@ static ConfigRegistrar registrarNetworkConfig(1300, NetworkConfigNew); NetworkConfig::NetworkConfig(Settings &st) : ConfigWidget(st) { setupUi(this); + qcbType->setAccessibleName(tr("Type")); qleHostname->setAccessibleName(tr("Hostname")); qlePort->setAccessibleName(tr("Port")); @@ -72,7 +73,8 @@ void NetworkConfig::load(const Settings &r) { const QSignalBlocker blocker(qcbAutoUpdate); loadCheckBox(qcbAutoUpdate, r.bUpdateCheck); - loadCheckBox(qcbPluginUpdate, r.bPluginCheck); + loadCheckBox(qcbPluginUpdateCheck, r.bPluginCheck); + loadCheckBox(qcbPluginAutoUpdate, r.bPluginAutoUpdate); loadCheckBox(qcbUsage, r.bUsage); } @@ -91,9 +93,10 @@ void NetworkConfig::save() const { s.qsProxyUsername = qleUsername->text(); s.qsProxyPassword = qlePassword->text(); - s.bUpdateCheck = qcbAutoUpdate->isChecked(); - s.bPluginCheck = qcbPluginUpdate->isChecked(); - s.bUsage = qcbUsage->isChecked(); + s.bUpdateCheck = qcbAutoUpdate->isChecked(); + s.bPluginCheck = qcbPluginUpdateCheck->isChecked(); + s.bPluginAutoUpdate = qcbPluginAutoUpdate->isChecked(); + s.bUsage = qcbUsage->isChecked(); } static QNetworkProxy::ProxyType local_to_qt_proxy(Settings::ProxyType pt) { diff --git a/src/mumble/NetworkConfig.ui b/src/mumble/NetworkConfig.ui index f9c291288c8..ef783a98779 100644 --- a/src/mumble/NetworkConfig.ui +++ b/src/mumble/NetworkConfig.ui @@ -7,7 +7,7 @@ 0 0 576 - 572 + 584 @@ -315,7 +315,7 @@ Prevents the client from sending potentially identifying information about the o - + Check for new releases of plugins automatically. @@ -323,7 +323,14 @@ Prevents the client from sending potentially identifying information about the o This will check for new releases of plugins every time you start the program, and download them automatically. - Download plugin and overlay updates on startup + Check for plugin updates on startup + + + + + + + Automatically download and install plugin updates diff --git a/src/mumble/Plugin.cpp b/src/mumble/Plugin.cpp new file mode 100644 index 00000000000..8317c584feb --- /dev/null +++ b/src/mumble/Plugin.cpp @@ -0,0 +1,694 @@ +// Copyright 2021 The Mumble Developers. All rights reserved. +// Use of this source code is governed by a BSD-style license +// that can be found in the LICENSE file at the root of the +// Mumble source tree or at . + +#include "Plugin.h" +#include "Version.h" +#include "API.h" + +#include +#include + +#include + + +// initialize the static ID counter +plugin_id_t Plugin::s_nextID = 1; +QMutex Plugin::s_idLock(QMutex::NonRecursive); + +void assertPluginLoaded(const Plugin* plugin) { + // don't throw and exception in release build + if (!plugin->isLoaded()) { +#ifdef QT_DEBUG + throw std::runtime_error("Attempting to access plugin but it is not loaded!"); +#else + qWarning("Plugin assertion failed: Assumed plugin with ID %d to be loaded but it wasn't!", plugin->getID()); +#endif + } +} + +Plugin::Plugin(QString path, bool isBuiltIn, QObject *p) + : QObject(p), + m_lib(path), + m_pluginPath(path), + m_pluginIsLoaded(false), + m_pluginLock(QReadWriteLock::NonRecursive), + m_pluginFnc(), + m_isBuiltIn(isBuiltIn), + m_positionalDataIsEnabled(true), + m_positionalDataIsActive(false), + m_mayMonitorKeyboard(false) { + // See if the plugin is loadable in the first place unless it is a built-in plugin + m_pluginIsValid = isBuiltIn || m_lib.load(); + + if (!m_pluginIsValid) { + // throw an exception to indicate that the plugin isn't valid + throw PluginError("Unable to load the specified library"); + } + + // aquire id-lock in order to assign an ID to this plugin + QMutexLocker lock(&Plugin::s_idLock); + m_pluginID = Plugin::s_nextID; + Plugin::s_nextID++; +} + +Plugin::~Plugin() { + if (isLoaded()) { + shutdown(); + } + if (m_lib.isLoaded()) { + m_lib.unload(); + } +} + +QString Plugin::extractWrappedString(MumbleStringWrapper wrapper) const { + QString wrappedString = QString::fromUtf8(wrapper.data, wrapper.size); + + if (wrapper.needsReleasing) { + releaseResource(static_cast(wrapper.data)); + } + + return wrappedString; +} + +bool Plugin::doInitialize() { + resolveFunctionPointers(); + + return m_pluginIsValid; +} + +void Plugin::resolveFunctionPointers() { + if (isValid()) { + // The corresponding library was loaded -> try to locate all API functions and provide defaults for + // the missing ones + + QWriteLocker lock(&m_pluginLock); + + // resolve the mandatory functions first + m_pluginFnc.init = reinterpret_cast(m_lib.resolve("mumble_init")); + m_pluginFnc.shutdown = reinterpret_cast(m_lib.resolve("mumble_shutdown")); + m_pluginFnc.getName = reinterpret_cast(m_lib.resolve("mumble_getName")); + m_pluginFnc.getAPIVersion = reinterpret_cast(m_lib.resolve("mumble_getAPIVersion")); + m_pluginFnc.registerAPIFunctions = reinterpret_cast(m_lib.resolve("mumble_registerAPIFunctions")); + m_pluginFnc.releaseResource = reinterpret_cast(m_lib.resolve("mumble_releaseResource")); + + // validate that all those functions are available in the loaded lib + m_pluginIsValid = m_pluginFnc.init && m_pluginFnc.shutdown && m_pluginFnc.getName && m_pluginFnc.getAPIVersion + && m_pluginFnc.registerAPIFunctions && m_pluginFnc.releaseResource; + + if (!m_pluginIsValid) { + // Don't bother trying to resolve any other functions +#ifdef MUMBLE_PLUGIN_DEBUG +#define CHECK_AND_LOG(name) if (!m_pluginFnc.name) { qDebug("\t\"%s\" is missing the %s() function", qPrintable(m_pluginPath), "mumble_" #name); } + CHECK_AND_LOG(init); + CHECK_AND_LOG(shutdown); + CHECK_AND_LOG(getName); + CHECK_AND_LOG(getAPIVersion); + CHECK_AND_LOG(registerAPIFunctions); + CHECK_AND_LOG(releaseResource); +#undef CHECK_AND_LOG +#endif + + return; + } + + // The mandatory functions are there, now see if any optional functions are implemented as well + m_pluginFnc.setMumbleInfo = reinterpret_cast(m_lib.resolve("mumble_setMumbleInfo")); + m_pluginFnc.getVersion = reinterpret_cast(m_lib.resolve("mumble_getVersion")); + m_pluginFnc.getAuthor = reinterpret_cast(m_lib.resolve("mumble_getAuthor")); + m_pluginFnc.getDescription = reinterpret_cast(m_lib.resolve("mumble_getDescription")); + m_pluginFnc.getFeatures = reinterpret_cast(m_lib.resolve("mumble_getFeatures")); + m_pluginFnc.deactivateFeatures = reinterpret_cast(m_lib.resolve("mumble_deactivateFeatures")); + m_pluginFnc.initPositionalData = reinterpret_cast(m_lib.resolve("mumble_initPositionalData")); + m_pluginFnc.fetchPositionalData = reinterpret_cast(m_lib.resolve("mumble_fetchPositionalData")); + m_pluginFnc.shutdownPositionalData = reinterpret_cast(m_lib.resolve("mumble_shutdownPositionalData")); + m_pluginFnc.onServerConnected = reinterpret_cast(m_lib.resolve("mumble_onServerConnected")); + m_pluginFnc.onServerDisconnected = reinterpret_cast(m_lib.resolve("mumble_onServerDisconnected")); + m_pluginFnc.onChannelEntered = reinterpret_cast(m_lib.resolve("mumble_onChannelEntered")); + m_pluginFnc.onChannelExited = reinterpret_cast(m_lib.resolve("mumble_onChannelExited")); + m_pluginFnc.onUserTalkingStateChanged = reinterpret_cast(m_lib.resolve("mumble_onUserTalkingStateChanged")); + m_pluginFnc.onReceiveData = reinterpret_cast(m_lib.resolve("mumble_onReceiveData")); + m_pluginFnc.onAudioInput = reinterpret_cast(m_lib.resolve("mumble_onAudioInput")); + m_pluginFnc.onAudioSourceFetched = reinterpret_cast(m_lib.resolve("mumble_onAudioSourceFetched")); + m_pluginFnc.onAudioOutputAboutToPlay = reinterpret_cast(m_lib.resolve("mumble_onAudioOutputAboutToPlay")); + m_pluginFnc.onServerSynchronized = reinterpret_cast(m_lib.resolve("mumble_onServerSynchronized")); + m_pluginFnc.onUserAdded = reinterpret_cast(m_lib.resolve("mumble_onUserAdded")); + m_pluginFnc.onUserRemoved = reinterpret_cast(m_lib.resolve("mumble_onUserRemoved")); + m_pluginFnc.onChannelAdded = reinterpret_cast(m_lib.resolve("mumble_onChannelAdded")); + m_pluginFnc.onChannelRemoved = reinterpret_cast(m_lib.resolve("mumble_onChannelRemoved")); + m_pluginFnc.onChannelRenamed = reinterpret_cast(m_lib.resolve("mumble_onChannelRenamed")); + m_pluginFnc.onKeyEvent = reinterpret_cast(m_lib.resolve("mumble_onKeyEvent")); + m_pluginFnc.hasUpdate = reinterpret_cast(m_lib.resolve("mumble_hasUpdate")); + m_pluginFnc.getUpdateDownloadURL = reinterpret_cast(m_lib.resolve("mumble_getUpdateDownloadURL")); + +#ifdef MUMBLE_PLUGIN_DEBUG +#define CHECK_AND_LOG(name) qDebug("\t" "mumble_" #name ": %s", (m_pluginFnc.name == nullptr ? "no" : "yes")) + qDebug(">>>> Found optional functions for plugin \"%s\"", qUtf8Printable(m_pluginPath)); + CHECK_AND_LOG(setMumbleInfo); + CHECK_AND_LOG(getVersion); + CHECK_AND_LOG(getAuthor); + CHECK_AND_LOG(getDescription); + CHECK_AND_LOG(getFeatures); + CHECK_AND_LOG(deactivateFeatures); + CHECK_AND_LOG(initPositionalData); + CHECK_AND_LOG(fetchPositionalData); + CHECK_AND_LOG(shutdownPositionalData); + CHECK_AND_LOG(onServerConnected); + CHECK_AND_LOG(onServerDisconnected); + CHECK_AND_LOG(onChannelEntered); + CHECK_AND_LOG(onChannelExited); + CHECK_AND_LOG(onUserTalkingStateChanged); + CHECK_AND_LOG(onReceiveData); + CHECK_AND_LOG(onAudioInput); + CHECK_AND_LOG(onAudioSourceFetched); + CHECK_AND_LOG(onAudioOutputAboutToPlay); + CHECK_AND_LOG(onServerSynchronized); + CHECK_AND_LOG(onUserAdded); + CHECK_AND_LOG(onUserRemoved); + CHECK_AND_LOG(onChannelAdded); + CHECK_AND_LOG(onChannelRemoved); + CHECK_AND_LOG(onChannelRenamed); + CHECK_AND_LOG(onKeyEvent); + CHECK_AND_LOG(hasUpdate); + CHECK_AND_LOG(getUpdateDownloadURL); + qDebug("<<<<"); +#endif + + // If positional audio is to be supported, all three corresponding functions have to be implemented + // For PA it is all or nothing + if (!(m_pluginFnc.initPositionalData && m_pluginFnc.fetchPositionalData && m_pluginFnc.shutdownPositionalData) + && (m_pluginFnc.initPositionalData || m_pluginFnc.fetchPositionalData || m_pluginFnc.shutdownPositionalData)) { + m_pluginFnc.initPositionalData = nullptr; + m_pluginFnc.fetchPositionalData = nullptr; + m_pluginFnc.shutdownPositionalData = nullptr; +#ifdef MUMBLE_PLUGIN_DEBUG + qDebug("\t\"%s\" has only partially implemented positional data functions -> deactivating all of them", qPrintable(m_pluginPath)); +#endif + } + } +} + +bool Plugin::isValid() const { + PluginReadLocker lock(&m_pluginLock); + + return m_pluginIsValid; +} + +bool Plugin::isLoaded() const { + PluginReadLocker lock(&m_pluginLock); + + return m_pluginIsLoaded; +} + +plugin_id_t Plugin::getID() const { + PluginReadLocker lock(&m_pluginLock); + + return m_pluginID; +} + +bool Plugin::isBuiltInPlugin() const { + PluginReadLocker lock(&m_pluginLock); + + return m_isBuiltIn; +} + +QString Plugin::getFilePath() const { + PluginReadLocker lock(&m_pluginLock); + + return m_pluginPath; +} + +bool Plugin::isPositionalDataEnabled() const { + PluginReadLocker lock(&m_pluginLock); + + return m_positionalDataIsEnabled; +} + +void Plugin::enablePositionalData(bool enable) { + QWriteLocker lock(&m_pluginLock); + + m_positionalDataIsEnabled = enable; +} + +bool Plugin::isPositionalDataActive() const { + PluginReadLocker lock(&m_pluginLock); + + return m_positionalDataIsActive; +} + +void Plugin::allowKeyboardMonitoring(bool allow) { + QWriteLocker lock(&m_pluginLock); + + m_mayMonitorKeyboard = allow; +} + +bool Plugin::isKeyboardMonitoringAllowed() const { + PluginReadLocker lock(&m_pluginLock); + + return m_mayMonitorKeyboard; +} + +mumble_error_t Plugin::init() { + { + QReadLocker lock(&m_pluginLock); + + if (m_pluginIsLoaded) { + return STATUS_OK; + } + } + + ////////////////////////////// + // Step 1: Introduce ourselves (inform the plugin about Mumble's (API) version + + // Get Mumble version + int mumbleMajor, mumbleMinor, mumblePatch; + MumbleVersion::get(&mumbleMajor, &mumbleMinor, &mumblePatch); + + // Require API version 1.0.0 as the minimal supported one + setMumbleInfo({ mumbleMajor, mumbleMinor, mumblePatch }, MUMBLE_PLUGIN_API_VERSION, { 1, 0, 0 }); + + + ////////////////////////////// + // Step 2: Provide the API functions to the plugin + const mumble_version_t apiVersion = getAPIVersion(); + if (apiVersion >= mumble_version_t({1, 0, 0}) && apiVersion < mumble_version_t({1, 2, 0})) { + MumbleAPI_v_1_0_x api = API::getMumbleAPI_v_1_0_x(); + registerAPIFunctions(&api); + } else { + // The API version could not be obtained -> this is an invalid plugin that shouldn't have been loaded in the first place + qWarning("Unable to obtain requested MumbleAPI version"); + return EC_INVALID_API_VERSION; + } + + + ////////////////////////////// + // Step 3: Actually try to load the plugin + + mumble_error_t retStatus; + if (m_pluginFnc.init) { + retStatus = m_pluginFnc.init(m_pluginID); + } else { + retStatus = EC_GENERIC_ERROR; + } + + { + QWriteLocker lock(&m_pluginLock); + m_pluginIsLoaded = retStatus == STATUS_OK; + } + + return retStatus; +} + +void Plugin::shutdown() { + bool posDataActive; + { + QReadLocker rLock(&m_pluginLock); + if (!m_pluginIsLoaded) { + return; + } + + posDataActive = m_positionalDataIsActive; + } + + if (posDataActive) { + shutdownPositionalData(); + } + + if (m_pluginFnc.shutdown) { + m_pluginFnc.shutdown(); + } + + { + QWriteLocker lock(&m_pluginLock); + + m_pluginIsLoaded = false; + } +} + +QString Plugin::getName() const { + if (m_pluginFnc.getName) { + return extractWrappedString(m_pluginFnc.getName()); + } else { + return QString::fromLatin1("Unknown plugin"); + } +} + +mumble_version_t Plugin::getAPIVersion() const { + if (m_pluginFnc.getAPIVersion) { + return m_pluginFnc.getAPIVersion(); + } else { + return VERSION_UNKNOWN; + } +} + +void Plugin::registerAPIFunctions(void *api) const { + if (m_pluginFnc.registerAPIFunctions) { + m_pluginFnc.registerAPIFunctions(api); + } +} + +void Plugin::releaseResource(const void *pointer) const { + if (m_pluginFnc.releaseResource) { + m_pluginFnc.releaseResource(pointer); + } +} + +void Plugin::setMumbleInfo(mumble_version_t mumbleVersion, mumble_version_t mumbleAPIVersion, mumble_version_t minimalExpectedAPIVersion) const { + if (m_pluginFnc.setMumbleInfo) { + m_pluginFnc.setMumbleInfo(mumbleVersion, mumbleAPIVersion, minimalExpectedAPIVersion); + } +} + +mumble_version_t Plugin::getVersion() const { + if (m_pluginFnc.getVersion) { + return m_pluginFnc.getVersion(); + } else { + return VERSION_UNKNOWN; + } +} + +QString Plugin::getAuthor() const { + if (m_pluginFnc.getAuthor) { + return extractWrappedString(m_pluginFnc.getAuthor()); + } else { + return QString::fromLatin1("Unknown"); + } +} + +QString Plugin::getDescription() const { + if (m_pluginFnc.getDescription) { + return extractWrappedString(m_pluginFnc.getDescription()); + } else { + return QString::fromLatin1("No description provided"); + } +} + +uint32_t Plugin::getFeatures() const { + if (m_pluginFnc.getFeatures) { + return m_pluginFnc.getFeatures(); + } else { + return FEATURE_NONE; + } +} + +uint32_t Plugin::deactivateFeatures(uint32_t features) const { + assertPluginLoaded(this); + + if (m_pluginFnc.deactivateFeatures) { + return m_pluginFnc.deactivateFeatures(features); + } else { + return features; + } +} + +bool Plugin::showAboutDialog(QWidget *parent) const { + assertPluginLoaded(this); + + Q_UNUSED(parent); + return false; +} + +bool Plugin::showConfigDialog(QWidget *parent) const { + assertPluginLoaded(this); + + Q_UNUSED(parent); + return false; +} + +uint8_t Plugin::initPositionalData(const char *const*programNames, const uint64_t *programPIDs, size_t programCount) { + assertPluginLoaded(this); + + if (m_pluginFnc.initPositionalData) { + uint8_t returnCode = m_pluginFnc.initPositionalData(programNames, programPIDs, programCount); + + { + QWriteLocker lock(&m_pluginLock); + m_positionalDataIsActive = returnCode == PDEC_OK; + } + + return returnCode; + } else { + return PDEC_ERROR_PERM; + } +} + +bool Plugin::fetchPositionalData(Position3D& avatarPos, Vector3D& avatarDir, Vector3D& avatarAxis, Position3D& cameraPos, Vector3D& cameraDir, + Vector3D& cameraAxis, QString& context, QString& identity) const { + assertPluginLoaded(this); + + if (m_pluginFnc.fetchPositionalData) { + const char *contextPtr = ""; + const char *identityPtr = ""; + + bool retStatus = m_pluginFnc.fetchPositionalData(static_cast(avatarPos), static_cast(avatarDir), + static_cast(avatarAxis), static_cast(cameraPos), static_cast(cameraDir), static_cast(cameraAxis), + &contextPtr, &identityPtr); + + context = QString::fromUtf8(contextPtr); + identity = QString::fromUtf8(identityPtr); + + return retStatus; + } else { + avatarPos.toZero(); + avatarDir.toZero(); + avatarAxis.toZero(); + cameraPos.toZero(); + cameraDir.toZero(); + cameraAxis.toZero(); + context = QString(); + identity = QString(); + + return false; + } +} + +void Plugin::shutdownPositionalData() { + assertPluginLoaded(this); + + if (m_pluginFnc.shutdownPositionalData) { + m_positionalDataIsActive = false; + + m_pluginFnc.shutdownPositionalData(); + } +} + +void Plugin::onServerConnected(mumble_connection_t connection) const { + assertPluginLoaded(this); + + if (m_pluginFnc.onServerConnected) { + m_pluginFnc.onServerConnected(connection); + } +} + +void Plugin::onServerDisconnected(mumble_connection_t connection) const { + assertPluginLoaded(this); + + if (m_pluginFnc.onServerDisconnected) { + m_pluginFnc.onServerDisconnected(connection); + } +} + +void Plugin::onChannelEntered(mumble_connection_t connection, mumble_userid_t userID, mumble_channelid_t previousChannelID, + mumble_channelid_t newChannelID) const { + assertPluginLoaded(this); + + if (m_pluginFnc.onChannelEntered) { + m_pluginFnc.onChannelEntered(connection, userID, previousChannelID, newChannelID); + } +} + +void Plugin::onChannelExited(mumble_connection_t connection, mumble_userid_t userID, mumble_channelid_t channelID) const { + assertPluginLoaded(this); + + if (m_pluginFnc.onChannelExited) { + m_pluginFnc.onChannelExited(connection, userID, channelID); + } +} + +void Plugin::onUserTalkingStateChanged(mumble_connection_t connection, mumble_userid_t userID, mumble_talking_state_t talkingState) const { + assertPluginLoaded(this); + + if (m_pluginFnc.onUserTalkingStateChanged) { + m_pluginFnc.onUserTalkingStateChanged(connection, userID, talkingState); + } +} + +bool Plugin::onReceiveData(mumble_connection_t connection, mumble_userid_t sender, const uint8_t *data, size_t dataLength, const char *dataID) const { + assertPluginLoaded(this); + + if (m_pluginFnc.onReceiveData) { + return m_pluginFnc.onReceiveData(connection, sender, data, dataLength, dataID); + } else { + return false; + } +} + +bool Plugin::onAudioInput(short *inputPCM, uint32_t sampleCount, uint16_t channelCount, uint32_t sampleRate, bool isSpeech) const { + assertPluginLoaded(this); + + if (m_pluginFnc.onAudioInput) { + return m_pluginFnc.onAudioInput(inputPCM, sampleCount, channelCount, sampleRate, isSpeech); + } else { + return false; + } +} + +bool Plugin::onAudioSourceFetched(float *outputPCM, uint32_t sampleCount, uint16_t channelCount, uint32_t sampleRate, bool isSpeech, mumble_userid_t userID) const { + assertPluginLoaded(this); + + if (m_pluginFnc.onAudioSourceFetched) { + return m_pluginFnc.onAudioSourceFetched(outputPCM, sampleCount, channelCount, sampleRate, isSpeech, userID); + } else { + return false; + } +} + +bool Plugin::onAudioOutputAboutToPlay(float *outputPCM, uint32_t sampleCount, uint16_t channelCount, uint32_t sampleRate) const { + assertPluginLoaded(this); + + if (m_pluginFnc.onAudioOutputAboutToPlay) { + return m_pluginFnc.onAudioOutputAboutToPlay(outputPCM, sampleCount, channelCount, sampleRate); + } else { + return false; + } +} + +void Plugin::onServerSynchronized(mumble_connection_t connection) const { + assertPluginLoaded(this); + + if (m_pluginFnc.onServerSynchronized) { + m_pluginFnc.onServerSynchronized(connection); + } +} + +void Plugin::onUserAdded(mumble_connection_t connection, mumble_userid_t userID) const { + assertPluginLoaded(this); + + if (m_pluginFnc.onUserAdded) { + m_pluginFnc.onUserAdded(connection, userID); + } +} + +void Plugin::onUserRemoved(mumble_connection_t connection, mumble_userid_t userID) const { + assertPluginLoaded(this); + + if (m_pluginFnc.onUserRemoved) { + m_pluginFnc.onUserRemoved(connection, userID); + } +} + +void Plugin::onChannelAdded(mumble_connection_t connection, mumble_channelid_t channelID) const { + assertPluginLoaded(this); + + if (m_pluginFnc.onChannelAdded) { + m_pluginFnc.onChannelAdded(connection, channelID); + } +} + +void Plugin::onChannelRemoved(mumble_connection_t connection, mumble_channelid_t channelID) const { + assertPluginLoaded(this); + + if (m_pluginFnc.onChannelRemoved) { + m_pluginFnc.onChannelRemoved(connection, channelID); + } +} + +void Plugin::onChannelRenamed(mumble_connection_t connection, mumble_channelid_t channelID) const { + assertPluginLoaded(this); + + if (m_pluginFnc.onChannelRenamed) { + m_pluginFnc.onChannelRenamed(connection, channelID); + } +} + +void Plugin::onKeyEvent(mumble_keycode_t keyCode, bool wasPress) const { + assertPluginLoaded(this); + + if (!m_mayMonitorKeyboard) { + // Keyboard monitoring is forbidden for this plugin + return; + } + + if (m_pluginFnc.onKeyEvent) { + m_pluginFnc.onKeyEvent(keyCode, wasPress); + } +} + +bool Plugin::hasUpdate() const { + if (m_pluginFnc.hasUpdate) { + return m_pluginFnc.hasUpdate(); + } else { + // A plugin that doesn't implement this function is assumed to never know about + // any potential updates + return false; + } +} + +QUrl Plugin::getUpdateDownloadURL() const { + if (m_pluginFnc.getUpdateDownloadURL) { + return QUrl(extractWrappedString(m_pluginFnc.getUpdateDownloadURL())); + } else { + // Return an empty URL as a fallback + return QUrl(); + } +} + +bool Plugin::providesAboutDialog() const { + return false; +} + +bool Plugin::providesConfigDialog() const { + return false; +} + + + +/////////////////// Implementation of the PluginReadLocker ///////////////////////// +PluginReadLocker::PluginReadLocker(QReadWriteLock *lock) + : m_lock(lock), + m_unlocked(false) { + relock(); +} + +void PluginReadLocker::unlock() { + if (!m_lock) { + // do nothgin for nullptr + return; + } + + m_unlocked = true; + + m_lock->unlock(); +} + +void PluginReadLocker::relock() { + if (!m_lock) { + // do nothing for a nullptr + return; + } + + // First try to lock for read-access + if (!m_lock->tryLockForRead()) { + // if that fails, we'll try to lock for write-access + // That will only succeed in the case that the current thread holds the write-access to this lock already which caused + // the previous attempt to lock for reading to fail (by design of the QtReadWriteLock). + // As we are in the thread with the write-access, it means that this threads has asked for read-access on top of it which we will + // grant (in contrast of QtReadLocker) because if you have the permission to change something you surely should have permission + // to read it. This assumes that the thread won't try to read data it temporarily has corrupted. + if (!m_lock->tryLockForWrite()) { + // If we couldn't lock for write at this point, it means another thread has write-access granted by the lock so we'll have to wait + // in order to gain regular read-access as would be with QtReadLocker + m_lock->lockForRead(); + } + } + + m_unlocked = false; +} + +PluginReadLocker::~PluginReadLocker() { + if (m_lock && !m_unlocked) { + // unlock the lock if it isn't nullptr + m_lock->unlock(); + } +} diff --git a/src/mumble/Plugin.h b/src/mumble/Plugin.h new file mode 100644 index 00000000000..049e622f242 --- /dev/null +++ b/src/mumble/Plugin.h @@ -0,0 +1,417 @@ +// Copyright 2021 The Mumble Developers. All rights reserved. +// Use of this source code is governed by a BSD-style license +// that can be found in the LICENSE file at the root of the +// Mumble source tree or at . + +#ifndef MUMBLE_MUMBLE_PLUGIN_H_ +#define MUMBLE_MUMBLE_PLUGIN_H_ + +#include "MumbleAPI_v_1_0_x.h" +#include "PluginComponents_v_1_0_x.h" +#include "PositionalData.h" +#include "MumblePlugin_v_1_0_x.h" + +#include +#include +#include +#include +#include +#include + +#include +#include + +/// A struct for holding the function pointers to the functions inside the plugin's library +/// For the documentation of those functions, see the plugin's header file (the one used when developing a plugin) +struct MumblePluginFunctions { + decltype(&mumble_init) init; + decltype(&mumble_shutdown) shutdown; + decltype(&mumble_getName) getName; + decltype(&mumble_getAPIVersion) getAPIVersion; + decltype(&mumble_registerAPIFunctions) registerAPIFunctions; + decltype(&mumble_releaseResource) releaseResource; + + // Further utility functions the plugin may implement + decltype(&mumble_setMumbleInfo) setMumbleInfo; + decltype(&mumble_getVersion) getVersion; + decltype(&mumble_getAuthor) getAuthor; + decltype(&mumble_getDescription) getDescription; + decltype(&mumble_getFeatures) getFeatures; + decltype(&mumble_deactivateFeatures) deactivateFeatures; + + // Functions for dealing with positional audio (or rather the fetching of the needed data) + decltype(&mumble_initPositionalData) initPositionalData; + decltype(&mumble_fetchPositionalData) fetchPositionalData; + decltype(&mumble_shutdownPositionalData) shutdownPositionalData; + + // Callback functions and EventHandlers + decltype(&mumble_onServerConnected) onServerConnected; + decltype(&mumble_onServerDisconnected) onServerDisconnected; + decltype(&mumble_onChannelEntered) onChannelEntered; + decltype(&mumble_onChannelExited) onChannelExited; + decltype(&mumble_onUserTalkingStateChanged) onUserTalkingStateChanged; + decltype(&mumble_onReceiveData) onReceiveData; + decltype(&mumble_onAudioInput) onAudioInput; + decltype(&mumble_onAudioSourceFetched) onAudioSourceFetched; + decltype(&mumble_onAudioOutputAboutToPlay) onAudioOutputAboutToPlay; + decltype(&mumble_onServerSynchronized) onServerSynchronized; + decltype(&mumble_onUserAdded) onUserAdded; + decltype(&mumble_onUserRemoved) onUserRemoved; + decltype(&mumble_onChannelAdded) onChannelAdded; + decltype(&mumble_onChannelRemoved) onChannelRemoved; + decltype(&mumble_onChannelRenamed) onChannelRenamed; + decltype(&mumble_onKeyEvent) onKeyEvent; + + // Plugin updates + decltype(&mumble_hasUpdate) hasUpdate; + decltype(&mumble_getUpdateDownloadURL) getUpdateDownloadURL; +}; + + +/// An exception that is being thrown by a plugin whenever it encounters an error +class PluginError : public std::runtime_error { + public: + // inherit constructors of runtime_error + using std::runtime_error::runtime_error; +}; + + +/// An implementation similar to QReadLocker except that this one allows to lock on a lock the same thread is already +/// holding a write-lock on. This could also result in obtaining a write-lock though so it shouldn't be used for code regions +/// that take quite some time and rely on other readers still having access to the locked object. +class PluginReadLocker { + protected: + /// The lock this lock-guard is acting upon + QReadWriteLock *m_lock; + /// A flag indicating whether the lock has been unlocked (manually) and thus doesn't have to be unlocked + /// in the destructor. + bool m_unlocked; + public: + /// Constructor of the PluginReadLocker. If the passed lock-pointer is not nullptr, the constructor will + /// already lock the provided lock. + /// + /// @param lock A pointer to the QReadWriteLock that shall be managed by this object. May be nullptr + PluginReadLocker(QReadWriteLock *lock); + /// Locks this lock again after it has been unlocked before (Locking a locked lock results in a runtime error) + void relock(); + /// Unlocks this lock + void unlock(); + ~PluginReadLocker(); +}; + +class Plugin; + +/// Typedef for the plugin ID +typedef uint32_t plugin_id_t; +/// Typedef for a plugin pointer +typedef std::shared_ptr plugin_ptr_t; +/// Typedef for a const plugin pointer +typedef std::shared_ptr const_plugin_ptr_t; + +/// A class representing a plugin library attached to Mumble. It can be used to manage (load/unload) and access plugin libraries. +class Plugin : public QObject { + friend class PluginManager; + friend class PluginConfig; + + private: + Q_OBJECT + Q_DISABLE_COPY(Plugin) + protected: + /// A mutex guarding Plugin::nextID + static QMutex s_idLock; + /// The ID of the plugin that will be loaded next. Whenever accessing this field, Plugin::idLock should be locked. + static plugin_id_t s_nextID; + + /// Constructor of the Plugin. + /// + /// @param path The path to the plugin's shared library file. This path has to exist unless isBuiltIn is true + /// @param isBuiltIn A flag indicating that this is a plugin built into Mumble itself and is does not backed by a shared library + /// @param p A pointer to a QObject representing the parent of this object or nullptr if there is no parent + Plugin(QString path, bool isBuiltIn = false, QObject *p = nullptr); + + /// A flag indicating whether this plugin is valid. It is mainly used throughout the plugin's initialization. + bool m_pluginIsValid; + /// The QLibrary representing the shared library of this plugin + QLibrary m_lib; + /// The path to the shared library file in the host's filesystem + QString m_pluginPath; + /// The unique ID of this plugin. Note though that this ID is not suitable for uniquely identifying this plugin between restarts of Mumble + /// (not even between rescans of the plugins) let alone across clients. + plugin_id_t m_pluginID; + // a flag indicating whether this plugin has been loaded by calling its init function. + bool m_pluginIsLoaded; + /// The lock guarding this plugin object. Every time a member is accessed this lock should be locked accordingly. + /// After successful construction and initialization (doInitilize()), this member variable is effectively const + /// and therefore no locking is required in order to read from it! + /// In fact protecting read-accesses by a non-recursive lock can introduce deadlocks by plugins using certain + /// API functions. + mutable QReadWriteLock m_pluginLock; + /// The struct holding the function pointers to the functions in the shared library. + MumblePluginFunctions m_pluginFnc; + /// A flag indicating whether this plugin is built into Mumble and is thus not represented by a shared library. + bool m_isBuiltIn; + /// A flag indicating whether positional data gathering is enabled for this plugin (Enabled as in allowed via preferences). + bool m_positionalDataIsEnabled; + /// A flag indicating whether positional data gathering is currently active (Active as in running) + bool m_positionalDataIsActive; + /// A flag indicating whether this plugin has permission to monitor keyboard events that occur while + /// Mumble has the keyboard focus. + bool m_mayMonitorKeyboard; + + + QString extractWrappedString(MumbleStringWrapper wrapper) const; + + + // Most of this class's functions are protected in order to only allow access to them via the PluginManager + // as some require additional handling before/after calling them. + + /// Initializes this plugin. This function must be called directly after construction. This is guaranteed when the + /// plugin is created via Plugin::createNew + virtual bool doInitialize(); + /// Resolves the function pointers in the shared library and sets the respective fields in Plugin::apiFnc + virtual void resolveFunctionPointers(); + /// Enables positional data gathering for this plugin (as in allowing) + /// + /// @param enable Whether to enable the data gathering + virtual void enablePositionalData(bool enable = true); + /// Allows or forbids the monitoring of keyboard events for this plugin. + /// + /// @param allow Whether to allow or forbid it + virtual void allowKeyboardMonitoring(bool allow); + + + /// Initializes this plugin + virtual mumble_error_t init(); + /// Shuts this plugin down + virtual void shutdown(); + /// Delegates the struct of API function pointers to the plugin backend + /// + /// @param api The pointer to the API struct + virtual void registerAPIFunctions(void *api) const; + /// Asks the plugin to release (free/delete) the resource pointed to by the given pointer + /// + /// @param pointer Pointer to the resource + virtual void releaseResource(const void *pointer) const; + /// Provides the plugin backend with some version information about Mumble + /// + /// @param mumbleVersion The version of the Mumble client + /// @param mumbleAPIVersion The API version used by the Mumble client + /// @param minimalExpectedAPIVersion The minimal API version expected to be used by the plugin backend + virtual void setMumbleInfo(mumble_version_t mumbleVersion, mumble_version_t mumbleAPIVersion, mumble_version_t minimalExpectedAPIVersion) const; + /// Asks the plugin to deactivate certain features + /// + /// @param features The feature list or'ed together + /// @returns The list of features that couldn't be deactivated or'ed together + virtual uint32_t deactivateFeatures(uint32_t features) const; + /// Shows an about-dialog + /// + /// @parent A pointer to the QWidget that should be used as a parent + /// @returns Whether the dialog could be shown successfully + virtual bool showAboutDialog(QWidget *parent) const; + /// Shows a config-dialog + /// + /// @parent A pointer to the QWidget that should be used as a parent + /// @returns Whether the dialog could be shown successfully + virtual bool showConfigDialog(QWidget *parent) const; + /// Initializes the positional data gathering + /// + /// @params programNames A pointer to an array of const char* representing the program names + /// @params programCount A pointer to an array of PIDs corresponding to the program names + /// @params programCount The length of the two previous arrays + virtual uint8_t initPositionalData(const char *const*programNames, const uint64_t *programPIDs, size_t programCount); + /// Fetches the positional data + /// + /// @param[out] avatarPos The position of the ingame avatar (player) + /// @param[out] avatarDir The directiion in which the avatar (player) is looking/facing + /// @param[out] avatarAxis The vector from the avatar's toes to its head + /// @param[out] cameraPos The position of the ingame camera + /// @param[out] cameraDir The direction in which the camera is looking/facing + /// @param[out] cameraAxis The vector from the camera's bottom to its top + /// @param[out] context The context of the current game-session (includes server/squad info) + /// @param[out] identity The ingame identity of the player (name) + virtual bool fetchPositionalData(Position3D& avatarPos, Vector3D& avatarDir, Vector3D& avatarAxis, Position3D& cameraPos, Vector3D& cameraDir, + Vector3D& cameraAxis, QString& context, QString& identity) const; + /// Shuts down positional data gathering + virtual void shutdownPositionalData(); + /// Called to indicate that the client has connected to a server + /// + /// @param connection An object used to identify the current connection + virtual void onServerConnected(mumble_connection_t connection) const; + /// Called to indicate that the client disconnected from a server + /// + /// @param connection An object used to identify the connection that has been disconnected + virtual void onServerDisconnected(mumble_connection_t connection) const; + /// Called to indicate that a user has switched its channel + /// + /// @param connection An object used to identify the current connection + /// @param userID The ID of the user that switched channel + /// @param previousChannelID The ID of the channel the user came from (-1 if there is no previous channel) + /// æparam newChannelID The ID of the channel the user has switched to + virtual void onChannelEntered(mumble_connection_t connection, mumble_userid_t userID, mumble_channelid_t previousChannelID, + mumble_channelid_t newChannelID) const; + /// Called to indicate that a user exited a channel. + /// + /// @param connection An object used to identify the current connection + /// @param userID The ID of the user that switched channel + /// @param channelID The ID of the channel the user exited + virtual void onChannelExited(mumble_connection_t connection, mumble_userid_t userID, mumble_channelid_t channelID) const; + /// Called to indicate that a user has changed its talking state + /// + /// @param connection An object used to identify the current connection + /// @param userID The ID of the user that switched channel + /// @param talkingState The new talking state of the user + virtual void onUserTalkingStateChanged(mumble_connection_t connection, mumble_userid_t userID, mumble_talking_state_t talkingState) const; + /// Called to indicate that a data packet has been received + /// + /// @param connection An object used to identify the current connection + /// @param sender The ID of the user whose client sent the data + /// @param data The actual data + /// @param dataLength The length of the data array + /// @param datID The ID of the data used to determine whether this plugin handles this data or not + /// @returns Whether this plugin handled the data + virtual bool onReceiveData(mumble_connection_t connection, mumble_userid_t sender, const uint8_t *data, size_t dataLength, const char *dataID) const; + /// Called to indicate that there is audio input + /// + /// @param inputPCM A pointer to a short array representing the input PCM + /// @param sampleCount The amount of samples per channel + /// @param channelCount The amount of channels in the PCM + /// @param sampleRate The used sample rate in Hz + /// @param isSpeech Whether Mumble considers the input as speech + /// @returns Whether this pluign has modified the audio + virtual bool onAudioInput(short *inputPCM, uint32_t sampleCount, uint16_t channelCount, uint32_t sampleRate, bool isSpeech) const; + /// Called to indicate that an audio source has been fetched + /// + /// @param outputPCM A pointer to a short array representing the output PCM + /// @param sampleCount The amount of samples per channel + /// @param channelCount The amount of channels in the PCM + /// @param sampleRate The used sample rate in Hz + /// @param isSpeech Whether Mumble considers the output as speech + /// @param userID The ID of the user responsible for the output (only relevant if isSpeech == true) + /// @returns Whether this pluign has modified the audio + virtual bool onAudioSourceFetched(float *outputPCM, uint32_t sampleCount, uint16_t channelCount, uint32_t sampleRate, bool isSpeech, mumble_userid_t userID) const; + /// Called to indicate that audio is about to be played + /// + /// @param outputPCM A pointer to a short array representing the output PCM + /// @param sampleCount The amount of samples per channel + /// @param channelCount The amount of channels in the PCM + /// @param sampleRate The used sample rate in Hz + /// @returns Whether this pluign has modified the audio + virtual bool onAudioOutputAboutToPlay(float *outputPCM, uint32_t sampleCount, uint16_t channelCount, uint32_t sampleRate) const; + /// Called when the server has synchronized with the client + /// + /// @param connection An object used to identify the current connection + virtual void onServerSynchronized(mumble_connection_t connection) const; + /// Called when a new user gets added to the user model. This is the case when that new user freshly connects to the server the + /// local user is on but also when the local user connects to a server other clients are already connected to (in this case this + /// method will be called for every client already on that server). + /// + /// @param connection An object used to identify the current connection + /// @param userID The ID of the user that has been added + virtual void onUserAdded(mumble_connection_t connection, mumble_userid_t userID) const; + /// Called when a user gets removed from the user model. This is the case when that user disconnects from the server the + /// local user is on but also when the local user disconnects from a server other clients are connected to (in this case this + /// method will be called for every client on that server). + /// + /// @param connection An object used to identify the current connection + /// @param userID The ID of the user that has been removed + virtual void onUserRemoved(mumble_connection_t connection, mumble_userid_t userID) const; + /// Called when a new channel gets added to the user model. This is the case when a new channel is created on the server the local + /// user is on but also when the local user connects to a server that contains channels other than the root-channel (in this case + /// this method will be called for ever non-root channel on that server). + /// + /// @param connection An object used to identify the current connection + /// @param channelID The ID of the channel that has been added + virtual void onChannelAdded(mumble_connection_t connection, mumble_channelid_t channelID) const; + /// Called when a channel gets removed from the user model. This is the case when a channel is removed on the server the local + /// user is on but also when the local user disconnects from a server that contains channels other than the root-channel (in this case + /// this method will be called for ever non-root channel on that server). + /// + /// @param connection An object used to identify the current connection + /// @param channelID The ID of the channel that has been removed + virtual void onChannelRemoved(mumble_connection_t connection, mumble_channelid_t channelID) const; + /// Called when a channel gets renamed. This also applies when a new channel is created (thus assigning it an initial name is + /// also considered renaming). + /// + /// @param connection An object used to identify the current connection + /// @param channelID The ID of the channel that has been renamed + virtual void onChannelRenamed(mumble_connection_t connection, mumble_channelid_t channelID) const; + /// Called when a key has been pressed or released while Mumble has keyboard focus. + /// + /// @param keyCode The key code of the respective key. The character codes are defined + /// via the KeyCode enum. For printable 7-bit ASCII characters these codes conform + /// to the ASCII code-page with the only difference that case is not distinguished. Therefore + /// always the upper-case letter code will be used for letters. + /// @param wasPress Whether the key has been pressed (instead of released) + virtual void onKeyEvent(mumble_keycode_t keyCode, bool wasPress) const; + + + public: + /// A template function for instantiating new plugin objects and initializing them. The plugin will be allocated on the heap and has + /// thus to be deleted via the delete instruction. + /// + /// @tparam T The type of the plugin to be instantiated + /// @tparam Ts The types of the contructor arguments + /// @param args A list of args passed to the contructor of the plugin object + /// @returns A pointer to the allocated plugin + /// + /// @throws PluginError if the plugin could not be loaded + template + static T* createNew(Ts&&...args) { + static_assert(std::is_base_of::value, "The Plugin::create() can only be used to instantiate objects of base-type Plugin"); + static_assert(!std::is_pointer::value, "Plugin::create() can't be used to instantiate pointers. It will return a pointer automatically"); + + T *instancePtr = new T(std::forward(args)...); + + // call the initialize-method and throw an exception of it doesn't succeed + if (!instancePtr->doInitialize()) { + delete instancePtr; + // Delete the constructed object to prevent a memory leak + throw PluginError("Failed to initialize plugin"); + } + + return instancePtr; + } + + /// Destructor + virtual ~Plugin() Q_DECL_OVERRIDE; + /// @returns Whether this plugin is in a valid state + virtual bool isValid() const; + /// @returns Whether this plugin is loaded (has been initialized via Plugin::init()) + virtual bool isLoaded() const Q_DECL_FINAL; + /// @returns The unique ID of this plugin. This ID holds only as long as this plugin isn't "reconstructed". + virtual plugin_id_t getID() const Q_DECL_FINAL; + /// @returns Whether this plugin is built into Mumble (thus not backed by a shared library) + virtual bool isBuiltInPlugin() const Q_DECL_FINAL; + /// @returns The path to the shared library in the host's filesystem + virtual QString getFilePath() const; + /// @returns Whether positional data gathering is enabled (as in allowed via preferences) + virtual bool isPositionalDataEnabled() const Q_DECL_FINAL; + /// @returns Whether positional data gathering is currently active (as in running) + virtual bool isPositionalDataActive() const Q_DECL_FINAL; + /// @returns Whether this plugin is currently allowed to monitor keyboard events + virtual bool isKeyboardMonitoringAllowed() const Q_DECL_FINAL; + + + /// @returns Whether this plugin provides an about-dialog + virtual bool providesAboutDialog() const; + /// @returns Whether this plugin provides an config-dialog + virtual bool providesConfigDialog() const; + /// @returns The name of this plugin + virtual QString getName() const; + /// @returns The API version this plugin intends to use + virtual mumble_version_t getAPIVersion() const; + /// @returns The version of this plugin + virtual mumble_version_t getVersion() const; + /// @returns The author of this plugin + virtual QString getAuthor() const; + /// @returns The plugin's description + virtual QString getDescription() const; + /// @returns The plugin's features or'ed together (See the PluginFeature enum in MumblePlugin.h for what features are available) + virtual uint32_t getFeatures() const; + /// @return Whether the plugin has found a new/updated version of itself available for download + virtual bool hasUpdate() const; + /// @return The URL to download the updated plugin. May be empty + virtual QUrl getUpdateDownloadURL() const; +}; + +#endif diff --git a/src/mumble/PluginConfig.cpp b/src/mumble/PluginConfig.cpp new file mode 100644 index 00000000000..e708770676e --- /dev/null +++ b/src/mumble/PluginConfig.cpp @@ -0,0 +1,247 @@ +// Copyright 2021 The Mumble Developers. All rights reserved. +// Use of this source code is governed by a BSD-style license +// that can be found in the LICENSE file at the root of the +// Mumble source tree or at . + +#include "PluginConfig.h" + +#include "Log.h" +#include "MainWindow.h" +#include "Message.h" +#include "ServerHandler.h" +#include "WebFetch.h" +#include "MumbleApplication.h" +#include "ManualPlugin.h" +#include "Utils.h" +#include "PluginInstaller.h" +#include "PluginManager.h" + +#include +#include +#include +#include +#include +#include "Global.h" + +const QString PluginConfig::name = QLatin1String("PluginConfig"); + +static ConfigWidget *PluginConfigDialogNew(Settings &st) { + return new PluginConfig(st); +} + +static ConfigRegistrar registrarPluginConfig(5000, PluginConfigDialogNew); + + +PluginConfig::PluginConfig(Settings &st) : ConfigWidget(st) { + setupUi(this); + + qtwPlugins->header()->setSectionResizeMode(0, QHeaderView::Stretch); + qtwPlugins->header()->setSectionResizeMode(1, QHeaderView::ResizeToContents); + qtwPlugins->header()->setSectionResizeMode(2, QHeaderView::ResizeToContents); + qtwPlugins->header()->setSectionResizeMode(3, QHeaderView::ResizeToContents); + + qpbUnload->setEnabled(false); + + refillPluginList(); +} + +QString PluginConfig::title() const { + return tr("Plugins"); +} + +const QString &PluginConfig::getName() const { + return PluginConfig::name; +} + +QIcon PluginConfig::icon() const { + return QIcon(QLatin1String("skin:config_plugin.png")); +} + +void PluginConfig::load(const Settings &r) { + loadCheckBox(qcbTransmit, r.bTransmitPosition); +} + +void PluginConfig::on_qpbInstallPlugin_clicked() { + QString pluginFile = QFileDialog::getOpenFileName(this, tr("Install plugin..."), QDir::homePath()); + + if (pluginFile.isEmpty()) { + return; + } + + try { + PluginInstaller installer(pluginFile, this); + if (installer.exec() == QDialog::Accepted) { + // Reload plugins so the new one actually shows up + on_qpbReload_clicked(); + + QMessageBox::information(this, "Mumble", tr("The plugin was installed successfully"), QMessageBox::Ok, QMessageBox::NoButton); + } + } catch (const PluginInstallException &e) { + QMessageBox::critical(this, "Mumble", e.getMessage(), QMessageBox::Ok, QMessageBox::NoButton); + } +} + +void PluginConfig::save() const { + s.bTransmitPosition = qcbTransmit->isChecked(); + s.qhPluginSettings.clear(); + + if (!s.bTransmitPosition) { + // Make sure that if posData is currently running, it gets reset + // The setting will prevent the system from reactivating + Global::get().pluginManager->unlinkPositionalData(); + } + + constexpr int enableCol = 1; + constexpr int positionalDataCol = 2; + constexpr int keyboardMonitorCol = 3; + + QList list = qtwPlugins->findItems(QString(), Qt::MatchContains); + for(QTreeWidgetItem *i : list) { + + bool enable = (i->checkState(enableCol) == Qt::Checked); + bool positionalDataEnabled = (i->checkState(positionalDataCol) == Qt::Checked); + bool keyboardMonitoringEnabled = (i->checkState(keyboardMonitorCol) == Qt::Checked); + + const_plugin_ptr_t plugin = pluginForItem(i); + if (plugin) { + // insert plugin to settings + Global::get().pluginManager->enablePositionalDataFor(plugin->getID(), positionalDataEnabled); + Global::get().pluginManager->allowKeyboardMonitoringFor(plugin->getID(), keyboardMonitoringEnabled); + + if (enable) { + if (Global::get().pluginManager->loadPlugin(plugin->getID())) { + // potentially deactivate plugin features + // A plugin's feature is considered to be enabled by default after loading. Thus we only need to + // deactivate the ones we don't want + uint32_t featuresToDeactivate = FEATURE_NONE; + const uint32_t pluginFeatures = plugin->getFeatures(); + + if (!positionalDataEnabled && (pluginFeatures & FEATURE_POSITIONAL)) { + // deactivate this feature only if it is available in the first place + featuresToDeactivate |= FEATURE_POSITIONAL; + } + + if (featuresToDeactivate != FEATURE_NONE) { + uint32_t remainingFeatures = Global::get().pluginManager->deactivateFeaturesFor(plugin->getID(), featuresToDeactivate); + + if (remainingFeatures != FEATURE_NONE) { + Global::get().l->log(Log::Warning, tr("Unable to deactivate all requested features for plugin \"%1\"").arg(plugin->getName())); + } + } + } else { + // loading failed + enable = false; + Global::get().l->log(Log::Warning, tr("Unable to load plugin \"%1\"").arg(plugin->getName())); + } + } else { + Global::get().pluginManager->unloadPlugin(plugin->getID()); + } + + QString pluginKey = QLatin1String(QCryptographicHash::hash(plugin->getFilePath().toUtf8(), QCryptographicHash::Sha1).toHex()); + s.qhPluginSettings.insert(pluginKey, { plugin->getFilePath(), enable, positionalDataEnabled, keyboardMonitoringEnabled }); + } + } +} + +const_plugin_ptr_t PluginConfig::pluginForItem(QTreeWidgetItem *i) const { + if (i) { + return Global::get().pluginManager->getPlugin(i->data(0, Qt::UserRole).toUInt()); + } + + return nullptr; +} + +void PluginConfig::on_qpbConfig_clicked() { + const_plugin_ptr_t plugin = pluginForItem(qtwPlugins->currentItem()); + + if (plugin) { + if (!plugin->showConfigDialog(this)) { + // if the plugin doesn't support showing such a dialog, we'll show a default one + QMessageBox::information(this, QLatin1String("Mumble"), tr("Plugin has no configure function."), QMessageBox::Ok, QMessageBox::NoButton); + } + } +} + +void PluginConfig::on_qpbAbout_clicked() { + const_plugin_ptr_t plugin = pluginForItem(qtwPlugins->currentItem()); + + if (plugin) { + if (!plugin->showAboutDialog(this)) { + // if the plugin doesn't support showing such a dialog, we'll show a default one + QMessageBox::information(this, QLatin1String("Mumble"), tr("Plugin has no about function."), QMessageBox::Ok, QMessageBox::NoButton); + } + } +} + +void PluginConfig::on_qpbReload_clicked() { + Global::get().pluginManager->rescanPlugins(); + refillPluginList(); +} + +void PluginConfig::on_qpbUnload_clicked() { + QTreeWidgetItem *currentItem = qtwPlugins->currentItem(); + if (!currentItem) { + return; + } + + const_plugin_ptr_t plugin = pluginForItem(currentItem); + if (!plugin) { + return; + } + + if (Global::get().pluginManager->clearPlugin(plugin->getID())) { + // Plugin was successfully cleared + currentItem = qtwPlugins->takeTopLevelItem(qtwPlugins->indexOfTopLevelItem(currentItem)); + + delete currentItem; + } else { + qWarning("PluginConfig.cpp: Failed to delete unloaded plugin entry"); + } +} + +void PluginConfig::refillPluginList() { + qtwPlugins->clear(); + + // get plugins already sorted according to their name + const QVector plugins = Global::get().pluginManager->getPlugins(true); + + foreach(const_plugin_ptr_t currentPlugin, plugins) { + QTreeWidgetItem *i = new QTreeWidgetItem(qtwPlugins); + i->setFlags(Qt::ItemIsUserCheckable | Qt::ItemIsEnabled | Qt::ItemIsSelectable); + i->setCheckState(1, currentPlugin->isLoaded() ? Qt::Checked : Qt::Unchecked); + + if (currentPlugin->getFeatures() & FEATURE_POSITIONAL) { + i->setCheckState(2, currentPlugin->isPositionalDataEnabled() ? Qt::Checked : Qt::Unchecked); + i->setToolTip(2, tr("Whether the positional audio feature of this plugin should be enabled")); + } else { + i->setToolTip(2, tr("This plugin does not provide support for positional audio")); + } + + i->setCheckState(3, currentPlugin->isKeyboardMonitoringAllowed() ? Qt::Checked : Qt::Unchecked); + i->setToolTip(3, tr("Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus")); + + i->setText(0, currentPlugin->getName()); + i->setToolTip(0, currentPlugin->getDescription().toHtmlEscaped()); + i->setToolTip(1, tr("Whether this plugin should be enabled")); + i->setData(0, Qt::UserRole, currentPlugin->getID()); + } + + qtwPlugins->setCurrentItem(qtwPlugins->topLevelItem(0)); + on_qtwPlugins_currentItemChanged(qtwPlugins->topLevelItem(0), NULL); +} + +void PluginConfig::on_qtwPlugins_currentItemChanged(QTreeWidgetItem *current, QTreeWidgetItem *) { + const_plugin_ptr_t plugin = pluginForItem(current); + + if (plugin) { + qpbAbout->setEnabled(plugin->providesAboutDialog()); + + qpbConfig->setEnabled(plugin->providesConfigDialog()); + + qpbUnload->setEnabled(true); + } else { + qpbAbout->setEnabled(false); + qpbConfig->setEnabled(false); + qpbUnload->setEnabled(false); + } +} diff --git a/src/mumble/PluginConfig.h b/src/mumble/PluginConfig.h new file mode 100644 index 00000000000..cab9ce7b321 --- /dev/null +++ b/src/mumble/PluginConfig.h @@ -0,0 +1,66 @@ +// Copyright 2021 The Mumble Developers. All rights reserved. +// Use of this source code is governed by a BSD-style license +// that can be found in the LICENSE file at the root of the +// Mumble source tree or at . + +#ifndef MUMBLE_MUMBLE_PLUGINS_H_ +#define MUMBLE_MUMBLE_PLUGINS_H_ + +#include "ConfigDialog.h" +#include "ui_PluginConfig.h" +#include "Plugin.h" + +#include +#include +#include + +struct PluginInfo; + +class PluginConfig : public ConfigWidget, public Ui::PluginConfig { + private: + Q_OBJECT + Q_DISABLE_COPY(PluginConfig) + protected: + /// Clears and (re-) populates the plugin list in the UI with the currently available plugins + void refillPluginList(); + /// @param item The QTreeWidgetItem to retrieve the plugin for + /// @returns The plugin corresponding to the provided item + const_plugin_ptr_t pluginForItem(QTreeWidgetItem *item) const; + public: + /// The unique name of this ConfigWidget + static const QString name; + /// Constructor + /// + /// @param st The settings object to work on + PluginConfig(Settings &st); + /// @returns The title of this widget + virtual QString title() const Q_DECL_OVERRIDE; + /// @returns The name of this ConfigWidget + const QString &getName() const Q_DECL_OVERRIDE; + /// @returns The icon for this widget + virtual QIcon icon() const Q_DECL_OVERRIDE; + public slots: + /// Saves the current configuration to the respective settings object + void save() const Q_DECL_OVERRIDE; + /// Loads the transmit-position from the provided settings object + /// + /// @param The setting sobject to read from + void load(const Settings &r) Q_DECL_OVERRIDE; + /// Slot triggered when the install-button in the UI has been clicked + void on_qpbInstallPlugin_clicked(); + /// Slot triggered when the config-button in the UI has been clicked + void on_qpbConfig_clicked(); + /// Slot triggered when the about-button in the UI has been clicked + void on_qpbAbout_clicked(); + /// Slot triggered when the reload-button in the UI has been clicked + void on_qpbReload_clicked(); + /// Slot triggered when the unload-button in the UI has been clicked + void on_qpbUnload_clicked(); + /// Slot triggered when the selection in the plugin list hast changed + /// + /// @param current The currently selected item + /// @param old The previously selected item (if applicable - otherwise NULL/nullptr) + void on_qtwPlugins_currentItemChanged(QTreeWidgetItem *current, QTreeWidgetItem *old); +}; + +#endif diff --git a/src/mumble/Plugins.ui b/src/mumble/PluginConfig.ui similarity index 77% rename from src/mumble/Plugins.ui rename to src/mumble/PluginConfig.ui index b634d3f0b24..fff242cac75 100644 --- a/src/mumble/Plugins.ui +++ b/src/mumble/PluginConfig.ui @@ -6,14 +6,14 @@ 0 0 - 321 - 235 + 570 + 289 Plugins - + @@ -53,9 +53,6 @@ false - - false - Name @@ -63,7 +60,17 @@ - Enabled + Enable + + + + + PA + + + + + KeyEvents @@ -83,6 +90,16 @@ + + + + Install a plugin from a local file + + + Install plugin... + + + @@ -122,6 +139,16 @@ + + + + Unload the currently selected plugin. This will remove it from the plugin list for the current session. + + + Unload + + + diff --git a/src/mumble/PluginInstaller.cpp b/src/mumble/PluginInstaller.cpp new file mode 100644 index 00000000000..41494d64edb --- /dev/null +++ b/src/mumble/PluginInstaller.cpp @@ -0,0 +1,200 @@ +// Copyright 2021 The Mumble Developers. All rights reserved. +// Use of this source code is governed by a BSD-style license +// that can be found in the LICENSE file at the root of the +// Mumble source tree or at . + +#include "PluginInstaller.h" +#include "Global.h" + +#include +#include +#include +#include +#include + +#include + +#include +#include +#include + +#include +#include +#include +#include + +PluginInstallException::PluginInstallException(const QString& msg) + : m_msg(msg) { +} + +QString PluginInstallException::getMessage() const { + return m_msg; +} + +const QString PluginInstaller::pluginFileExtension = QLatin1String("mumble_plugin"); + +bool PluginInstaller::canBePluginFile(const QFileInfo& fileInfo) noexcept { + if (!fileInfo.isFile()) { + // A plugin file has to be a file (obviously) + return false; + } + + if (fileInfo.suffix().compare(PluginInstaller::pluginFileExtension, Qt::CaseInsensitive) == 0 + || fileInfo.suffix().compare(QLatin1String("zip"), Qt::CaseInsensitive) == 0) { + // A plugin file has either the extension given in PluginInstaller::pluginFileExtension or zip + return true; + } + + // We might also accept a shared library directly + return QLibrary::isLibrary(fileInfo.fileName()); +} + +PluginInstaller::PluginInstaller(const QFileInfo& fileInfo, QWidget *p) + : QDialog(p), + m_pluginArchive(fileInfo), + m_plugin(nullptr), + m_pluginSource(), + m_pluginDestination(), + m_copyPlugin(false) { + setupUi(this); + + setWindowIcon(QIcon(QLatin1String("skin:mumble.svg"))); + + QObject::connect(qpbYes, &QPushButton::clicked, this, &PluginInstaller::on_qpbYesClicked); + QObject::connect(qpbNo, &QPushButton::clicked, this, &PluginInstaller::on_qpbNoClicked); + + init(); +} + +PluginInstaller::~PluginInstaller() { + if (m_plugin) { + delete m_plugin; + } +} + +void PluginInstaller::init() { + if (!PluginInstaller::canBePluginFile(m_pluginArchive)) { + throw PluginInstallException(tr("The file \"%1\" is not a valid plugin file!").arg(m_pluginArchive.fileName())); + } + + if (QLibrary::isLibrary(m_pluginArchive.fileName())) { + // For a library the fileInfo provided is already the actual plugin library + m_pluginSource = m_pluginArchive; + + m_copyPlugin = true; + } else { + // We have been provided with a zip-file + try { + std::ifstream zipInput(m_pluginArchive.filePath().toStdString()); + Poco::Zip::ZipArchive archive(zipInput); + + // Iterate over all files in the archive to see which ones could be the correct plugin library + QString pluginName; + auto it = archive.fileInfoBegin(); + while (it != archive.fileInfoEnd()) { + QString currentFileName = QString::fromStdString(it->first); + if (QLibrary::isLibrary(currentFileName)) { + if (!pluginName.isEmpty()) { + // There seem to be multiple plugins in here. That's not allowed + throw PluginInstallException(tr("Found more than one plugin library for the current OS in \"%1\" (\"%2\" and \"%3\")!").arg( + m_pluginArchive.fileName()).arg(pluginName).arg(currentFileName)); + } + + pluginName = currentFileName; + } + + it++; + } + + if (pluginName.isEmpty()) { + throw PluginInstallException(tr("Unable to find a plugin for the current OS in \"%1\"").arg(m_pluginArchive.fileName())); + } + + // Unpack the plugin library into the tmp dir + // We don't have to create the directory structure as we're only interested in the library itself + QString tmpPluginPath = QDir::temp().filePath(QFileInfo(pluginName).fileName()); + auto pluginIt = archive.findHeader(pluginName.toStdString()); + zipInput.clear(); + Poco::Zip::ZipInputStream zipin(zipInput, pluginIt->second); + std::ofstream out(tmpPluginPath.toStdString()); + Poco::StreamCopier::copyStream(zipin, out); + + m_pluginSource = QFileInfo(tmpPluginPath); + } catch(const Poco::Exception &e) { + // Something didn't work out during the Zip processing + throw PluginInstallException(QString::fromStdString(std::string("Failed to process zip archive: ") + e.message())); + } + } + + QString pluginFileName = m_pluginSource.fileName(); + + // Try to load the plugin up to see if it is actually valid + try { + m_plugin = Plugin::createNew(m_pluginSource.absoluteFilePath()); + } catch(const PluginError&) { + throw PluginInstallException(tr("Unable to load plugin \"%1\" - check the plugin interface!").arg(pluginFileName)); + } + + m_pluginDestination = QFileInfo(QString::fromLatin1("%1/%2").arg(getInstallDir()).arg(pluginFileName)); + + + // Now that we located the plugin, it is time to fill in its details in the UI + qlName->setText(m_plugin->getName()); + + mumble_version_t pluginVersion = m_plugin->getVersion(); + mumble_version_t usedAPIVersion = m_plugin->getAPIVersion(); + qlVersion->setText(QString::fromLatin1("%1 (API %2)").arg(pluginVersion == VERSION_UNKNOWN ? + "Unknown" : static_cast(pluginVersion)).arg( + usedAPIVersion == VERSION_UNKNOWN ? "Unknown" : static_cast(usedAPIVersion))); + + qlAuthor->setText(m_plugin->getAuthor()); + + qlDescription->setText(m_plugin->getDescription()); +} + +void PluginInstaller::install() const { + if (!m_plugin) { + // This function shouldn't even be called, if the plugin object has not been created... + throw PluginInstallException(QLatin1String("[INTERNAL ERROR]: Trying to install an invalid plugin")); + } + + if (m_pluginSource == m_pluginDestination) { + // Apparently the plugin is already installed + return; + } + + if (m_pluginDestination.exists()) { + // Delete old version first + if (!QFile(m_pluginDestination.absoluteFilePath()).remove()) { + throw PluginInstallException(tr("Unable to delete old plugin at \"%1\"").arg(m_pluginDestination.absoluteFilePath())); + } + } + + if (m_copyPlugin) { + if (!QFile(m_pluginSource.absoluteFilePath()).copy(m_pluginDestination.absoluteFilePath())) { + throw PluginInstallException(tr("Unable to copy plugin library from \"%1\" to \"%2\"").arg(m_pluginSource.absoluteFilePath()).arg( + m_pluginDestination.absoluteFilePath())); + } + } else { + // Move the plugin into the respective dir + if (!QFile(m_pluginSource.absoluteFilePath()).rename(m_pluginDestination.absoluteFilePath())) { + throw PluginInstallException(tr("Unable to move plugin library to \"%1\"").arg(m_pluginDestination.absoluteFilePath())); + } + } +} + +QString PluginInstaller::getInstallDir() { + // Get the path to the plugin-dir in "user-land" (aka: the user definitely has write access to this + // location). + return Global::get().qdBasePath.absolutePath() + QLatin1String("/Plugins"); +} + +void PluginInstaller::on_qpbYesClicked() { + install(); + + accept(); +} + +void PluginInstaller::on_qpbNoClicked() { + close(); +} diff --git a/src/mumble/PluginInstaller.h b/src/mumble/PluginInstaller.h new file mode 100644 index 00000000000..d8df3c303bd --- /dev/null +++ b/src/mumble/PluginInstaller.h @@ -0,0 +1,84 @@ +// Copyright 2021 The Mumble Developers. All rights reserved. +// Use of this source code is governed by a BSD-style license +// that can be found in the LICENSE file at the root of the +// Mumble source tree or at . + +#ifndef MUMBLE_MUMBLE_PLUGININSTALLER_H_ +#define MUMBLE_MUMBLE_PLUGININSTALLER_H_ + + +#include +#include + +#include "Plugin.h" + +#include "ui_PluginInstaller.h" + +/// An exception thrown by the PluginInstaller +class PluginInstallException : public QException { + protected: + /// The exception's message + QString m_msg; + public: + /// @param msg The message stating why this exception has been thrown + PluginInstallException(const QString& msg); + + /// @returns This exception's message + QString getMessage() const; +}; + +/// The PluginInstaller can be used to install plugins into Mumble. It verifies that the respective +/// plugin is functional and will automatiacally copy/move the plugin library to the respective +/// directory on the FileSystem. +class PluginInstaller : public QDialog, public Ui::PluginInstaller { + private: + Q_OBJECT; + Q_DISABLE_COPY(PluginInstaller); + protected: + /// The file the installer has been invoked on + QFileInfo m_pluginArchive; + /// A pointer to the plugin instance created from the plugin library that shall be installed + Plugin *m_plugin; + /// The actual plugin library file + QFileInfo m_pluginSource; + /// The destinaton file to which the plugin library shall be copied + QFileInfo m_pluginDestination; + /// A flag indicating that the plugin library shall be copied instead of moved in order + /// to install it. + bool m_copyPlugin; + + /// Initializes this installer by processing the provided plugin source and filling all + /// internal fields. This function is called from the constructor. + /// + /// @throws PluginInstallException If something isn't right or goes wrong + void init(); + public: + /// The "special" file-extension associated with Mumble plugins + static const QString pluginFileExtension; + + /// A helper function checking whether the provided file could be a plugin source + /// + /// @param fileInfo The file to check + /// @returns Whether the provided file could (!) be a plugin source + static bool canBePluginFile(const QFileInfo& fileInfo) noexcept; + + /// @param fileInfo The plugin source to process + /// + /// @throws PluginInstallException If something isn't right or goes wrong + PluginInstaller(const QFileInfo& fileInfo, QWidget *p = nullptr); + /// Destructor + ~PluginInstaller(); + + /// Performs the actual installation (moving/copying of the library) of the plugin + void install() const; + + static QString getInstallDir(); + + public slots: + /// Slot called when the user clicks the yes button + void on_qpbYesClicked(); + /// Slot called when the user clicks the no button + void on_qpbNoClicked(); +}; + +#endif // MUMBLE_MUMBLE_PLUGININSTALLER_H_ diff --git a/src/mumble/PluginInstaller.ui b/src/mumble/PluginInstaller.ui new file mode 100644 index 00000000000..39f575dea09 --- /dev/null +++ b/src/mumble/PluginInstaller.ui @@ -0,0 +1,243 @@ + + + PluginInstaller + + + + 0 + 0 + 360 + 332 + + + + PluginInstaller + + + false + + + false + + + + + + + 0 + 0 + + + + QFrame::NoFrame + + + You are about to install the plugin listed below. Do you wish to proceed? + + + true + + + + + + + Qt::Horizontal + + + + + + + QFrame::NoFrame + + + true + + + + + 0 + 0 + 348 + 208 + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + 12 + + + + + <html><head/><body><p><span style=" font-weight:600;">Name:</span></p></body></html> + + + + + + + + 0 + 0 + + + + + + + true + + + + + + + <html><head/><body><p><span style=" font-weight:600;">Version:</span></p></body></html> + + + + + + + + 0 + 0 + + + + + + + true + + + + + + + <html><head/><body><p><span style=" font-weight:600;">Author(s):</span></p></body></html> + + + + + + + + 0 + 0 + + + + + + + true + + + + + + + <html><head/><body><p><span style=" font-weight:600;">Description:</span></p></body></html> + + + + + + + + 0 + 0 + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + true + + + + + + + + + + + Qt::Horizontal + + + + + + + + 0 + + + 6 + + + 6 + + + + + + 80 + 30 + + + + &No + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 0 + 0 + + + + + 80 + 30 + + + + &Yes + + + + + + + + + + + diff --git a/src/mumble/PluginManager.cpp b/src/mumble/PluginManager.cpp new file mode 100644 index 00000000000..817eb69be5c --- /dev/null +++ b/src/mumble/PluginManager.cpp @@ -0,0 +1,933 @@ +// Copyright 2021 The Mumble Developers. All rights reserved. +// Use of this source code is governed by a BSD-style license +// that can be found in the LICENSE file at the root of the +// Mumble source tree or at . + +#include + +#include "PluginManager.h" +#include "LegacyPlugin.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "ManualPlugin.h" +#include "Log.h" +#include "PluginInstaller.h" +#include "ProcessResolver.h" +#include "ServerHandler.h" +#include "PluginUpdater.h" +#include "API.h" +#include "Global.h" + +#include + +#ifdef Q_OS_WIN + #include + #include +#endif + +#ifdef Q_OS_LINUX + #include +#endif + +PluginManager::PluginManager(QSet *additionalSearchPaths, QObject *p) + : QObject(p), + m_pluginCollectionLock(QReadWriteLock::NonRecursive), + m_pluginHashMap(), + m_positionalData(), + m_positionalDataCheckTimer(), + m_sentDataMutex(), + m_sentData(), + m_activePosDataPluginLock(QReadWriteLock::NonRecursive), + m_activePositionalDataPlugin(), + m_updater() { + + // Setup search-paths + if (additionalSearchPaths) { + for (const auto ¤tPath : *additionalSearchPaths) { + m_pluginSearchPaths.insert(currentPath); + } + } + +#ifdef Q_OS_MAC + // Path to plugins inside AppBundle + m_pluginSearchPaths.insert(QString::fromLatin1("%1/../Plugins").arg(qApp->applicationDirPath())); +#endif + +#ifdef MUMBLE_PLUGIN_PATH + // Path to where plugins are/will be installed on the system + m_pluginSearchPaths.insert(QString::fromLatin1(MUMTEXT(MUMBLE_PLUGIN_PATH))); +#endif + + // Path to "plugins" dir right next to the executable's location. This is the case for when Mumble + // is run after compilation without having installed it anywhere special + m_pluginSearchPaths.insert(QString::fromLatin1("%1/plugins").arg(MumbleApplication::instance()->applicationVersionRootPath())); + + // Path to where the plugin installer will write plugins + m_pluginSearchPaths.insert(PluginInstaller::getInstallDir()); + +#ifdef Q_OS_WIN + // According to MS KB Q131065, we need this to OpenProcess() + + m_hToken = nullptr; + + if (!OpenThreadToken(GetCurrentThread(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, FALSE, &m_hToken)) { + if (GetLastError() == ERROR_NO_TOKEN) { + ImpersonateSelf(SecurityImpersonation); + OpenThreadToken(GetCurrentThread(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, FALSE, &m_hToken); + } + } + + TOKEN_PRIVILEGES tp; + LUID luid; + m_cbPrevious=sizeof(TOKEN_PRIVILEGES); + + LookupPrivilegeValue(NULL, SE_DEBUG_NAME, &luid); + + tp.PrivilegeCount = 1; + tp.Privileges[0].Luid = luid; + tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED; + + AdjustTokenPrivileges(m_hToken, FALSE, &tp, sizeof(TOKEN_PRIVILEGES), &m_tpPrevious, &m_cbPrevious); +#endif + + // Synchronize the positional data in a regular interval + // By making this the parent of the created timer, we don't have to delete it explicitly + QTimer *serverSyncTimer = new QTimer(this); + QObject::connect(serverSyncTimer, &QTimer::timeout, this, &PluginManager::on_syncPositionalData); + serverSyncTimer->start(POSITIONAL_SERVER_SYNC_INTERVAL); + + // Install this manager as a global eventFilter in order to get notified about all keypresses + if (QCoreApplication::instance()) { + QCoreApplication::instance()->installEventFilter(this); + } + + // Set up the timer for regularly checking for available positional data plugins + m_positionalDataCheckTimer.setInterval(POSITIONAL_DATA_CHECK_INTERVAL); + m_positionalDataCheckTimer.start(); + QObject::connect(&m_positionalDataCheckTimer, &QTimer::timeout, this, &PluginManager::checkForAvailablePositionalDataPlugin); + + QObject::connect(&m_updater, &PluginUpdater::updatesAvailable, this, &PluginManager::on_updatesAvailable); + QObject::connect(this, &PluginManager::keyEvent, this, &PluginManager::on_keyEvent); +} + +PluginManager::~PluginManager() { + clearPlugins(); + +#ifdef Q_OS_WIN + AdjustTokenPrivileges(m_hToken, FALSE, &m_tpPrevious, m_cbPrevious, NULL, NULL); + CloseHandle(m_hToken); +#endif +} + +/// Emits a log about a plugin with the given name having lost link (positional audio) +/// +/// @param pluginName The name of the plugin that lost link +void reportLostLink(const QString& pluginName) { + Global::get().l->log(Log::Information, PluginManager::tr("%1 lost link").arg(pluginName.toHtmlEscaped())); +} + +bool PluginManager::eventFilter(QObject *target, QEvent *event) { + if (event->type() == QEvent::KeyPress || event->type() == QEvent::KeyRelease) { + static QVector processedEvents; + + QKeyEvent *kEvent = static_cast(event); + + // We have to keep track of which events we have processed already as + // the same event might be sent to multiple targets and since this is + // installed as a global event filter, we get notified about each of + // them. However we want to process each event only once. + if (!kEvent->isAutoRepeat() && !processedEvents.contains(kEvent)) { + // Fire event + emit keyEvent(kEvent->key(), kEvent->modifiers(), kEvent->type() == QEvent::KeyPress); + + processedEvents << kEvent; + + if (processedEvents.size() == 1) { + // Make sure to clear the list of processed events after each iteration + // of the event loop (we don't want to let the vector grow to infinity + // over time. Firing the timer only when the size of processedEvents is + // exactly 1, we avoid adding multiple timers in a single iteration. + QTimer::singleShot(0, []() { processedEvents.clear(); }); + } + } + + } + + // standard event processing + return QObject::eventFilter(target, event); +} + +void PluginManager::unloadPlugins() const { + QReadLocker lock(&m_pluginCollectionLock); + + auto it = m_pluginHashMap.begin(); + + while (it != m_pluginHashMap.end()) { + unloadPlugin(*it.value()); + it++; + } +} + +void PluginManager::clearPlugins() { + // Unload plugins so that they aren't implicitly unloaded once they go out of scope after having been + // removed from the pluginHashMap. + // This could lead to one of the plugins making an API call in its shutdown function which then would try + // to verify the plugin's ID. For that it'll ask this PluginManager for a plugin with that ID. To check + // that it will have to aquire a read-lock for the pluginHashMap which is impossible after we aquire the + // write-lock in this function leading to a deadlock. + unloadPlugins(); + + QWriteLocker lock(&m_pluginCollectionLock); + + // Clear the list itself + m_pluginHashMap.clear(); +} + +bool PluginManager::selectActivePositionalDataPlugin() { + QReadLocker pluginLock(&m_pluginCollectionLock); + QWriteLocker activePluginLock(&m_activePosDataPluginLock); + + if (!Global::get().s.bTransmitPosition) { + // According to the settings the position shall not be transmitted meaning that we don't have to select any plugin + // for positional data + m_activePositionalDataPlugin = nullptr; + + return false; + } + + ProcessResolver procRes(true); + + auto it = m_pluginHashMap.begin(); + + // We assume that there is only one (enabled) plugin for the currently played game so we don't have to remember + // which plugin was active last + while (it != m_pluginHashMap.end()) { + plugin_ptr_t currentPlugin = it.value(); + + if (currentPlugin->isPositionalDataEnabled() && currentPlugin->isLoaded()) { + switch(currentPlugin->initPositionalData(procRes.getProcessNames().data(), + procRes.getProcessPIDs().data(), procRes.amountOfProcesses())) { + case PDEC_OK: + // the plugin is ready to provide positional data + m_activePositionalDataPlugin = currentPlugin; + + Global::get().l->log(Log::Information, tr("%1 linked").arg(currentPlugin->getName().toHtmlEscaped())); + + return true; + + case PDEC_ERROR_PERM: + // the plugin encountered a permanent error -> disable it + Global::get().l->log(Log::Warning, tr( + "Plugin \"%1\" encountered a permanent error in positional data gathering").arg(currentPlugin->getName())); + + currentPlugin->enablePositionalData(false); + break; + + case PDEC_ERROR_TEMP: + //The plugin encountered a temporary error -> skip it for now (that is: do nothing) + break; + } + } + + it++; + } + + m_activePositionalDataPlugin = nullptr; + + return false; +} + +#define LOG_FOUND(plugin, path, legacyStr) qDebug("Found %splugin '%s' at \"%s\"", legacyStr, qUtf8Printable(plugin->getName()), qUtf8Printable(path));\ + qDebug() << "Its description:" << qUtf8Printable(plugin->getDescription()) +#define LOG_FOUND_PLUGIN(plugin, path) LOG_FOUND(plugin, path, "") +#define LOG_FOUND_LEGACY_PLUGIN(plugin, path) LOG_FOUND(plugin, path, "legacy ") +#define LOG_FOUND_BUILTIN(plugin) LOG_FOUND(plugin, QString::fromLatin1(""), "built-in ") +void PluginManager::rescanPlugins() { + clearPlugins(); + + { + QWriteLocker lock(&m_pluginCollectionLock); + + // iterate over all files in the respective directories and try to construct a plugin from them + for (const auto ¤tPath : m_pluginSearchPaths) { + QFileInfoList currentList = QDir(currentPath).entryInfoList(); + + for (int k=0; k(currentInfo.absoluteFilePath())); + +#ifdef MUMBLE_PLUGIN_DEBUG + LOG_FOUND_PLUGIN(p, currentInfo.absoluteFilePath()); +#endif + + // if this code block is reached, the plugin was instantiated successfully so we can add it to the map + m_pluginHashMap.insert(p->getID(), p); + } catch(const PluginError& e) { + Q_UNUSED(e); + // If an exception is thrown, this library does not represent a proper plugin + // Check if it might be a legacy plugin instead + try { + legacy_plugin_ptr_t lp(Plugin::createNew(currentInfo.absoluteFilePath())); + +#ifdef MUMBLE_PLUGIN_DEBUG + LOG_FOUND_LEGACY_PLUGIN(lp, currentInfo.absoluteFilePath()); +#endif + m_pluginHashMap.insert(lp->getID(), lp); + } catch(const PluginError& e) { + Q_UNUSED(e); + + // At the time this function is running the MainWindow is not necessarily created yet, so we can't use + // the normal Log::log function + Log::logOrDefer(Log::Warning, + tr("Non-plugin found in plugin directory: \"%1\"").arg(currentInfo.absoluteFilePath())); + } + } + } + } + + // handle built-in plugins +#ifdef USE_MANUAL_PLUGIN + try { + std::shared_ptr mp(Plugin::createNew()); + + m_pluginHashMap.insert(mp->getID(), mp); +#ifdef MUMBLE_PLUGIN_DEBUG + LOG_FOUND_BUILTIN(mp); +#endif + } catch(const PluginError& e) { + // At the time this function is running the MainWindow is not necessarily created yet, so we can't use + // the normal Log::log function + Log::logOrDefer(Log::Warning, tr("Failed at loading manual plugin: %1").arg(QString::fromUtf8(e.what()))); + } +#endif + } + + QReadLocker readLock(&m_pluginCollectionLock); + + // load plugins based on settings + // iterate over all plugins that have saved settings + auto it = Global::get().s.qhPluginSettings.constBegin(); + while (it != Global::get().s.qhPluginSettings.constEnd()) { + // for this we need a way to get a plugin based on the filepath + const QString pluginKey = it.key(); + const PluginSetting setting = it.value(); + + // iterate over all loaded plugins to see if the current setting is applicable + auto pluginIt = m_pluginHashMap.begin(); + while (pluginIt != m_pluginHashMap.end()) { + plugin_ptr_t plugin = pluginIt.value(); + QString pluginHash = QLatin1String(QCryptographicHash::hash(plugin->getFilePath().toUtf8(), QCryptographicHash::Sha1).toHex()); + if (pluginKey == pluginHash) { + if (setting.enabled) { + loadPlugin(plugin->getID()); + + const uint32_t features = plugin->getFeatures(); + + if (!setting.positionalDataEnabled && (features & FEATURE_POSITIONAL)) { + // try to deactivate the feature if the setting says so + plugin->deactivateFeatures(FEATURE_POSITIONAL); + } + } + + // positional data is a special feature that has to be enabled/disabled in the Plugin wrapper class + // additionally to telling the plugin library that the feature shall be deactivated + plugin->enablePositionalData(setting.positionalDataEnabled); + + plugin->allowKeyboardMonitoring(setting.allowKeyboardMonitoring); + + break; + } + + pluginIt++; + } + + it++; + } +} + +const_plugin_ptr_t PluginManager::getPlugin(plugin_id_t pluginID) const { + QReadLocker lock(&m_pluginCollectionLock); + + return m_pluginHashMap.value(pluginID); +} + +void PluginManager::checkForPluginUpdates() { + m_updater.checkForUpdates(); +} + +bool PluginManager::fetchPositionalData() { + if (Global::get().bPosTest) { + // This is for testing-purposes only so the "fetched" position doesn't have any real meaning + m_positionalData.reset(); + + m_positionalData.m_playerDir.z = 1.0f; + m_positionalData.m_playerAxis.y = 1.0f; + m_positionalData.m_cameraDir.z = 1.0f; + m_positionalData.m_cameraAxis.y = 1.0f; + + return true; + } + + QReadLocker activePluginLock(&m_activePosDataPluginLock); + + if (!m_activePositionalDataPlugin) { + // It appears as if there is currently no plugin capable of delivering positional audio + // Set positional data to zero-values + m_positionalData.reset(); + + return false; + } + + QWriteLocker posDataLock(&m_positionalData.m_lock); + + bool retStatus = m_activePositionalDataPlugin->fetchPositionalData(m_positionalData.m_playerPos, m_positionalData.m_playerDir, + m_positionalData.m_playerAxis, m_positionalData.m_cameraPos, m_positionalData.m_cameraDir, m_positionalData.m_cameraAxis, + m_positionalData.m_context, m_positionalData.m_identity); + + // Add the plugin's name to the context as well to prevent name-clashes between plugins + if (!m_positionalData.m_context.isEmpty()) { + m_positionalData.m_context = m_activePositionalDataPlugin->getName() + QChar::Null + m_positionalData.m_context; + } + + if (!retStatus) { + // Shut the currently active plugin down and set a new one (if available) + m_activePositionalDataPlugin->shutdownPositionalData(); + + reportLostLink(m_activePositionalDataPlugin->getName()); + + // unlock the read-lock in order to allow selectActivePositionaldataPlugin to gain a write-lock + activePluginLock.unlock(); + + selectActivePositionalDataPlugin(); + } else { + // If the return-status doesn't indicate an error, we can assume that positional data is available + // The remaining problematic case is, if the player is exactly at position (0,0,0) as this is used as an indicator for the + // absence of positional data in the mix() function in AudioOutput.cpp + // Thus we have to make sure that this position is never set if positional data is actually available. + // We solve this problem by shifting the player a minimal amount on the z-axis + if (m_positionalData.m_playerPos == Position3D(0.0f, 0.0f, 0.0f)) { + m_positionalData.m_playerPos = {0.0f, 0.0f, std::numeric_limits::min()}; + } + if (m_positionalData.m_cameraPos == Position3D(0.0f, 0.0f, 0.0f)) { + m_positionalData.m_cameraPos = {0.0f, 0.0f, std::numeric_limits::min()}; + } + } + + return retStatus; +} + +void PluginManager::unlinkPositionalData() { + QWriteLocker lock(&m_activePosDataPluginLock); + + if (m_activePositionalDataPlugin) { + m_activePositionalDataPlugin->shutdownPositionalData(); + + reportLostLink(m_activePositionalDataPlugin->getName()); + + // Set the pointer to nullptr + m_activePositionalDataPlugin = nullptr; + } +} + +bool PluginManager::isPositionalDataAvailable() const { + QReadLocker lock(&m_activePosDataPluginLock); + + return m_activePositionalDataPlugin != nullptr; +} + +const PositionalData& PluginManager::getPositionalData() const { + return m_positionalData; +} + +void PluginManager::enablePositionalDataFor(plugin_id_t pluginID, bool enable) const { + QReadLocker lock(&m_pluginCollectionLock); + + plugin_ptr_t plugin = m_pluginHashMap.value(pluginID); + + if (plugin) { + plugin->enablePositionalData(enable); + } +} + +const QVector PluginManager::getPlugins(bool sorted) const { + QReadLocker lock(&m_pluginCollectionLock); + + QVector pluginList; + + auto it = m_pluginHashMap.constBegin(); + if (sorted) { + QList ids = m_pluginHashMap.keys(); + + // sort keys so that the corresponding Plugins are in alphabetical order based on their name + std::sort(ids.begin(), ids.end(), [this](plugin_id_t first, plugin_id_t second) { + return QString::compare(m_pluginHashMap.value(first)->getName(), m_pluginHashMap.value(second)->getName(), + Qt::CaseInsensitive) <= 0; + }); + + foreach(plugin_id_t currentID, ids) { + pluginList.append(m_pluginHashMap.value(currentID)); + } + } else { + while (it != m_pluginHashMap.constEnd()) { + pluginList.append(it.value()); + + it++; + } + } + + return pluginList; +} + +bool PluginManager::loadPlugin(plugin_id_t pluginID) const { + QReadLocker lock(&m_pluginCollectionLock); + + plugin_ptr_t plugin = m_pluginHashMap.value(pluginID); + + if (plugin) { + if (plugin->isLoaded()) { + // Don't attempt to load a plugin if it already is loaded. + // This can happen if the user clicks the apply button in the settings + // before hitting ok. + return true; + } + + return plugin->init() == STATUS_OK; + } + + return false; +} + +void PluginManager::unloadPlugin(plugin_id_t pluginID) const { + plugin_ptr_t plugin; + { + QReadLocker lock(&m_pluginCollectionLock); + + plugin = m_pluginHashMap.value(pluginID); + } + + if (plugin) { + unloadPlugin(*plugin); + } +} + +void PluginManager::unloadPlugin(Plugin &plugin) const { + if (plugin.isLoaded()) { + // Only shut down loaded plugins + plugin.shutdown(); + } +} + +bool PluginManager::clearPlugin(plugin_id_t pluginID) { + // We have to unload the plugin before we take the write lock. The reasoning being that if + // the plugin makes an API call in its shutdown callback, that'll lead to this manager being + // asked for whether a plugin with such an ID exists. The function performing this check will + // take a read lock which is not possible if we hold a write lock here already (deadlock). + unloadPlugin(pluginID); + + QWriteLocker lock(&m_pluginCollectionLock); + + // Remove the plugin from the list of known plugins + plugin_ptr_t plugin = m_pluginHashMap.take(pluginID); + + return plugin != nullptr; +} + +uint32_t PluginManager::deactivateFeaturesFor(plugin_id_t pluginID, uint32_t features) const { + QReadLocker lock(&m_pluginCollectionLock); + + plugin_ptr_t plugin = m_pluginHashMap.value(pluginID); + + if (plugin) { + return plugin->deactivateFeatures(features); + } + + return FEATURE_NONE; +} + +void PluginManager::allowKeyboardMonitoringFor(plugin_id_t pluginID, bool allow) const { + QReadLocker lock(&m_pluginCollectionLock); + + plugin_ptr_t plugin = m_pluginHashMap.value(pluginID); + + if (plugin) { + return plugin->allowKeyboardMonitoring(allow); + } +} + +bool PluginManager::pluginExists(plugin_id_t pluginID) const { + QReadLocker lock(&m_pluginCollectionLock); + + return m_pluginHashMap.contains(pluginID); +} + +void PluginManager::foreachPlugin(std::function pluginProcessor) const { + QReadLocker lock(&m_pluginCollectionLock); + + auto it = m_pluginHashMap.constBegin(); + + while (it != m_pluginHashMap.constEnd()) { + pluginProcessor(*it.value()); + + it++; + } +} + +void PluginManager::on_serverConnected() const { + const mumble_connection_t connectionID = Global::get().sh->getConnectionID(); + +#ifdef MUMBLE_PLUGIN_CALLBACK_DEBUG + qDebug("PluginManager: Connected to a server with connection ID %d", connectionID); +#endif + + foreachPlugin([connectionID](Plugin& plugin) { + if (plugin.isLoaded()) { + plugin.onServerConnected(connectionID); + } + }); +} + +void PluginManager::on_serverDisconnected() const { + const mumble_connection_t connectionID = Global::get().sh->getConnectionID(); + +#ifdef MUMBLE_PLUGIN_CALLBACK_DEBUG + qDebug("PluginManager: Disconnected from a server with connection ID %d", connectionID); +#endif + + foreachPlugin([connectionID](Plugin& plugin) { + if (plugin.isLoaded()) { + plugin.onServerDisconnected(connectionID); + } + }); +} + +void PluginManager::on_channelEntered(const Channel *newChannel, const Channel *prevChannel, const User *user) const { +#ifdef MUMBLE_PLUGIN_CALLBACK_DEBUG + qDebug() << "PluginManager: User" << user->qsName << "entered channel" << newChannel->qsName << "- ID:" << newChannel->iId; +#endif + + if (!Global::get().sh) { + // if there is no server-handler, there is no (real) channel to enter + return; + } + + const mumble_connection_t connectionID = Global::get().sh->getConnectionID(); + + foreachPlugin([user, newChannel, prevChannel, connectionID](Plugin& plugin) { + if (plugin.isLoaded()) { + plugin.onChannelEntered(connectionID, user->uiSession, prevChannel ? prevChannel->iId : -1, newChannel->iId); + } + }); +} + +void PluginManager::on_channelExited(const Channel *channel, const User *user) const { +#ifdef MUMBLE_PLUGIN_CALLBACK_DEBUG + qDebug() << "PluginManager: User" << user->qsName << "left channel" << channel->qsName << "- ID:" << channel->iId; +#endif + + const mumble_connection_t connectionID = Global::get().sh->getConnectionID(); + + foreachPlugin([user, channel, connectionID](Plugin& plugin) { + if (plugin.isLoaded()) { + plugin.onChannelExited(connectionID, user->uiSession, channel->iId); + } + }); +} + +QString getTalkingStateStr(Settings::TalkState ts) { + switch(ts) { + case Settings::TalkState::Passive: + return QString::fromLatin1("Passive"); + case Settings::TalkState::Talking: + return QString::fromLatin1("Talking"); + case Settings::TalkState::Whispering: + return QString::fromLatin1("Whispering"); + case Settings::TalkState::Shouting: + return QString::fromLatin1("Shouting"); + case Settings::TalkState::MutedTalking: + return QString::fromLatin1("MutedTalking"); + } + + return QString::fromLatin1("Unknown"); +} + +void PluginManager::on_userTalkingStateChanged() const { + const ClientUser *user = qobject_cast(QObject::sender()); +#ifdef MUMBLE_PLUGIN_CALLBACK_DEBUG + if (user) { + qDebug() << "PluginManager: User" << user->qsName << "changed talking state to" << getTalkingStateStr(user->tsState); + } else { + qCritical() << "PluginManager: Unable to identify ClientUser"; + } +#endif + + if (user) { + // Convert Mumble's talking state to the TalkingState used in the API + mumble_talking_state_t ts = INVALID; + + switch(user->tsState) { + case Settings::TalkState::Passive: + ts = PASSIVE; + break; + case Settings::TalkState::Talking: + ts = TALKING; + break; + case Settings::TalkState::Whispering: + ts = WHISPERING; + break; + case Settings::TalkState::Shouting: + ts = SHOUTING; + break; + case Settings::TalkState::MutedTalking: + ts = TALKING_MUTED; + break; + } + + if (ts == INVALID) { + qWarning("PluginManager.cpp: Invalid talking state encountered"); + // An error occured + return; + } + + const mumble_connection_t connectionID = Global::get().sh->getConnectionID(); + + foreachPlugin([user, ts, connectionID](Plugin& plugin) { + if (plugin.isLoaded()) { + plugin.onUserTalkingStateChanged(connectionID, user->uiSession, ts); + } + }); + } +} + +void PluginManager::on_audioInput(short *inputPCM, unsigned int sampleCount, unsigned int channelCount, unsigned int sampleRate, bool isSpeech) const { +#ifdef MUMBLE_PLUGIN_CALLBACK_DEBUG + qDebug() << "PluginManager: AudioInput with" << channelCount << "channels and" << sampleCount << "samples per channel. IsSpeech:" << isSpeech; +#endif + + foreachPlugin([inputPCM, sampleCount, channelCount, sampleRate, isSpeech](Plugin& plugin) { + if (plugin.isLoaded()) { + plugin.onAudioInput(inputPCM, sampleCount, channelCount, sampleRate, isSpeech); + } + }); +} + +void PluginManager::on_audioSourceFetched(float* outputPCM, unsigned int sampleCount, unsigned int channelCount, unsigned int sampleRate, bool isSpeech, const ClientUser* user) const { +#ifdef MUMBLE_PLUGIN_CALLBACK_DEBUG + qDebug() << "PluginManager: AudioSource with" << channelCount << "channels and" << sampleCount << "samples per channel fetched. IsSpeech:" << isSpeech; + if (user != nullptr) { + qDebug() << "Sender-ID:" << user->uiSession; + } +#endif + + foreachPlugin([outputPCM, sampleCount, channelCount, sampleRate, isSpeech, user](Plugin& plugin) { + if (plugin.isLoaded()) { + plugin.onAudioSourceFetched(outputPCM, sampleCount, channelCount, sampleRate, isSpeech, user ? user->uiSession : -1); + } + }); +} + +void PluginManager::on_audioOutputAboutToPlay(float *outputPCM, unsigned int sampleCount, unsigned int channelCount, unsigned int sampleRate, bool *modifiedAudio) const { +#ifdef MUMBLE_PLUGIN_CALLBACK_DEBUG + qDebug() << "PluginManager: AudioOutput with" << channelCount << "channels and" << sampleCount << "samples per channel"; +#endif + foreachPlugin([outputPCM, sampleCount, channelCount, sampleRate, modifiedAudio](Plugin& plugin) { + if (plugin.isLoaded()) { + if(plugin.onAudioOutputAboutToPlay(outputPCM, sampleCount, sampleRate, channelCount)) { + *modifiedAudio = true; + } + } + }); +} + +void PluginManager::on_receiveData(const ClientUser *sender, const uint8_t *data, size_t dataLength, const char *dataID) const { +#ifdef MUMBLE_PLUGIN_CALLBACK_DEBUG + qDebug() << "PluginManager: Data with ID" << dataID << "and length" << dataLength << "received. Sender-ID:" << sender->uiSession; +#endif + + const mumble_connection_t connectionID = Global::get().sh->getConnectionID(); + + foreachPlugin([sender, data, dataLength, dataID, connectionID](Plugin& plugin) { + if (plugin.isLoaded()) { + plugin.onReceiveData(connectionID, sender->uiSession, data, dataLength, dataID); + } + }); +} + +void PluginManager::on_serverSynchronized() const { +#ifdef MUMBLE_PLUGIN_CALLBACK_DEBUG + qDebug() << "PluginManager: Server synchronized"; +#endif + + const mumble_connection_t connectionID = Global::get().sh->getConnectionID(); + + foreachPlugin([connectionID](Plugin& plugin) { + if (plugin.isLoaded()) { + plugin.onServerSynchronized(connectionID); + } + }); +} + +void PluginManager::on_userAdded(mumble_userid_t userID) const { +#ifdef MUMBLE_PLUGIN_CALLBACK_DEBUG + qDebug() << "PluginManager: Added user with ID" << userID; +#endif + + const mumble_connection_t connectionID = Global::get().sh->getConnectionID(); + + foreachPlugin([userID, connectionID](Plugin& plugin) { + if (plugin.isLoaded()) { + plugin.onUserAdded(connectionID, userID); + }; + }); +} + +void PluginManager::on_userRemoved(mumble_userid_t userID) const { +#ifdef MUMBLE_PLUGIN_CALLBACK_DEBUG + qDebug() << "PluginManager: Removed user with ID" << userID; +#endif + + const mumble_connection_t connectionID = Global::get().sh->getConnectionID(); + + foreachPlugin([userID, connectionID](Plugin& plugin) { + if (plugin.isLoaded()) { + plugin.onUserRemoved(connectionID, userID); + }; + }); +} + +void PluginManager::on_channelAdded(mumble_channelid_t channelID) const { +#ifdef MUMBLE_PLUGIN_CALLBACK_DEBUG + qDebug() << "PluginManager: Added channel with ID" << channelID; +#endif + + const mumble_connection_t connectionID = Global::get().sh->getConnectionID(); + + foreachPlugin([channelID, connectionID](Plugin& plugin) { + if (plugin.isLoaded()) { + plugin.onChannelAdded(connectionID, channelID); + }; + }); +} + +void PluginManager::on_channelRemoved(mumble_channelid_t channelID) const { +#ifdef MUMBLE_PLUGIN_CALLBACK_DEBUG + qDebug() << "PluginManager: Removed channel with ID" << channelID; +#endif + + const mumble_connection_t connectionID = Global::get().sh->getConnectionID(); + + foreachPlugin([channelID, connectionID](Plugin& plugin) { + if (plugin.isLoaded()) { + plugin.onChannelRemoved(connectionID, channelID); + }; + }); +} + +void PluginManager::on_channelRenamed(int channelID) const { +#ifdef MUMBLE_PLUGIN_CALLBACK_DEBUG + qDebug() << "PluginManager: Renamed channel with ID" << channelID; +#endif + + const mumble_connection_t connectionID = Global::get().sh->getConnectionID(); + + foreachPlugin([channelID, connectionID](Plugin& plugin) { + if (plugin.isLoaded()) { + plugin.onChannelRenamed(connectionID, channelID); + }; + }); +} + +void PluginManager::on_keyEvent(unsigned int key, Qt::KeyboardModifiers modifiers, bool isPress) const { +#ifdef MUMBLE_PLUGIN_CALLBACK_DEBUG + qDebug() << "PluginManager: Key event detected: keyCode =" << key << "modifiers:" + << modifiers << "isPress =" << isPress; +#else + Q_UNUSED(modifiers); +#endif + + // Convert from Qt encoding to our own encoding + mumble_keycode_t keyCode = API::qtKeyCodeToAPIKeyCode(key); + + foreachPlugin([keyCode, isPress](Plugin &plugin) { + if (plugin.isLoaded()) { + plugin.onKeyEvent(keyCode, isPress); + } + }); +} + +void PluginManager::on_syncPositionalData() { + // fetch positional data + if (fetchPositionalData()) { + // Sync the gathered data (context + identity) with the server + if (!Global::get().uiSession) { + // For some reason the local session ID is not set -> clear all data sent to the server in order to gurantee + // a re-send once the session is restored and there is data available + QMutexLocker mLock(&m_sentDataMutex); + + m_sentData.context.clear(); + m_sentData.identity.clear(); + } else { + // Check if the identity and/or the context has changed and if it did, send that new info to the server + QMutexLocker mLock(&m_sentDataMutex); + QReadLocker rLock(&m_positionalData.m_lock); + + if (m_sentData.context != m_positionalData.m_context || m_sentData.identity != m_positionalData.m_identity ) { + MumbleProto::UserState mpus; + mpus.set_session(Global::get().uiSession); + + if (m_sentData.context != m_positionalData.m_context) { + m_sentData.context = m_positionalData.m_context; + mpus.set_plugin_context(m_sentData.context.toUtf8().constData(), m_sentData.context.size()); + } + if (m_sentData.identity != m_positionalData.m_identity) { + m_sentData.identity = m_positionalData.m_identity; + mpus.set_plugin_identity(m_sentData.identity.toUtf8().constData()); + } + + if (Global::get().sh) { + // send the message if the serverHandler is available + Global::get().sh->sendMessage(mpus); + } + } + } + } +} + +void PluginManager::on_updatesAvailable() { + if (Global::get().s.bPluginAutoUpdate) { + m_updater.update(); + } else { + m_updater.promptAndUpdate(); + } +} + +void PluginManager::checkForAvailablePositionalDataPlugin() { + bool performSearch = false; + { + QReadLocker activePluginLock(&m_activePosDataPluginLock); + + performSearch = !m_activePositionalDataPlugin; + } + + if (performSearch) { + selectActivePositionalDataPlugin(); + } +} diff --git a/src/mumble/PluginManager.h b/src/mumble/PluginManager.h new file mode 100644 index 00000000000..02f4db5fa0d --- /dev/null +++ b/src/mumble/PluginManager.h @@ -0,0 +1,279 @@ +// Copyright 2021 The Mumble Developers. All rights reserved. +// Use of this source code is governed by a BSD-style license +// that can be found in the LICENSE file at the root of the +// Mumble source tree or at . + +#ifndef MUMBLE_MUMBLE_PLUGINMANAGER_H_ +#define MUMBLE_MUMBLE_PLUGINMANAGER_H_ + +#include +#include +#include +#include +#include +#include +#ifdef Q_OS_WIN + #ifndef NOMINMAX + #define NOMINMAX + #endif + #include +#endif +#include "Plugin.h" +#include "MumbleApplication.h" +#include "PositionalData.h" + +#include "User.h" +#include "ClientUser.h" +#include "Channel.h" +#include "Settings.h" +#include "PluginUpdater.h" + +#include + +/// A struct for holding the values of the current context and identity that have been sent to the server +struct PluginManager_SentData { + QString context; + QString identity; +}; + + +/// The plugin manager is the central object dealing with everything plugin-related. It is responsible for +/// finding, loading and managing the plugins. It also is responsible for invoking callback functions in the plugins +/// and can be used by Mumble to communicate with them +class PluginManager : public QObject { + private: + Q_OBJECT + Q_DISABLE_COPY(PluginManager) + protected: + /// Lock for pluginHashMap. This lock has to be aquired when accessing pluginHashMap + mutable QReadWriteLock m_pluginCollectionLock; + /// A map between plugin-IDs and the actual plugin objects. You have to aquire pluginCollectionLock before + /// accessing this map. + QHash m_pluginHashMap; + /// A set of directories to search plugins in + QSet m_pluginSearchPaths; +#ifdef Q_OS_WIN + // This stuff is apparently needed on Windows in order to deal with DLLs + HANDLE m_hToken; + TOKEN_PRIVILEGES m_tpPrevious; + DWORD m_cbPrevious; +#endif + /// The PositionalData object holding the current positional data (as retrieved by the respective plugin) + PositionalData m_positionalData; + + /// A timer that causes the manager to regularly check for available plugins that can currently + /// deliver positional data. + QTimer m_positionalDataCheckTimer; + + /// The mutex for sentData. This has to be aquired before accessing sentData + mutable QMutex m_sentDataMutex; + /// The bits of the positional data that have already been sent to the server. It is used to determine whether + /// the new data has to be sent to the server (in case it has changed). You have ti aquire sentDataMutex before + /// accessing this field. + PluginManager_SentData m_sentData; + + /// The lock for activePositionalDataPlugin. It has to be aquired before accessing the respective field. + mutable QReadWriteLock m_activePosDataPluginLock; + /// The plugin that is currently used to retrieve positional data. You have to aquire activePosDataPluginLock before + /// accessing this field. + plugin_ptr_t m_activePositionalDataPlugin; + /// The PluginUpdater used to handle plugin updates. + PluginUpdater m_updater; + + // We override the QObject::eventFilter function in order to be able to install the pluginManager as an event filter + // to the main application in order to get notified about keystrokes. + bool eventFilter(QObject *target, QEvent *event) Q_DECL_OVERRIDE; + + /// Unloads all plugins that are currently loaded. + void unloadPlugins() const; + /// Clears the current list of plugins + void clearPlugins(); + /// Iterates over the plugins and tries to select a plugin that currently claims to be able to deliver positional data. If + /// it found a plugin, activePositionalDataPlugin is set accordingly. If not, it is set to nullptr. + /// + /// @returns Whether this function succeeded in finding such a plugin + bool selectActivePositionalDataPlugin(); + + /// A internal helper function that iterates over all plugins and calls the given function providing the current plugin as + /// a parameter. + void foreachPlugin(std::function) const; + public: + // How often positional data (identity & context) should be synched with the server if there is any (in ms) + static constexpr int POSITIONAL_SERVER_SYNC_INTERVAL = 500; + // How often the manager should check for available positional data plugins + static constexpr int POSITIONAL_DATA_CHECK_INTERVAL = 1000; + + /// Constructor + /// + /// @param additionalSearchPaths A pointer to a set of additional search paths or nullptr if no additional + /// paths are required. + /// @param p The parent QObject + PluginManager(QSet *additionalSearchPaths = nullptr, QObject *p = nullptr); + /// Destructor + virtual ~PluginManager() Q_DECL_OVERRIDE; + + /// @param pluginID The ID of the plugin that should be retreved + /// @returns A pointer to the plugin with the given ID or nullptr if no such plugin could be found + const_plugin_ptr_t getPlugin(plugin_id_t pluginID) const; + /// Checks whether there are any updates for the plugins and if there are it invokes the PluginUpdater. + void checkForPluginUpdates(); + /// Fetches positional data from the activePositionalDataPlugin if there is one set. This function will update the + /// positionalData field + /// + /// @returns Whether the positional data could be retrieved successfully + bool fetchPositionalData(); + /// Unlinks the currently active positional data plugin. Effectively this sets activePositionalDataPlugin to nullptr + void unlinkPositionalData(); + /// @returns Whether positional data is currently available (it has been successfully set via fetchPositionalData) + bool isPositionalDataAvailable() const; + /// @returns The most recent positional data + const PositionalData& getPositionalData() const; + /// Enables positional data gathering for the plugin with the given ID. A plugin is only even asked whether it can deliver + /// positional data if this is enabled. + /// + /// @param pluginID The ID of the plugin to access + /// @param enable Whether to enable positional data (alternative is to disable it) + void enablePositionalDataFor(plugin_id_t pluginID, bool enable = true) const; + /// @returns A const vector of the plugins + const QVector getPlugins(bool sorted = false) const; + /// Loads the plugin with the given ID. Loading means initializing the plugin. + /// + /// @param pluginID The ID of the plugin to load + /// @returns Whether the plugin could be successfully loaded + bool loadPlugin(plugin_id_t pluginID) const; + /// Unloads the plugin with the given ID. Unloading means shutting the plugign down. + /// + /// @param pluginID The ID of the plugin to unload + void unloadPlugin(plugin_id_t pluginID) const; + /// Unloads the given plugin. Unloading means shutting the plugign down. + /// + /// @param plugin The plugin to unload + void unloadPlugin(Plugin &plugin) const; + /// Clears the plugin from the list of known plugins + /// + /// @param pluginID The ID of the plugin to forget about + /// @returns Whether the plugin has been cleared successfully + bool clearPlugin(plugin_id_t pluginID); + /// Deactivates the given features for the plugin with the given ID + /// + /// @param pluginID The ID of the plugin to access + /// @param features The feature set that should be deactivated. The features are or'ed together. + /// @returns The feature set that could not be deactivated + uint32_t deactivateFeaturesFor(plugin_id_t pluginID, uint32_t features) const; + /// Allows or forbids the given plugin to monitor keyboard events. + /// + /// @param pluginID The ID of the plugin to access + /// @param allow Whether to allow the monitoring or not + void allowKeyboardMonitoringFor(plugin_id_t pluginID, bool allow) const; + /// Checks whether a plugin with the given ID exists. + /// + /// @param pluginID The ID to check + /// @returns Whether such a plugin exists + bool pluginExists(plugin_id_t pluginID) const; + + public slots: + /// Rescans the plugin directory and load all plugins from there after having cleared the current plugin list + void rescanPlugins(); + /// Slot that gets called whenever data from another plugin has been received. This function will then delegate + /// this to the respective plugin callback + /// + /// @param sender A pointer to the ClientUser whose client has sent the data + /// @param data The byte-array representing the sent data + /// @param dataLength The length of the data array + /// @param dataID The ID of the data + void on_receiveData(const ClientUser *sender, const uint8_t *data, size_t dataLength, const char *dataID) const; + /// Slot that gets called when the local client connects to a server. It will delegate it to the respective plugin callback. + void on_serverConnected() const; + /// Slot that gets called when the local client disconnects to a server. It will delegate it to the respective plugin callback. + void on_serverDisconnected() const; + /// Slot that gets called when a client enters a channel. It will delegate it to the respective plugin callback. + /// + /// @param newChannel A pointer to the new channel + /// @param prevChannel A pointer to the previous channel or nullptr if no such channel exists + /// @param user A pointer to the user that entered the channel + void on_channelEntered(const Channel *newChannel, const Channel *prevChannel, const User *user) const; + /// Slot that gets called when a client leaves a channel. It will delegate it to the respective plugin callback. + /// + /// @param channel A pointer to the channel that has been left + /// @param user A pointer to the user that entered the channel + void on_channelExited(const Channel *channel, const User *user) const; + /// Slot that gets called when the local client changes its talking state. It will delegate it to the respective plugin callback. + void on_userTalkingStateChanged() const; + /// Slot that gets called when the local client receives audio input. It will delegate it to the respective plugin callback. + /// + /// @param inputPCM The array containing the input PCM (pulse-code-modulation). Its length is sampleCount * channelCount + /// @param sampleCount The amount of samples in the PCM array + /// @param channelCount The amount of channels in the PCM array + /// @param sampleRate The used sample rate in Hz + /// @param isSpeech Whether Mumble considers this input as speech + void on_audioInput(short *inputPCM, unsigned int sampleCount, unsigned int channelCount, unsigned int sampleRate, bool isSpeech) const; + /// Slot that gets called when the local client has fetched an audio source. It will delegate it to the respective plugin callback. + /// + /// @param outputPCM The array containing the output-PCM (pulse-code-modulation). Its length is sampleCount * channelCount + /// @param sampleCount The amount of samples in the PCM array + /// @param channelCount The amount of channels in the PCM array + /// @param sampleRate The used sample rate in Hz + /// @param isSpeech Whether Mumble considers this input as speech + /// @param user A pointer to the ClientUser the audio source corresposnds to + void on_audioSourceFetched(float *outputPCM, unsigned int sampleCount, unsigned int channelCount, unsigned int sampleRate, bool isSpeech, + const ClientUser *user) const; + /// Slot that gets called when the local client is about to play some audio. It will delegate it to the respective plugin callback. + /// + /// @param outputPCM The array containing the output-PCM (pulse-code-modulation). Its length is sampleCount * channelCount + /// @param sampleCount The amount of samples in the PCM array + /// @param channelCount The amount of channels in the PCM array + /// @param sampleRate The used sample rate in Hz + void on_audioOutputAboutToPlay(float *outputPCM, unsigned int sampleCount, unsigned int channelCount, unsigned int sampleRate, + bool *modifiedAudio) const; + /// Slot that gets called after the local client has finished synchronizing with the server. It will delegate it to the respective + /// plugin callback. + void on_serverSynchronized() const; + /// Slot that gets called when a new user is added to the user model. It will delegate it to the respective plugin callbacks. + /// + /// @param userID The ID of the added user + void on_userAdded(unsigned int userID) const; + /// Slot that gets called when a user is removed from the user model. It will delegate it to the respective plugin callbacks. + /// + /// @param userID The ID of the removed user + void on_userRemoved(unsigned int userID) const; + /// Slot that gets called when a new channel is added to the user model. It will delegate it to the respective plugin callbacks. + /// + /// @param channelID The ID of the added channel + void on_channelAdded(int channelID) const; + /// Slot that gets called when a channel is removed from the user model. It will delegate it to the respective plugin callbacks. + /// + /// @param channelID The ID of the removed channel + void on_channelRemoved(int channelID) const; + /// Slot that gets called when a channel is renamed. It will delegate it to the respective plugin callbacks. + /// + /// @param channelID The ID of the renamed channel + void on_channelRenamed(int channelID) const; + /// Slot that gets called when a key has been pressed or released while Mumble has keyboard focus. + /// + /// @param key The code of the affected key (as encoded by Qt::Key) + /// @param modifiers The modifiers that were active in the moment of the event + /// @param isPress True if the key has been pressed, false if it has been released + void on_keyEvent(unsigned int key, Qt::KeyboardModifiers modifiers, bool isPress) const; + + /// Slot that gets called whenever the positional data should be synchronized with the server. Before it does that, it tries to + /// fetch new data. + void on_syncPositionalData(); + /// Slot called if there are plugin updates available + void on_updatesAvailable(); + + protected slots: + /// If there is no active positional data plugin, this function will initiate searching for a + /// new one. + void checkForAvailablePositionalDataPlugin(); + + signals: + /// A signal emitted if the PluginManager (acting as an event filter) detected + /// a QKeyEvent. + /// + /// @param key The code of the affected key (as encoded by Qt::Key) + /// @param modifiers The modifiers that were active in the moment of the event + /// @param isPress True if the key has been pressed, false if it has been released + void keyEvent(unsigned int key, Qt::KeyboardModifiers modifiers, bool isPress); +}; + +#endif diff --git a/src/mumble/PluginUpdater.cpp b/src/mumble/PluginUpdater.cpp new file mode 100644 index 00000000000..e7f4f259474 --- /dev/null +++ b/src/mumble/PluginUpdater.cpp @@ -0,0 +1,379 @@ +// Copyright 2021 The Mumble Developers. All rights reserved. +// Use of this source code is governed by a BSD-style license +// that can be found in the LICENSE file at the root of the +// Mumble source tree or at . + +#include "PluginUpdater.h" +#include "PluginManager.h" +#include "Log.h" +#include "PluginInstaller.h" +#include "Global.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +PluginUpdater::PluginUpdater(QWidget *parent) + : QDialog(parent), + m_wasInterrupted(false), + m_dataMutex(), + m_pluginsToUpdate(), + m_networkManager(), + m_pluginUpdateWidgets() { + + QObject::connect(&m_networkManager, &QNetworkAccessManager::finished, this, &PluginUpdater::on_updateDownloaded); +} + +PluginUpdater::~PluginUpdater() { + m_wasInterrupted.store(true); +} + +void PluginUpdater::checkForUpdates() { + // Dispatch a thread in which each plugin can check for updates + QtConcurrent::run([this]() { + QMutexLocker lock(&m_dataMutex); + + const QVector plugins = Global::get().pluginManager->getPlugins(); + + for (int i = 0; i < plugins.size(); i++) { + const_plugin_ptr_t plugin = plugins[i]; + + if (plugin->hasUpdate()) { + QUrl updateURL = plugin->getUpdateDownloadURL(); + + if (updateURL.isValid() && !updateURL.isEmpty() && !updateURL.fileName().isEmpty()) { + UpdateEntry entry = { plugin->getID(), updateURL, updateURL.fileName(), 0 }; + m_pluginsToUpdate << entry; + } + } + + // if the update has been asked to be interrupted, exit here + if (m_wasInterrupted.load()) { + emit updateInterrupted(); + return; + } + } + + if (!m_pluginsToUpdate.isEmpty()) { + emit updatesAvailable(); + } + }); +} + +void PluginUpdater::promptAndUpdate() { + setupUi(this); + populateUI(); + + setWindowIcon(QIcon(QLatin1String("skin:mumble.svg"))); + + QObject::connect(qcbSelectAll, &QCheckBox::stateChanged, this, &PluginUpdater::on_selectAll); + QObject::connect(this, &QDialog::finished, this, &PluginUpdater::on_finished); + + if (exec() == QDialog::Accepted) { + update(); + } +} + +void PluginUpdater::update() { + QMutexLocker l(&m_dataMutex); + + for (int i = 0; i < m_pluginsToUpdate.size(); i++) { + UpdateEntry currentEntry = m_pluginsToUpdate[i]; + + // The network manager will be emit a signal once the request has finished processing. + // Thus we can ignore the returned QNetworkReply* here. + m_networkManager.get(QNetworkRequest(currentEntry.updateURL)); + } +} + +void PluginUpdater::populateUI() { + clearUI(); + + QMutexLocker l(&m_dataMutex); + for (int i = 0; i < m_pluginsToUpdate.size(); i++) { + UpdateEntry currentEntry = m_pluginsToUpdate[i]; + plugin_id_t pluginID = currentEntry.pluginID; + + const_plugin_ptr_t plugin = Global::get().pluginManager->getPlugin(pluginID); + + if (!plugin) { + continue; + } + + QCheckBox *checkBox = new QCheckBox(qwContent); + checkBox->setText(plugin->getName()); + checkBox->setToolTip(plugin->getDescription()); + + checkBox->setProperty("pluginID", pluginID); + + QLabel *urlLabel = new QLabel(qwContent); + urlLabel->setText(currentEntry.updateURL.toString()); + urlLabel->setTextInteractionFlags(Qt::TextSelectableByMouse); + + UpdateWidgetPair pair = { checkBox, urlLabel }; + m_pluginUpdateWidgets << pair; + + QObject::connect(checkBox, &QCheckBox::stateChanged, this, &PluginUpdater::on_singleSelectionChanged); + } + + // sort the plugins alphabetically + std::sort(m_pluginUpdateWidgets.begin(), m_pluginUpdateWidgets.end(), [](const UpdateWidgetPair &first, const UpdateWidgetPair &second) { + return first.pluginCheckBox->text().compare(second.pluginCheckBox->text(), Qt::CaseInsensitive) < 0; + }); + + // add the widgets to the layout + for (int i = 0; i < m_pluginUpdateWidgets.size(); i++) { + UpdateWidgetPair ¤tPair = m_pluginUpdateWidgets[i]; + + static_cast(qwContent->layout())->addRow(currentPair.pluginCheckBox, currentPair.urlLabel); + } +} + +void PluginUpdater::clearUI() { + // There are always as many checkboxes as there are labels + for (int i = 0; i < m_pluginUpdateWidgets.size(); i++) { + UpdateWidgetPair ¤tPair = m_pluginUpdateWidgets[i]; + + qwContent->layout()->removeWidget(currentPair.pluginCheckBox); + qwContent->layout()->removeWidget(currentPair.urlLabel); + + delete currentPair.pluginCheckBox; + delete currentPair.urlLabel; + } +} + +void PluginUpdater::on_selectAll(int checkState) { + // failsafe for partially selected state (shouldn't happen though) + if (checkState == Qt::PartiallyChecked) { + checkState = Qt::Unchecked; + } + + // Select or deselect all plugins + for (int i = 0; i < m_pluginUpdateWidgets.size(); i++) { + UpdateWidgetPair ¤tPair = m_pluginUpdateWidgets[i]; + + currentPair.pluginCheckBox->setCheckState(static_cast(checkState)); + } +} + +void PluginUpdater::on_singleSelectionChanged(int checkState) { + bool isChecked = checkState == Qt::Checked; + + // Block signals for the selectAll checkBox in order to not trigger its + // check-logic when changing its check-state here + const QSignalBlocker blocker(qcbSelectAll); + + if (!isChecked) { + // If even a single item is unchecked, the selectAll checkbox has to be unchecked + qcbSelectAll->setCheckState(Qt::Unchecked); + return; + } + + // iterate through all checkboxes to see whether we have to toggle the selectAll checkbox + for (int i = 0; i < m_pluginUpdateWidgets.size(); i++) { + const UpdateWidgetPair ¤tPair = m_pluginUpdateWidgets[i]; + + if (!currentPair.pluginCheckBox->isChecked()) { + // One unchecked checkBox is enough to know that the selectAll + // CheckBox can't be checked, so we can abort at this point + return; + } + } + + qcbSelectAll->setCheckState(Qt::Checked); +} + +void PluginUpdater::on_finished(int result) { + if (result == QDialog::Accepted) { + if (qcbSelectAll->isChecked()) { + // all plugins shall be updated, so we don't have to check them individually + return; + } + + QMutexLocker l(&m_dataMutex); + + // The user wants to update the selected plugins only + // remove the plugins that shouldn't be updated from m_pluginsToUpdate + auto it = m_pluginsToUpdate.begin(); + while (it != m_pluginsToUpdate.end()) { + plugin_id_t id = it->pluginID; + + // find the corresponding checkbox + bool updateCurrent = false; + for (int k = 0; k < m_pluginUpdateWidgets.size(); k++) { + QCheckBox *checkBox = m_pluginUpdateWidgets[k].pluginCheckBox; + QVariant idVariant = checkBox->property("pluginID"); + + if (idVariant.isValid() && static_cast(idVariant.toInt()) == id) { + updateCurrent = checkBox->isChecked(); + break; + } + } + + if (!updateCurrent) { + // remove this entry from the update-vector + it = m_pluginsToUpdate.erase(it); + } else { + it++; + } + } + } else { + // Nothing to do as the user doesn't want to update anyways + } +} + +void PluginUpdater::interrupt() { + m_wasInterrupted.store(true); +} + +void PluginUpdater::on_updateDownloaded(QNetworkReply *reply) { + if (reply) { + // Schedule reply for deletion + reply->deleteLater(); + + if (m_wasInterrupted.load()) { + emit updateInterrupted(); + return; + } + + // Find the ID of the plugin this update is for by comparing the URLs + UpdateEntry entry; + bool foundID = false; + { + QMutexLocker l(&m_dataMutex); + + for (int i = 0; i < m_pluginsToUpdate.size(); i++) { + if (m_pluginsToUpdate[i].updateURL == reply->url()) { + foundID = true; + + // remove that entry from the vector as it is being updated right here + entry = m_pluginsToUpdate.takeAt(i); + break; + } + } + } + + if (!foundID) { + // Can't match the URL to a pluginID + qWarning() << "PluginUpdater: Requested update for plugin from" + << reply->url() << "but didn't find corresponding plugin again!"; + return; + } + + // Now get a handle to that plugin + const_plugin_ptr_t plugin = Global::get().pluginManager->getPlugin(entry.pluginID); + + if (!plugin) { + // Can't find plugin with given ID + qWarning() << "PluginUpdater: Got update for plugin with id" + << entry.pluginID << "but it doesn't seem to exist anymore!"; + return; + } + + // We can start actually checking the reply here + if (reply->error() != QNetworkReply::NoError) { + // There was an error during this request. Report it + Log::logOrDefer(Log::Warning, + tr("Unable to download plugin update for \"%1\" from \"%2\" (%3)").arg( + plugin->getName()).arg(reply->url().toString()).arg( + QString::fromLatin1( + QMetaEnum::fromType().valueToKey(reply->error()) + ) + ) + ); + return; + } + + // Check HTTP status code (just because the request was successful, doesn't + // mean the data was downloaded successfully + int httpStatusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + + if (httpStatusCode >= 300 && httpStatusCode < 400) { + // We have been redirected + if (entry.redirects >= MAX_REDIRECTS - 1) { + // Maximum redirect count exceeded + Log::logOrDefer(Log::Warning, tr("Update for plugin \"%1\" failed due to too many redirects").arg(plugin->getName())); + + return; + } + + QUrl redirectedUrl = reply->attribute(QNetworkRequest::RedirectionTargetAttribute).toUrl(); + // Because the redirection url can be relative, + // we have to use the previous one to resolve it + redirectedUrl = reply->url().resolved(redirectedUrl); + + // Re-insert the current plugin into the list of updating plugins (using the + // new URL so that it will be associated with that instead of the old one) + entry.updateURL = redirectedUrl; + entry.redirects++; + { + QMutexLocker l(&m_dataMutex); + + m_pluginsToUpdate << entry; + } + + // Post a new request for the file to the new URL + m_networkManager.get(QNetworkRequest(redirectedUrl)); + + return; + } + + if (httpStatusCode < 200 || httpStatusCode >= 300) { + // HTTP request has failed + Log::logOrDefer(Log::Warning, + tr("Unable to download plugin update for \"%1\" from \"%2\" (HTTP status code %3)").arg( + plugin->getName()).arg(reply->url().toString()).arg(httpStatusCode) + ); + + return; + } + + // Reply seems fine -> write file to disk and fire installer + QByteArray content = reply->readAll(); + + // Write the content to a file in the temp-dir + if (content.isEmpty()) { + qWarning() << "PluginUpdater: Update for" << plugin->getName() << "from" + << reply->url().toString() << "resulted in no content!"; + return; + } + + QFile file(QDir::temp().filePath(entry.fileName)); + if (!file.open(QIODevice::WriteOnly)) { + qWarning() << "PluginUpdater: Can't open" << file.fileName() << "for writing!"; + return; + } + + file.write(content); + file.close(); + + try { + // Launch installer + PluginInstaller installer(QFileInfo(file.fileName())); + installer.install(); + + Log::logOrDefer(Log::Information, tr("Successfully updated plugin \"%1\"").arg(plugin->getName())); + + // Make sure Mumble won't use the old version of the plugin + Global::get().pluginManager->rescanPlugins(); + } catch (const PluginInstallException &e) { + Log::logOrDefer(Log::CriticalError, e.getMessage()); + } + + { + QMutexLocker l(&m_dataMutex); + + if (m_pluginsToUpdate.isEmpty()) { + emit updatingFinished(); + } + } + } +} diff --git a/src/mumble/PluginUpdater.h b/src/mumble/PluginUpdater.h new file mode 100644 index 00000000000..2d9041d3dd0 --- /dev/null +++ b/src/mumble/PluginUpdater.h @@ -0,0 +1,107 @@ +// Copyright 2021 The Mumble Developers. All rights reserved. +// Use of this source code is governed by a BSD-style license +// that can be found in the LICENSE file at the root of the +// Mumble source tree or at . + +#ifndef MUMBLE_MUMBLE_PLUGINUPDATER_H_ +#define MUMBLE_MUMBLE_PLUGINUPDATER_H_ + +#include +#include +#include +#include +#include + +#include + +#include "ui_PluginUpdater.h" +#include "Plugin.h" + +/// A helper struct to store a pair of a CheckBox and a label corresponding to +/// a single plugin. +struct UpdateWidgetPair { + QCheckBox *pluginCheckBox; + QLabel *urlLabel; +}; + +/// A helper struct to store a pair of a plugin ID and an URL corresponding to +/// the same plugin. +struct UpdateEntry { + plugin_id_t pluginID; + QUrl updateURL; + QString fileName; + int redirects; +}; + +/// A class designed for managing plugin updates. At the same time this also represents +/// a Dialog that can be used to prompt the user whether certain updates should be updated. +class PluginUpdater : public QDialog, public Ui::PluginUpdater { + private: + Q_OBJECT; + Q_DISABLE_COPY(PluginUpdater); + + protected: + /// An atomic flag indicating whether the plugin update has been interrupted. It is used + /// to exit some loops in different threads before they are done. + std::atomic m_wasInterrupted; + /// A mutex for m_pluginsToUpdate. + QMutex m_dataMutex; + /// A vector holding plugins that can be updated by storing a pluginID and the download URL + /// in form of an UpdateEntry. + QVector m_pluginsToUpdate; + /// The NetworkManager used to perform the downloding of plugins. + QNetworkAccessManager m_networkManager; + /// A vector of the UI elements created for the individual plugins (in form of UpdateWidgetPairs). + /// NOTE: This vector may only be accessed from the UI thread this dialog is living in! + QVector m_pluginUpdateWidgets; + + /// Populates the UI with plugins that have been found to have an update available (through a call + /// to checkForUpdates()). + void populateUI(); + + public: + /// Constructor + /// + /// @param parent A pointer to the QWidget parent of this object + PluginUpdater(QWidget *parent = nullptr); + /// Destructor + ~PluginUpdater(); + + // The maximum number of redirects to allow + static constexpr int MAX_REDIRECTS = 10; + + /// Triggers an update check for all plugins that are currently recognized by Mumble. This is done + /// in a non-blocking fashion (in another thread). Once all plugins have been checked and if there + /// are updates available, the updatesAvailable signal is emitted. + void checkForUpdates(); + /// Launches a Dialog that asks the user which of the plugins an update has been found for, shall be + /// updated. If the user has selected at least selected one plugin and has accepted the dialog, this + /// function will automatically call update(). + void promptAndUpdate(); + /// Starts the update process of the plugins. This is done asynchronously. + void update(); + public slots: + /// Clears the UI from the widgets created for the individual plugins. + void clearUI(); + /// Slot triggered if the user changes the state of the selectAll CheckBox. + void on_selectAll(int checkState); + /// Slot triggered if the user toggles the CheckBox for any individual plugin. + void on_singleSelectionChanged(int checkState); + /// Slot triggered when the dialog is being closed. + void on_finished(int result); + /// Slot that can be triggered to ask for the update process to be interrupted. + void interrupt(); + protected slots: + /// Slot triggered once an update for a plugin has been downloaded. + void on_updateDownloaded(QNetworkReply *reply); + + signals: + /// This signal is emitted once it has been determined that there are plugin updates available. + void updatesAvailable(); + /// This signal is emitted once all plugin updates have been downloaded and processed. + void updatingFinished(); + /// This signal is emitted every time the update process has been interrupted. + void updateInterrupted(); +}; + +#endif // MUMBLE_MUMBLE_PLUGINUPDATER_H_ diff --git a/src/mumble/PluginUpdater.ui b/src/mumble/PluginUpdater.ui new file mode 100644 index 00000000000..8f2118d65ab --- /dev/null +++ b/src/mumble/PluginUpdater.ui @@ -0,0 +1,224 @@ + + + PluginUpdater + + + + 0 + 0 + 616 + 460 + + + + PluginUpdater + + + false + + + false + + + + + + + 0 + 0 + + + + The following plugins can be updated. + + + true + + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 20 + 7 + + + + + + + + background-color: rgb(94, 94, 94); + + + 1 + + + 0 + + + Qt::Horizontal + + + + + + + Select all + + + + + + + false + + + + + + QFrame::Sunken + + + true + + + + + 0 + 0 + 600 + 284 + + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + 15 + + + + + font-weight: bold; + + + Plugin + + + + + + + font-weight: bold; + + + Download-URL + + + + + + + + + + + background-color: rgb(94, 94, 94); + + + 1 + + + 0 + + + Qt::Horizontal + + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 20 + 7 + + + + + + + + Do you want to update the selected plugins? + + + true + + + + + + + Qt::Horizontal + + + QDialogButtonBox::No|QDialogButtonBox::Yes + + + + + + + + + buttonBox + accepted() + PluginUpdater + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + PluginUpdater + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/src/mumble/Plugins.cpp b/src/mumble/Plugins.cpp deleted file mode 100644 index 4510f757c12..00000000000 --- a/src/mumble/Plugins.cpp +++ /dev/null @@ -1,792 +0,0 @@ -// Copyright 2007-2021 The Mumble Developers. All rights reserved. -// Use of this source code is governed by a BSD-style license -// that can be found in the LICENSE file at the root of the -// Mumble source tree or at . - -#include "Plugins.h" - -#include "../../plugins/mumble_plugin.h" -#include "Log.h" -#include "MainWindow.h" -#include "Message.h" -#include "MumbleApplication.h" -#include "ServerHandler.h" -#include "Utils.h" -#include "WebFetch.h" -#ifdef USE_MANUAL_PLUGIN -# include "ManualPlugin.h" -#endif -#include "Global.h" - -#include -#include - -#ifdef Q_OS_WIN -# include -#endif - -#include -#include - -#ifdef Q_OS_WIN -# include -# include -#endif - -const QString PluginConfig::name = QLatin1String("PluginConfig"); - -static ConfigWidget *PluginConfigDialogNew(Settings &st) { - return new PluginConfig(st); -} - -static ConfigRegistrar registrarPlugins(5000, PluginConfigDialogNew); - -struct PluginInfo { - bool locked; - bool enabled; - QLibrary lib; - QString filename; - QString description; - QString shortname; - MumblePlugin *p; - MumblePlugin2 *p2; - MumblePluginQt *pqt; - PluginInfo(); -}; - -PluginInfo::PluginInfo() { - locked = false; - enabled = false; - p = nullptr; - p2 = nullptr; - pqt = nullptr; -} - -struct PluginFetchMeta { - QString hash; - QString path; - - PluginFetchMeta(const QString &hash_ = QString(), const QString &path_ = QString()) - : hash(hash_), path(path_) { /* Empty */ - } -}; - - -PluginConfig::PluginConfig(Settings &st) : ConfigWidget(st) { - setupUi(this); - qtwPlugins->setAccessibleName(tr("Plugins")); - qtwPlugins->header()->setSectionResizeMode(0, QHeaderView::Stretch); - qtwPlugins->header()->setSectionResizeMode(1, QHeaderView::ResizeToContents); - - refillPluginList(); -} - -QString PluginConfig::title() const { - return tr("Plugins"); -} - -const QString &PluginConfig::getName() const { - return PluginConfig::name; -} - -QIcon PluginConfig::icon() const { - return QIcon(QLatin1String("skin:config_plugin.png")); -} - -void PluginConfig::load(const Settings &r) { - loadCheckBox(qcbTransmit, r.bTransmitPosition); -} - -void PluginConfig::save() const { - QReadLocker lock(&Global::get().p->qrwlPlugins); - - s.bTransmitPosition = qcbTransmit->isChecked(); - s.qmPositionalAudioPlugins.clear(); - - QList< QTreeWidgetItem * > list = qtwPlugins->findItems(QString(), Qt::MatchContains); - foreach (QTreeWidgetItem *i, list) { - bool enabled = (i->checkState(1) == Qt::Checked); - - PluginInfo *pi = pluginForItem(i); - if (pi) { - s.qmPositionalAudioPlugins.insert(pi->filename, enabled); - pi->enabled = enabled; - } - } -} - -PluginInfo *PluginConfig::pluginForItem(QTreeWidgetItem *i) const { - if (i) { - foreach (PluginInfo *pi, Global::get().p->qlPlugins) { - if (pi->filename == i->data(0, Qt::UserRole).toString()) - return pi; - } - } - return nullptr; -} - -void PluginConfig::on_qpbConfig_clicked() { - PluginInfo *pi; - { - QReadLocker lock(&Global::get().p->qrwlPlugins); - pi = pluginForItem(qtwPlugins->currentItem()); - } - - if (!pi) - return; - - if (pi->pqt && pi->pqt->config) { - pi->pqt->config(this); - } else if (pi->p->config) { - pi->p->config(0); - } else { - QMessageBox::information(this, QLatin1String("Mumble"), tr("Plugin has no configure function."), - QMessageBox::Ok, QMessageBox::NoButton); - } -} - -void PluginConfig::on_qpbAbout_clicked() { - PluginInfo *pi; - { - QReadLocker lock(&Global::get().p->qrwlPlugins); - pi = pluginForItem(qtwPlugins->currentItem()); - } - - if (!pi) - return; - - if (pi->pqt && pi->pqt->about) { - pi->pqt->about(this); - } else if (pi->p->about) { - pi->p->about(0); - } else { - QMessageBox::information(this, QLatin1String("Mumble"), tr("Plugin has no about function."), QMessageBox::Ok, - QMessageBox::NoButton); - } -} - -void PluginConfig::on_qpbReload_clicked() { - Global::get().p->rescanPlugins(); - refillPluginList(); -} - -void PluginConfig::refillPluginList() { - QReadLocker lock(&Global::get().p->qrwlPlugins); - qtwPlugins->clear(); - - foreach (PluginInfo *pi, Global::get().p->qlPlugins) { - QTreeWidgetItem *i = new QTreeWidgetItem(qtwPlugins); - i->setFlags(Qt::ItemIsUserCheckable | Qt::ItemIsEnabled | Qt::ItemIsSelectable); - i->setCheckState(1, pi->enabled ? Qt::Checked : Qt::Unchecked); - i->setText(0, pi->description); - if (pi->p->longdesc) - i->setToolTip(0, QString::fromStdWString(pi->p->longdesc()).toHtmlEscaped()); - i->setData(0, Qt::UserRole, pi->filename); - } - qtwPlugins->setCurrentItem(qtwPlugins->topLevelItem(0)); - on_qtwPlugins_currentItemChanged(qtwPlugins->topLevelItem(0), nullptr); -} - -void PluginConfig::on_qtwPlugins_currentItemChanged(QTreeWidgetItem *current, QTreeWidgetItem *) { - QReadLocker lock(&Global::get().p->qrwlPlugins); - - PluginInfo *pi = pluginForItem(current); - if (pi) { - bool showAbout = false; - if (pi->p->about) { - showAbout = true; - } - if (pi->pqt && pi->pqt->about) { - showAbout = true; - } - qpbAbout->setEnabled(showAbout); - - bool showConfig = false; - if (pi->p->config) { - showConfig = true; - } - if (pi->pqt && pi->pqt->config) { - showConfig = true; - } - qpbConfig->setEnabled(showConfig); - } else { - qpbAbout->setEnabled(false); - qpbConfig->setEnabled(false); - } -} - -Plugins::Plugins(QObject *p) : QObject(p) { - QTimer *timer = new QTimer(this); - timer->setObjectName(QLatin1String("Timer")); - timer->start(500); - locked = prevlocked = nullptr; - bValid = false; - iPluginTry = 0; - for (int i = 0; i < 3; i++) - fPosition[i] = fFront[i] = fTop[i] = 0.0; - QMetaObject::connectSlotsByName(this); - -#ifdef QT_NO_DEBUG -# ifndef MUMBLE_PLUGIN_PATH - qsSystemPlugins = - QString::fromLatin1("%1/plugins").arg(MumbleApplication::instance()->applicationVersionRootPath()); -# ifdef Q_OS_MAC - qsSystemPlugins = QString::fromLatin1("%1/../Plugins").arg(qApp->applicationDirPath()); -# endif -# else - qsSystemPlugins = QLatin1String(MUMTEXT(MUMBLE_PLUGIN_PATH)); -# endif - - qsUserPlugins = Global::get().qdBasePath.absolutePath() + QLatin1String("/Plugins"); -#else -# ifdef MUMBLE_PLUGIN_PATH - qsSystemPlugins = QLatin1String(MUMTEXT(MUMBLE_PLUGIN_PATH)); -# else - qsSystemPlugins = QString(); -# endif - - qsUserPlugins = QString::fromLatin1("%1/plugins").arg(MumbleApplication::instance()->applicationVersionRootPath()); -#endif - -#ifdef Q_OS_WIN - // According to MS KB Q131065, we need this to OpenProcess() - - hToken = nullptr; - - if (!OpenThreadToken(GetCurrentThread(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, FALSE, &hToken)) { - if (GetLastError() == ERROR_NO_TOKEN) { - ImpersonateSelf(SecurityImpersonation); - OpenThreadToken(GetCurrentThread(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, FALSE, &hToken); - } - } - - TOKEN_PRIVILEGES tp; - LUID luid; - cbPrevious = sizeof(TOKEN_PRIVILEGES); - - LookupPrivilegeValue(nullptr, SE_DEBUG_NAME, &luid); - - tp.PrivilegeCount = 1; - tp.Privileges[0].Luid = luid; - tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED; - - AdjustTokenPrivileges(hToken, FALSE, &tp, sizeof(TOKEN_PRIVILEGES), &tpPrevious, &cbPrevious); -#endif -} - -Plugins::~Plugins() { - clearPlugins(); - -#ifdef Q_OS_WIN - AdjustTokenPrivileges(hToken, FALSE, &tpPrevious, cbPrevious, nullptr, nullptr); - CloseHandle(hToken); -#endif -} - -void Plugins::clearPlugins() { - QWriteLocker lock(&Global::get().p->qrwlPlugins); - foreach (PluginInfo *pi, qlPlugins) { - if (pi->locked) - pi->p->unlock(); - pi->lib.unload(); - delete pi; - } - qlPlugins.clear(); -} - -void Plugins::rescanPlugins() { - clearPlugins(); - - QWriteLocker lock(&Global::get().p->qrwlPlugins); - prevlocked = locked = nullptr; - bValid = false; - - QDir qd(qsSystemPlugins, QString(), QDir::Name, QDir::Files | QDir::Readable); - QDir qud(qsUserPlugins, QString(), QDir::Name, QDir::Files | QDir::Readable); - QFileInfoList libs = qud.entryInfoList() + qd.entryInfoList(); - - QSet< QString > evaluated; - foreach (const QFileInfo &libinfo, libs) { - QString fname = libinfo.fileName(); - QString libname = libinfo.absoluteFilePath(); - if (!evaluated.contains(fname) && QLibrary::isLibrary(libname)) { - PluginInfo *pi = new PluginInfo(); - pi->lib.setFileName(libname); - pi->filename = fname; - if (pi->lib.load()) { - mumblePluginFunc mpf = reinterpret_cast< mumblePluginFunc >(pi->lib.resolve("getMumblePlugin")); - if (mpf) { - evaluated.insert(fname); - pi->p = mpf(); - - // Check whether the plugin has a valid plugin magic and that it's not retracted. - // A retracted plugin is a plugin that clients should disregard, typically because - // the game the plugin was written for now provides positional audio via the - // link plugin (see null_plugin.cpp). - if (pi->p && pi->p->magic == MUMBLE_PLUGIN_MAGIC && pi->p->shortname != L"Retracted") { - pi->description = QString::fromStdWString(pi->p->description); - pi->shortname = QString::fromStdWString(pi->p->shortname); - pi->enabled = Global::get().s.qmPositionalAudioPlugins.value(pi->filename, true); - - mumblePlugin2Func mpf2 = - reinterpret_cast< mumblePlugin2Func >(pi->lib.resolve("getMumblePlugin2")); - if (mpf2) { - pi->p2 = mpf2(); - if (pi->p2->magic != MUMBLE_PLUGIN_MAGIC_2) { - pi->p2 = nullptr; - } - } - - mumblePluginQtFunc mpfqt = - reinterpret_cast< mumblePluginQtFunc >(pi->lib.resolve("getMumblePluginQt")); - if (mpfqt) { - pi->pqt = mpfqt(); - if (pi->pqt->magic != MUMBLE_PLUGIN_MAGIC_QT) { - pi->pqt = nullptr; - } - } - - qlPlugins << pi; - continue; - } - } - pi->lib.unload(); - } else { - qWarning("Plugins: Failed to load %s: %s", qPrintable(pi->filename), qPrintable(pi->lib.errorString())); - } - delete pi; - } - } - - // Handle built-in plugins - { -#if defined(USE_MANUAL_PLUGIN) - // Manual plugin - PluginInfo *pi = new PluginInfo(); - pi->filename = QLatin1String("manual.builtin"); - pi->p = ManualPlugin_getMumblePlugin(); - pi->pqt = ManualPlugin_getMumblePluginQt(); - pi->description = QString::fromStdWString(pi->p->description); - pi->shortname = QString::fromStdWString(pi->p->shortname); - pi->enabled = Global::get().s.qmPositionalAudioPlugins.value(pi->filename, true); - qlPlugins << pi; -#endif - } -} - -bool Plugins::fetch() { - if (Global::get().bPosTest) { - fPosition[0] = fPosition[1] = fPosition[2] = 0.0f; - fFront[0] = 0.0f; - fFront[1] = 0.0f; - fFront[2] = 1.0f; - fTop[0] = 0.0f; - fTop[1] = 1.0f; - fTop[2] = 0.0f; - - for (int i = 0; i < 3; ++i) { - fCameraPosition[i] = fPosition[i]; - fCameraFront[i] = fFront[i]; - fCameraTop[i] = fTop[i]; - } - - bValid = true; - return true; - } - - if (!locked) { - bValid = false; - return bValid; - } - - QReadLocker lock(&qrwlPlugins); - if (!locked) { - bValid = false; - return bValid; - } - - if (!locked->enabled) - bUnlink = true; - - bool ok; - { - QMutexLocker mlock(&qmPluginStrings); - ok = locked->p->fetch(fPosition, fFront, fTop, fCameraPosition, fCameraFront, fCameraTop, ssContext, - swsIdentity); - } - if (!ok || bUnlink) { - lock.unlock(); - QWriteLocker wlock(&qrwlPlugins); - - if (locked) { - locked->p->unlock(); - locked->locked = false; - prevlocked = locked; - locked = nullptr; - for (int i = 0; i < 3; i++) - fPosition[i] = fFront[i] = fTop[i] = fCameraPosition[i] = fCameraFront[i] = fCameraTop[i] = 0.0f; - } - } - bValid = ok; - return bValid; -} - -void Plugins::on_Timer_timeout() { - fetch(); - - QReadLocker lock(&qrwlPlugins); - - if (prevlocked) { - Global::get().l->log(Log::Information, tr("%1 lost link.").arg(prevlocked->shortname.toHtmlEscaped())); - prevlocked = nullptr; - } - - - { - QMutexLocker mlock(&qmPluginStrings); - - if (!locked) { - ssContext.clear(); - swsIdentity.clear(); - } - - std::string context; - if (locked) - context.assign(u8(QString::fromStdWString(locked->p->shortname)) + static_cast< char >(0) + ssContext); - - if (!Global::get().uiSession) { - ssContextSent.clear(); - swsIdentitySent.clear(); - } else if ((context != ssContextSent) || (swsIdentity != swsIdentitySent)) { - MumbleProto::UserState mpus; - mpus.set_session(Global::get().uiSession); - if (context != ssContextSent) { - ssContextSent.assign(context); - mpus.set_plugin_context(context); - } - if (swsIdentity != swsIdentitySent) { - swsIdentitySent.assign(swsIdentity); - mpus.set_plugin_identity(u8(QString::fromStdWString(swsIdentitySent))); - } - if (Global::get().sh) - Global::get().sh->sendMessage(mpus); - } - } - - if (locked) { - return; - } - - if (!Global::get().s.bTransmitPosition) - return; - - lock.unlock(); - QWriteLocker wlock(&qrwlPlugins); - - if (qlPlugins.isEmpty()) - return; - - ++iPluginTry; - if (iPluginTry >= qlPlugins.count()) - iPluginTry = 0; - - std::multimap< std::wstring, unsigned long long int > pids; -#if defined(Q_OS_WIN) - PROCESSENTRY32 pe; - - pe.dwSize = sizeof(pe); - HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); - if (hSnap != INVALID_HANDLE_VALUE) { - BOOL ok = Process32First(hSnap, &pe); - - while (ok) { - pids.insert( - std::pair< std::wstring, unsigned long long int >(std::wstring(pe.szExeFile), pe.th32ProcessID)); - ok = Process32Next(hSnap, &pe); - } - CloseHandle(hSnap); - } -#elif defined(Q_OS_LINUX) - QDir d(QLatin1String("/proc")); - QStringList entries = d.entryList(); - bool ok; - foreach (const QString &entry, entries) { - // Check if the entry is a PID - // by checking whether it's a number. - // If it is not, skip it. - unsigned long long int pid = static_cast< unsigned long long int >(entry.toLongLong(&ok, 10)); - if (!ok) { - continue; - } - - QString exe = QFile::symLinkTarget(QString(QLatin1String("/proc/%1/exe")).arg(entry)); - QFileInfo fi(exe); - QString firstPart = fi.baseName(); - QString completeSuffix = fi.completeSuffix(); - QString baseName; - if (completeSuffix.isEmpty()) { - baseName = firstPart; - } else { - baseName = firstPart + QLatin1String(".") + completeSuffix; - } - - if (baseName == QLatin1String("wine-preloader") || baseName == QLatin1String("wine64-preloader")) { - QFile f(QString(QLatin1String("/proc/%1/cmdline")).arg(entry)); - if (f.open(QIODevice::ReadOnly)) { - QByteArray cmdline = f.readAll(); - f.close(); - - int nul = cmdline.indexOf('\0'); - if (nul != -1) { - cmdline.truncate(nul); - } - - QString exe = QString::fromUtf8(cmdline); - if (exe.contains(QLatin1String("\\"))) { - int lastBackslash = exe.lastIndexOf(QLatin1String("\\")); - if (exe.count() > lastBackslash + 1) { - baseName = exe.mid(lastBackslash + 1); - } - } - } - } - - if (!baseName.isEmpty()) { - pids.insert(std::pair< std::wstring, unsigned long long int >(baseName.toStdWString(), pid)); - } - } -#endif - - PluginInfo *pi = qlPlugins.at(iPluginTry); - if (pi->enabled) { - if (pi->p2 ? pi->p2->trylock(pids) : pi->p->trylock()) { - pi->shortname = QString::fromStdWString(pi->p->shortname); - Global::get().l->log(Log::Information, tr("%1 linked.").arg(pi->shortname.toHtmlEscaped())); - pi->locked = true; - bUnlink = false; - locked = pi; - } - } -} - -void Plugins::checkUpdates() { - QUrl url; - url.setPath(QLatin1String("/v1/pa-plugins")); - - QList< QPair< QString, QString > > queryItems; - queryItems << qMakePair(QString::fromUtf8("ver"), - QString::fromUtf8(QUrl::toPercentEncoding(QString::fromUtf8(MUMBLE_RELEASE)))); -#if defined(Q_OS_WIN) -# if defined(Q_OS_WIN64) - queryItems << qMakePair(QString::fromUtf8("os"), QString::fromUtf8("WinX64")); -# else - queryItems << qMakePair(QString::fromUtf8("os"), QString::fromUtf8("Win32")); -# endif - queryItems << qMakePair(QString::fromUtf8("abi"), QString::fromUtf8(MUMTEXT(_MSC_VER))); -#elif defined(Q_OS_MAC) -# if defined(USE_MAC_UNIVERSAL) - queryItems << qMakePair(QString::fromUtf8("os"), QString::fromUtf8("MacOSX-Universal")); -# else - queryItems << qMakePair(QString::fromUtf8("os"), QString::fromUtf8("MacOSX")); -# endif -#else - queryItems << qMakePair(QString::fromUtf8("os"), QString::fromUtf8("Unix")); -#endif - - -#ifdef QT_NO_DEBUG - QUrlQuery query; - query.setQueryItems(queryItems); - url.setQuery(query); - - WebFetch::fetch(QLatin1String("update"), url, this, SLOT(fetchedUpdatePAPlugins(QByteArray, QUrl))); -#else - Global::get().mw->msgBox(tr("Skipping plugin update in debug mode.")); -#endif -} - -void Plugins::fetchedUpdatePAPlugins(QByteArray data, QUrl) { - if (data.isNull()) - return; - - bool rescan = false; - qmPluginFetchMeta.clear(); - QDomDocument doc; - doc.setContent(data); - - QDomElement root = doc.documentElement(); - QDomNode n = root.firstChild(); - while (!n.isNull()) { - QDomElement e = n.toElement(); - if (!e.isNull()) { - if (e.tagName() == QLatin1String("plugin")) { - QString name = QFileInfo(e.attribute(QLatin1String("name"))).fileName(); - QString hash = e.attribute(QLatin1String("hash")); - QString path = e.attribute(QLatin1String("path")); - qmPluginFetchMeta.insert(name, PluginFetchMeta(hash, path)); - } - } - n = n.nextSibling(); - } - - QDir qd(qsSystemPlugins, QString(), QDir::Name, QDir::Files | QDir::Readable); - QDir qdu(qsUserPlugins, QString(), QDir::Name, QDir::Files | QDir::Readable); - - QFileInfoList libs = qd.entryInfoList(); - foreach (const QFileInfo &libinfo, libs) { - QString libname = libinfo.absoluteFilePath(); - QString filename = libinfo.fileName(); - PluginFetchMeta pfm = qmPluginFetchMeta.value(filename); - QString wanthash = pfm.hash; - if (!wanthash.isNull() && QLibrary::isLibrary(libname)) { - QFile f(libname); - if (wanthash.isEmpty()) { - // Outdated plugin - if (f.exists()) { - clearPlugins(); - f.remove(); - rescan = true; - } - } else if (f.open(QIODevice::ReadOnly)) { - QString h = QLatin1String(sha1(f.readAll()).toHex()); - f.close(); - if (h == wanthash) { - if (qd != qdu) { - QFile qfuser(qsUserPlugins + QString::fromLatin1("/") + filename); - if (qfuser.exists()) { - clearPlugins(); - qfuser.remove(); - rescan = true; - } - } - // Mark for removal from userplugins - qmPluginFetchMeta.insert(filename, PluginFetchMeta()); - } - } - } - } - - if (qd != qdu) { - libs = qdu.entryInfoList(); - foreach (const QFileInfo &libinfo, libs) { - QString libname = libinfo.absoluteFilePath(); - QString filename = libinfo.fileName(); - PluginFetchMeta pfm = qmPluginFetchMeta.value(filename); - QString wanthash = pfm.hash; - if (!wanthash.isNull() && QLibrary::isLibrary(libname)) { - QFile f(libname); - if (wanthash.isEmpty()) { - // Outdated plugin - if (f.exists()) { - clearPlugins(); - f.remove(); - rescan = true; - } - } else if (f.open(QIODevice::ReadOnly)) { - QString h = QLatin1String(sha1(f.readAll()).toHex()); - f.close(); - if (h == wanthash) { - qmPluginFetchMeta.remove(filename); - } - } - } - } - } - QMap< QString, PluginFetchMeta >::const_iterator i; - for (i = qmPluginFetchMeta.constBegin(); i != qmPluginFetchMeta.constEnd(); ++i) { - PluginFetchMeta pfm = i.value(); - if (!pfm.hash.isEmpty()) { - QUrl pluginDownloadUrl; - if (pfm.path.isEmpty()) { - pluginDownloadUrl.setPath(QString::fromLatin1("%1").arg(i.key())); - } else { - pluginDownloadUrl.setPath(pfm.path); - } - - WebFetch::fetch(QLatin1String("pa-plugin-dl"), pluginDownloadUrl, this, - SLOT(fetchedPAPluginDL(QByteArray, QUrl))); - } - } - - if (rescan) - rescanPlugins(); -} - -void Plugins::fetchedPAPluginDL(QByteArray data, QUrl url) { - if (data.isNull()) - return; - - bool rescan = false; - - const QString &urlPath = url.path(); - QString fname = QFileInfo(urlPath).fileName(); - if (qmPluginFetchMeta.contains(fname)) { - PluginFetchMeta pfm = qmPluginFetchMeta.value(fname); - if (pfm.hash == QLatin1String(sha1(data).toHex())) { - bool verified = true; -#ifdef Q_OS_WIN - verified = false; - QString tempname; - std::wstring tempnative; - { - QTemporaryFile temp(QDir::tempPath() + QLatin1String("/plugin_XXXXXX.dll")); - if (temp.open()) { - tempname = temp.fileName(); - tempnative = QDir::toNativeSeparators(tempname).toStdWString(); - temp.write(data); - temp.setAutoRemove(false); - } - } - if (!tempname.isNull()) { - WINTRUST_FILE_INFO file; - ZeroMemory(&file, sizeof(file)); - file.cbStruct = sizeof(file); - file.pcwszFilePath = tempnative.c_str(); - - WINTRUST_DATA data; - ZeroMemory(&data, sizeof(data)); - data.cbStruct = sizeof(data); - data.dwUIChoice = WTD_UI_NONE; - data.fdwRevocationChecks = WTD_REVOKE_NONE; - data.dwUnionChoice = WTD_CHOICE_FILE; - data.pFile = &file; - data.dwProvFlags = WTD_SAFER_FLAG | WTD_USE_DEFAULT_OSVER_CHECK; - data.dwUIContext = WTD_UICONTEXT_INSTALL; - - static GUID guid = WINTRUST_ACTION_GENERIC_VERIFY_V2; - - LONG ts = WinVerifyTrust(0, &guid, &data); - - QFile deltemp(tempname); - deltemp.remove(); - verified = (ts == 0); - } -#endif - if (verified) { - clearPlugins(); - - QFile f; - f.setFileName(qsSystemPlugins + QLatin1String("/") + fname); - if (f.open(QIODevice::WriteOnly)) { - f.write(data); - f.close(); - Global::get().mw->msgBox(tr("Downloaded new or updated plugin to %1.").arg(f.fileName().toHtmlEscaped())); - } else { - f.setFileName(qsUserPlugins + QLatin1String("/") + fname); - if (f.open(QIODevice::WriteOnly)) { - f.write(data); - f.close(); - Global::get().mw->msgBox(tr("Downloaded new or updated plugin to %1.").arg(f.fileName().toHtmlEscaped())); - } else { - Global::get().mw->msgBox(tr("Failed to install new plugin to %1.").arg(f.fileName().toHtmlEscaped())); - } - } - - rescan = true; - } - } - } - - if (rescan) - rescanPlugins(); -} diff --git a/src/mumble/Plugins.h b/src/mumble/Plugins.h deleted file mode 100644 index fd951bb73ec..00000000000 --- a/src/mumble/Plugins.h +++ /dev/null @@ -1,91 +0,0 @@ -// Copyright 2007-2021 The Mumble Developers. All rights reserved. -// Use of this source code is governed by a BSD-style license -// that can be found in the LICENSE file at the root of the -// Mumble source tree or at . - -#ifndef MUMBLE_MUMBLE_PLUGINS_H_ -#define MUMBLE_MUMBLE_PLUGINS_H_ - -#include "ConfigDialog.h" - -#include "ui_Plugins.h" - -#ifdef Q_OS_WIN -# include "win.h" -#endif - -#include -#include -#include -#include - -struct PluginInfo; - -class PluginConfig : public ConfigWidget, public Ui::PluginConfig { -private: - Q_OBJECT - Q_DISABLE_COPY(PluginConfig) -protected: - void refillPluginList(); - PluginInfo *pluginForItem(QTreeWidgetItem *) const; - -public: - /// The unique name of this ConfigWidget - static const QString name; - PluginConfig(Settings &st); - virtual QString title() const Q_DECL_OVERRIDE; - const QString &getName() const Q_DECL_OVERRIDE; - virtual QIcon icon() const Q_DECL_OVERRIDE; -public slots: - void save() const Q_DECL_OVERRIDE; - void load(const Settings &r) Q_DECL_OVERRIDE; - void on_qpbConfig_clicked(); - void on_qpbAbout_clicked(); - void on_qpbReload_clicked(); - void on_qtwPlugins_currentItemChanged(QTreeWidgetItem *, QTreeWidgetItem *); -}; - -struct PluginFetchMeta; - -class Plugins : public QObject { - friend class PluginConfig; - -private: - Q_OBJECT - Q_DISABLE_COPY(Plugins) -protected: - QReadWriteLock qrwlPlugins; - QMutex qmPluginStrings; - QList< PluginInfo * > qlPlugins; - PluginInfo *locked; - PluginInfo *prevlocked; - void clearPlugins(); - int iPluginTry; - QMap< QString, PluginFetchMeta > qmPluginFetchMeta; - QString qsSystemPlugins; - QString qsUserPlugins; -#ifdef Q_OS_WIN - HANDLE hToken; - TOKEN_PRIVILEGES tpPrevious; - DWORD cbPrevious; -#endif -public: - std::string ssContext, ssContextSent; - std::wstring swsIdentity, swsIdentitySent; - bool bValid; - bool bUnlink; - float fPosition[3], fFront[3], fTop[3]; - float fCameraPosition[3], fCameraFront[3], fCameraTop[3]; - - Plugins(QObject *p = nullptr); - ~Plugins() Q_DECL_OVERRIDE; -public slots: - void on_Timer_timeout(); - void rescanPlugins(); - bool fetch(); - void checkUpdates(); - void fetchedUpdatePAPlugins(QByteArray, QUrl); - void fetchedPAPluginDL(QByteArray, QUrl); -}; - -#endif diff --git a/src/mumble/PositionalData.cpp b/src/mumble/PositionalData.cpp new file mode 100644 index 00000000000..8a510e1dff9 --- /dev/null +++ b/src/mumble/PositionalData.cpp @@ -0,0 +1,242 @@ +// Copyright 2021 The Mumble Developers. All rights reserved. +// Use of this source code is governed by a BSD-style license +// that can be found in the LICENSE file at the root of the +// Mumble source tree or at . + +#include "PositionalData.h" + +#include +#include +#include + +#include + +Vector3D::Vector3D() : x(0.0f), y(0.0f), z(0.0f) { +} + +Vector3D::Vector3D(float x, float y, float z) : x(x), y(y), z(z) { +} + +Vector3D::Vector3D(const Vector3D& other) : x(other.x), y(other.y), z(other.z) { +} + +Vector3D::~Vector3D() { +} + +float Vector3D::operator[](Coord coord) const { + switch(coord) { + case Coord::X: + return x; + case Coord::Y: + return y; + case Coord::Z: + return z; + } + + // invalid index + throw std::out_of_range("May only access x, y or z"); +} + +Vector3D Vector3D::operator*(float factor) const { + return { x * factor, y * factor, z * factor }; +} + +Vector3D Vector3D::operator/(float divisor) const { + return { x / divisor, y / divisor, z / divisor }; +} + +void Vector3D::operator*=(float factor) { + x *= factor; + y *= factor; + z *= factor; +} + +void Vector3D::operator/=(float divisor) { + x /= divisor; + y /= divisor; + z /= divisor; +} + +bool Vector3D::operator==(const Vector3D& other) const { + return equals(other, 0.0f); +} + +Vector3D Vector3D::operator-(const Vector3D& other) const { + return { x - other.x, y - other.y, z - other.z }; +} + +Vector3D Vector3D::operator+(const Vector3D& other) const { + return { x + other.x, y + other.y, z + other.z }; +} + +float Vector3D::normSquared() const { + return x * x + y * y + z * z; +} + +float Vector3D::norm() const { + return std::sqrt(normSquared()); +} + +float Vector3D::dotProduct(const Vector3D& other) const { + return x * other.x + y * other.y + z * other.z; +} + +Vector3D Vector3D::crossProduct(const Vector3D& other) const { + return { y * other.z - z * other.y, z * other.x - x * other.z, x * other.y - y * other.x }; +} + +bool Vector3D::equals(const Vector3D& other, float threshold) const { + if (threshold == 0.0f) { + return x == other.x && y == other.y && z == other.z; + } else { + threshold = std::abs(threshold); + + return std::abs(x - other.x) < threshold && std::abs(y - other.y) < threshold && std::abs(z - other.z) < threshold; + } +} + +bool Vector3D::isZero(float threshold) const { + if (threshold == 0.0f) { + return x == 0.0f && y == 0.0f && z == 0.0f; + } else { + return std::abs(x) < threshold && std::abs(y) < threshold && std::abs(z) < threshold; + } +} + +void Vector3D::normalize() { + float len = norm(); + + x /= len; + y /= len; + z /= len; +} + +void Vector3D::toZero() { + x = 0.0f; + y = 0.0f; + z = 0.0f; +} + +PositionalData::PositionalData() + : m_playerPos(), + m_playerDir(), + m_playerAxis(), + m_cameraPos(), + m_cameraDir(), + m_cameraAxis(), + m_context(), + m_identity(), + m_lock(QReadWriteLock::NonRecursive) { +} + +PositionalData::PositionalData(Position3D playerPos, Vector3D playerDir, Vector3D playerAxis, Position3D cameraPos, + Vector3D cameraDir, Vector3D cameraAxis, QString context, QString identity) + : m_playerPos(playerPos), + m_playerDir(playerDir), + m_playerAxis(playerAxis), + m_cameraPos(cameraPos), + m_cameraDir(cameraDir), + m_cameraAxis(cameraAxis), + m_context(context), + m_identity(identity), + m_lock(QReadWriteLock::NonRecursive) { +} + +PositionalData::~PositionalData() { +} + + +void PositionalData::getPlayerPos(Position3D& pos) const { + QReadLocker lock(&m_lock); + + pos = m_playerPos; +} + +Position3D PositionalData::getPlayerPos() const { + QReadLocker lock(&m_lock); + + return m_playerPos; +} + +void PositionalData::getPlayerDir(Vector3D& vec) const { + QReadLocker lock(&m_lock); + + vec = m_playerDir; +} + +Vector3D PositionalData::getPlayerDir() const { + QReadLocker lock(&m_lock); + + return m_playerDir; +} + +void PositionalData::getPlayerAxis(Vector3D& vec) const { + QReadLocker lock(&m_lock); + + vec = m_playerAxis; +} + +Vector3D PositionalData::getPlayerAxis() const { + QReadLocker lock(&m_lock); + + return m_playerAxis; +} + +void PositionalData::getCameraPos(Position3D& pos) const { + QReadLocker lock(&m_lock); + + pos = m_cameraPos; +} + +Position3D PositionalData::getCameraPos() const { + QReadLocker lock(&m_lock); + + return m_cameraPos; +} + +void PositionalData::getCameraDir(Vector3D& vec) const { + QReadLocker lock(&m_lock); + + vec = m_cameraDir; +} + +Vector3D PositionalData::getCameraDir() const { + QReadLocker lock(&m_lock); + + return m_cameraDir; +} + +void PositionalData::getCameraAxis(Vector3D& vec) const { + QReadLocker lock(&m_lock); + + vec = m_cameraAxis; +} + +Vector3D PositionalData::getCameraAxis() const { + QReadLocker lock(&m_lock); + + return m_cameraAxis; +} + +QString PositionalData::getPlayerIdentity() const { + QReadLocker lock(&m_lock); + + return m_identity; +} + +QString PositionalData::getContext() const { + QReadLocker lock(&m_lock); + + return m_context; +} + +void PositionalData::reset() { + m_playerPos.toZero(); + m_playerDir.toZero(); + m_playerAxis.toZero(); + m_cameraPos.toZero(); + m_cameraDir.toZero(); + m_cameraAxis.toZero(); + m_context = QString(); + m_identity = QString(); +} diff --git a/src/mumble/PositionalData.h b/src/mumble/PositionalData.h new file mode 100644 index 00000000000..a0f6f4013b4 --- /dev/null +++ b/src/mumble/PositionalData.h @@ -0,0 +1,171 @@ +// Copyright 2021 The Mumble Developers. All rights reserved. +// Use of this source code is governed by a BSD-style license +// that can be found in the LICENSE file at the root of the +// Mumble source tree or at . + +#ifndef MUMBLE_MUMBLE_POSITIONAL_AUDIO_CONTEXT_H_ +#define MUMBLE_MUMBLE_POSITIONAL_AUDIO_CONTEXT_H_ + +#include +#include + +/// An enum for the three cartesian coordinate axes x, y and z +enum class Coord {X=0,Y,Z}; + +/// A 3D vector class holding an x-, y- and z-coordinate +struct Vector3D { + /// The vector's x-coordinate + float x; + /// The vector's y-coordinate + float y; + /// The vector's z-coordinate + float z; + + /// Access the respective coordinate in an array-like fashion + /// + /// @param coord The Coord to access + /// @returns The value of the respective coordinate + float operator[](Coord coord) const; + /// @param factor The factor to scale by + /// @returns A new vector that has been created by scaling this vector by the given factor + Vector3D operator*(float factor) const; + /// @param divisor The divisor to apply to all coordinates + /// @returns A new vector obtained from this one by applying the divisor to all coordinates + Vector3D operator/(float divisor) const; + /// Scales this vector by the given factor + /// + /// @param factor The factor to use + void operator*=(float factor); + /// Divides all of this vector's coordinates by the given divisor + /// + /// @param divisor The divisor to use + void operator/=(float divisor); + /// @param other The vector to compare this one to + /// @returns Whether the given vector is equal to this one (their coordinates are the same) + bool operator==(const Vector3D& other) const; + /// @param other The vector to subtract from this one + /// @returns A new vector representing the difference of this vector and the other one + Vector3D operator-(const Vector3D& other) const; + /// @param other The vector to add to this one + /// @returns A new vector representing the sum of this vector and the other one + Vector3D operator+(const Vector3D& other) const; + /// @param other The vector to copy + /// @returns A copy of the other vector + Vector3D& operator=(const Vector3D& other) = default; + + // allow explicit conversions from this struct to a float-array / float-pointer + /// Explicit conversion to a float-array (of length 3) containing the coordinates of this vector + explicit operator const float*() const { return &x; }; + /// Explicit conversion to a float-array (of length 3) containing the coordinates of this vector + explicit operator float*() { return &x; }; + + /// Default constructor - sets all coordinates to 0 + Vector3D(); + /// @param x The x-coordinate + /// @param y The y-coordinate + /// @param z The z-coordinate + Vector3D(float x, float y, float z); + /// Copy constructor + /// + /// @param other The vector to copy + Vector3D(const Vector3D& other); + /// Destructor + ~Vector3D(); + /// @returns The squared euclidean norm (length of the vector) + float normSquared() const; + /// If possible normSquared() should be preferred as this doesn't require a square-root operator + /// + /// @returns The euclidean norm (length of the vector) + float norm() const; + /// @param other The vector to calculate the dot-product with + /// @returns The dot-product between this vector an the other one + float dotProduct(const Vector3D& other) const; + /// @param other The vector to calculate the cross-product (vector-product) with + /// @returns The vector resulting from the cross-product (vector-product) + Vector3D crossProduct(const Vector3D& other) const; + /// @param other The vector to compare this one to + /// @param threshold The maximum absolute difference for coordinates to still be considered equal + /// @returns Whether this and the given vector are equal + bool equals(const Vector3D& other, float threshold = 0.0f) const; + /// @param threshold The maximum absolute value a coordinate may have to still be considered zero + /// @returns Whether this vector is the zero-vector + bool isZero(float threshold = 0.0f) const; + /// Normalizes this vector to a unit-vector. Callin this function on a zero-vector results in undefined behaviour! + void normalize(); + /// Transforms this vector to a zero-vector by setting all coordinates to zero + void toZero(); +}; + +// As we're casting the vector struct to float-arrays, we have to make sure that the compiler won't introduce any padding +// into the structure +static_assert(sizeof(Vector3D) == 3*sizeof(float), "The compiler added padding to the Vector3D structure so it can't be cast to a float-array!"); + +/// A convenient alias as a position can be treated the same way a vector can +typedef Vector3D Position3D; + + +/// A class holding positional data used in the positional audio feature +class PositionalData { + friend class PluginManager; // needed in order for PluginManager::fetch to write to the contained fields + protected: + /// The player's position in the 3D world + Position3D m_playerPos; + /// The direction in which the player is looking + Vector3D m_playerDir; + /// The connection vector between the player's feet and his/her head + Vector3D m_playerAxis; + /// The camera's position un the 3D world + Position3D m_cameraPos; + /// The direction in which the camera is looking + Vector3D m_cameraDir; + /// The connection from the camera's bottom to its top + Vector3D m_cameraAxis; + /// The context of this positional data. This might include the game's name, the server currently connected to, etc. and is used + /// to determine which players can hear one another + QString m_context; + /// The player's ingame identity (name) + QString m_identity; + /// The lock guarding all fields of this class + mutable QReadWriteLock m_lock; + + public: + /// Default constructor + PositionalData(); + /// Constructor initializing all fields to a specific value + PositionalData(Position3D playerPos, Vector3D playerDir, Vector3D playerAxis, Position3D cameraPos, Vector3D cameraDir, + Vector3D cameraAxis, QString context, QString identity); + /// Destructor + ~PositionalData(); + /// @param[out] pos The player's 3D position + void getPlayerPos(Position3D& pos) const; + /// @returns The player's 3D position + Position3D getPlayerPos() const; + /// @param[out] vec The direction in which the player is currently looking + void getPlayerDir(Vector3D& vec) const; + /// @returns The direction in which the player is currently looking + Vector3D getPlayerDir() const; + /// @param[out] axis The connection between the player's feet and his/her head + void getPlayerAxis(Vector3D& axis) const; + /// @returns The connection between the player's feet and his/her head + Vector3D getPlayerAxis() const; + /// @param[out] pos The camera's 3D position + void getCameraPos(Position3D& pos) const; + /// @returns The camera's 3D position + Position3D getCameraPos() const; + /// @param[out] vec The direction in which the camera is currently looking + void getCameraDir(Vector3D& vec) const; + /// @returns The direction in which the camera is currently looking + Vector3D getCameraDir() const; + /// @param[out] axis The connection between the player's feet and his/her head + void getCameraAxis(Vector3D& axis) const; + /// @returns The connection between the player's feet and his/her head + Vector3D getCameraAxis() const; + /// @returns The player's identity + QString getPlayerIdentity() const; + /// @returns The current context + QString getContext() const; + /// Resets all fields in this object + void reset(); +}; + +#endif diff --git a/src/mumble/ServerHandler.cpp b/src/mumble/ServerHandler.cpp index 1e7291aac7a..429adc3f3d0 100644 --- a/src/mumble/ServerHandler.cpp +++ b/src/mumble/ServerHandler.cpp @@ -57,6 +57,10 @@ # include #endif +// Init ServerHandler::nextConnectionID +int ServerHandler::nextConnectionID = -1; +QMutex ServerHandler::nextConnectionIDMutex(QMutex::Recursive); + ServerHandlerMessageEvent::ServerHandlerMessageEvent(const QByteArray &msg, unsigned int mtype, bool flush) : QEvent(static_cast< QEvent::Type >(SERVERSEND_EVENT)) { qbaMsg = msg; @@ -109,6 +113,13 @@ ServerHandler::ServerHandler() : database(new Database(QLatin1String("ServerHand uiVersion = 0; iInFlightTCPPings = 0; + // assign connection ID + { + QMutexLocker lock(&nextConnectionIDMutex); + nextConnectionID++; + connectionID = nextConnectionID; + } + // Historically, the qWarning line below initialized OpenSSL for us. // It used to have this comment: // @@ -177,6 +188,10 @@ void ServerHandler::customEvent(QEvent *evt) { } } +int ServerHandler::getConnectionID() const { + return connectionID; +} + void ServerHandler::udpReady() { const unsigned int UDP_MAX_SIZE = 2048; while (qusUdp->hasPendingDatagrams()) { @@ -683,6 +698,9 @@ void ServerHandler::serverConnectionClosed(QAbstractSocket::SocketError err, con } } + // Having 2 signals here that basically fire at the same time is wanted behavior! + // See the documentation of "aboutToDisconnect" for an explanation. + emit aboutToDisconnect(err, reason); emit disconnected(err, reason); exit(0); diff --git a/src/mumble/ServerHandler.h b/src/mumble/ServerHandler.h index 5fc29c50449..d20833aff05 100644 --- a/src/mumble/ServerHandler.h +++ b/src/mumble/ServerHandler.h @@ -62,6 +62,9 @@ class ServerHandler : public QThread { Database *database; + static QMutex nextConnectionIDMutex; + static int nextConnectionID; + protected: QString qsHostName; QString qsUserName; @@ -70,6 +73,7 @@ class ServerHandler : public QThread { unsigned short usResolvedPort; bool bUdp; bool bStrong; + int connectionID; /// Flag indicating whether the server we are currently connected to has /// finished synchronizing already. @@ -117,6 +121,7 @@ class ServerHandler : public QThread { void getConnectionInfo(QString &host, unsigned short &port, QString &username, QString &pw) const; bool isStrong() const; void customEvent(QEvent *evt) Q_DECL_OVERRIDE; + int getConnectionID() const; void sendProtoMessage(const ::google::protobuf::Message &msg, unsigned int msgType); void sendMessage(const char *data, int len, bool force = false); @@ -169,6 +174,11 @@ class ServerHandler : public QThread { void run() Q_DECL_OVERRIDE; signals: void error(QAbstractSocket::SocketError, QString reason); + // This signal is basically the same as disconnected but it will be emitted + // *right before* disconnected is emitted. Thus this can be used by slots + // that need to block the disconnected signal from being emitted (using a + // direct connection) before they're done. + void aboutToDisconnect(QAbstractSocket::SocketError, QString reason); void disconnected(QAbstractSocket::SocketError, QString reason); void connected(); void pingRequested(); diff --git a/src/mumble/Settings.cpp b/src/mumble/Settings.cpp index 31d91bc68b6..aac13c4a9e5 100644 --- a/src/mumble/Settings.cpp +++ b/src/mumble/Settings.cpp @@ -15,6 +15,8 @@ #include #include +#include +#include #include #include #if QT_VERSION >= QT_VERSION_CHECK(5,9,0) @@ -287,6 +289,8 @@ Settings::Settings() { qRegisterMetaType< ShortcutTarget >("ShortcutTarget"); qRegisterMetaTypeStreamOperators< ShortcutTarget >("ShortcutTarget"); qRegisterMetaType< QVariant >("QVariant"); + qRegisterMetaType< PluginSetting >("PluginSetting"); + qRegisterMetaTypeStreamOperators< PluginSetting >("PluginSetting"); atTransmit = VAD; bTransmitPosition = false; @@ -346,6 +350,7 @@ Settings::Settings() { bUpdateCheck = true; bPluginCheck = true; #endif + bPluginAutoUpdate = false; qsImagePath = QStandardPaths::writableLocation(QStandardPaths::PicturesLocation); @@ -543,7 +548,8 @@ Settings::Settings() { qmMessages[Log::OtherSelfMute] = Settings::LogConsole; qmMessages[Log::OtherMutedOther] = Settings::LogConsole; qmMessages[Log::UserRenamed] = Settings::LogConsole; - + qmMessages[Log::PluginMessage] = Settings::LogConsole; + // Default theme themeName = QLatin1String("Mumble"); themeStyleName = QLatin1String("Lite"); @@ -888,6 +894,7 @@ void Settings::load(QSettings *settings_ptr) { LOAD(bUpdateCheck, "ui/updatecheck"); LOAD(bPluginCheck, "ui/plugincheck"); + LOAD(bPluginAutoUpdate, "ui/pluginAutoUpdate"); LOAD(bHideInTray, "ui/hidetray"); LOAD(bStateInTray, "ui/stateintray"); @@ -1002,9 +1009,26 @@ void Settings::load(QSettings *settings_ptr) { } settings_ptr->endGroup(); - settings_ptr->beginGroup(QLatin1String("audio/plugins")); - foreach (const QString &d, settings_ptr->childKeys()) { - qmPositionalAudioPlugins.insert(d, settings_ptr->value(d, true).toBool()); + // Plugins + settings_ptr->beginGroup(QLatin1String("plugins")); + foreach(const QString &pluginKey, settings_ptr->childGroups()) { + QString pluginHash; + + if (pluginKey.contains(QLatin1String("_"))) { + // The key contains the filename as well as the hash + int index = pluginKey.lastIndexOf(QLatin1String("_")); + pluginHash = pluginKey.right(pluginKey.size() - index - 1); + } else { + pluginHash = pluginKey; + } + + PluginSetting pluginSettings; + pluginSettings.path = settings_ptr->value(pluginKey + QLatin1String("/path")).toString(); + pluginSettings.allowKeyboardMonitoring = settings_ptr->value(pluginKey + QLatin1String("/allowKeyboardMonitoring")).toBool(); + pluginSettings.enabled = settings_ptr->value(pluginKey + QLatin1String("/enabled")).toBool(); + pluginSettings.positionalDataEnabled = settings_ptr->value(pluginKey + QLatin1String("/positionalDataEnabled")).toBool(); + + qhPluginSettings.insert(pluginHash, pluginSettings); } settings_ptr->endGroup(); @@ -1259,8 +1283,12 @@ void Settings::save() { SAVE(qsUsername, "ui/username"); SAVE(qsLastServer, "ui/server"); SAVE(ssFilter, "ui/serverfilter"); +#ifndef NO_UPDATE_CHECK + // If this flag has been set, we don't load the following settings so we shouldn't overwrite them here either SAVE(bUpdateCheck, "ui/updatecheck"); SAVE(bPluginCheck, "ui/plugincheck"); + SAVE(bPluginAutoUpdate, "ui/pluginAutoUpdate"); +#endif SAVE(bHideInTray, "ui/hidetray"); SAVE(bStateInTray, "ui/stateintray"); SAVE(bUsage, "ui/usage"); @@ -1373,22 +1401,56 @@ void Settings::save() { settings_ptr->remove(d); } settings_ptr->endGroup(); + + // Plugins + foreach(const QString &pluginHash, qhPluginSettings.keys()) { + QString savePath = QString::fromLatin1("plugins/"); + const PluginSetting settings = qhPluginSettings.value(pluginHash); + const QFileInfo info(settings.path); + QString baseName = info.baseName(); // Get the filename without file extensions + const bool containsNonASCII = baseName.contains(QRegularExpression(QStringLiteral("[^\\x{0000}-\\x{007F}]"))); + + if (containsNonASCII || baseName.isEmpty()) { + savePath += pluginHash; + } else { + // Make sure there are no spaces in the name + baseName.replace(QLatin1Char(' '), QLatin1Char('_')); + + // Also include the plugin's filename in the savepath in order + // to allow for easier identification + savePath += baseName + QLatin1String("__") + pluginHash; + } - settings_ptr->beginGroup(QLatin1String("audio/plugins")); - foreach (const QString &d, qmPositionalAudioPlugins.keys()) { - bool v = qmPositionalAudioPlugins.value(d); - if (!v) - settings_ptr->setValue(d, v); - else - settings_ptr->remove(d); + settings_ptr->beginGroup(savePath); + settings_ptr->setValue(QLatin1String("path"), settings.path); + settings_ptr->setValue(QLatin1String("enabled"), settings.enabled); + settings_ptr->setValue(QLatin1String("positionalDataEnabled"), settings.positionalDataEnabled); + settings_ptr->setValue(QLatin1String("allowKeyboardMonitoring"), settings.allowKeyboardMonitoring); + settings_ptr->endGroup(); } - settings_ptr->endGroup(); + settings_ptr->beginGroup(QLatin1String("overlay")); os.save(settings_ptr); settings_ptr->endGroup(); } +QDataStream& operator>>(QDataStream &arch, PluginSetting &setting) { + arch >> setting.enabled; + arch >> setting.positionalDataEnabled; + arch >> setting.allowKeyboardMonitoring; + + return arch; +} + +QDataStream& operator<<(QDataStream &arch, const PluginSetting &setting) { + arch << setting.enabled; + arch << setting.positionalDataEnabled; + arch << setting.allowKeyboardMonitoring; + + return arch; +} + #undef LOAD #undef LOADENUM #undef LOADFLAG diff --git a/src/mumble/Settings.h b/src/mumble/Settings.h index a074dd6ade1..ab46e5391cf 100644 --- a/src/mumble/Settings.h +++ b/src/mumble/Settings.h @@ -59,6 +59,17 @@ QDataStream &operator<<(QDataStream &, const ShortcutTarget &); QDataStream &operator>>(QDataStream &, ShortcutTarget &); Q_DECLARE_METATYPE(ShortcutTarget) +struct PluginSetting { + QString path; + bool enabled; + bool positionalDataEnabled; + bool allowKeyboardMonitoring; +}; +QDataStream& operator>>(QDataStream &arch, PluginSetting &setting); +QDataStream& operator<<(QDataStream &arch, const PluginSetting &setting); +Q_DECLARE_METATYPE(PluginSetting); + + struct OverlaySettings { enum OverlayPresets { AvatarAndName, LargeSquareAvatar }; @@ -249,7 +260,9 @@ struct Settings { bool bPositionalAudio; bool bPositionalHeadphone; float fAudioMinDistance, fAudioMaxDistance, fAudioMaxDistVolume, fAudioBloom; - QMap< QString, bool > qmPositionalAudioPlugins; + /// Contains the settings for each individual plugin. The key in this map is the Hex-represented SHA-1 + /// hash of the plugin's UTF-8 encoded absolute file-path on the hard-drive. + QHash< QString, PluginSetting > qhPluginSettings; OverlaySettings os; @@ -351,6 +364,7 @@ struct Settings { bool bUpdateCheck; bool bPluginCheck; + bool bPluginAutoUpdate; // PTT Button window bool bShowPTTButtonWindow; diff --git a/src/mumble/UserModel.cpp b/src/mumble/UserModel.cpp index 35fca925d83..ceb7c1dd427 100644 --- a/src/mumble/UserModel.cpp +++ b/src/mumble/UserModel.cpp @@ -1052,6 +1052,8 @@ ClientUser *UserModel::addUser(unsigned int id, const QString &name) { updateOverlay(); + emit userAdded(p->uiSession); + return p; } @@ -1087,6 +1089,8 @@ void UserModel::removeUser(ClientUser *p) { updateOverlay(); + emit userRemoved(p->uiSession); + delete p; delete item; } @@ -1303,6 +1307,8 @@ void UserModel::renameChannel(Channel *c, const QString &name) { moveItem(pi, pi, item); } + + emit channelRenamed(c->iId); } void UserModel::repositionChannel(Channel *c, const int position) { @@ -1341,6 +1347,9 @@ Channel *UserModel::addChannel(int id, Channel *p, const QString &name) { if (Global::get().s.ceExpand == Settings::AllChannels) Global::get().mw->qtvUsers->setExpanded(index(item), true); + + emit channelAdded(c->iId); + return c; } @@ -1501,6 +1510,8 @@ bool UserModel::removeChannel(Channel *c, const bool onlyIfUnoccupied) { Channel::remove(c); + emit channelRemoved(c->iId); + delete item; delete c; return true; diff --git a/src/mumble/UserModel.h b/src/mumble/UserModel.h index c9824f25a19..330500509b1 100644 --- a/src/mumble/UserModel.h +++ b/src/mumble/UserModel.h @@ -213,6 +213,27 @@ public slots: void recheckLinks(); void updateOverlay() const; void toggleChannelFiltered(Channel *c); +signals: + /// A signal emitted whenever a user is added to the model. + /// + /// @param userSessionID The ID of that user's session + void userAdded(unsigned int userSessionID); + /// A signal emitted whenever a user is removed from the model. + /// + /// @param userSessionID The ID of that user's session + void userRemoved(unsigned int userSessionID); + /// A signal that emitted whenever a channel is added to the model. + /// + /// @param channelID The ID of the channel + void channelAdded(int channelID); + /// A signal that emitted whenever a channel is removed from the model. + /// + /// @param channelID The ID of the channel + void channelRemoved(int channelID); + /// A signal that emitted whenever a channel is renamed. + /// + /// @param channelID The ID of the channel + void channelRenamed(int channelID); }; #endif diff --git a/src/mumble/main.cpp b/src/mumble/main.cpp index dad3cce16a1..a1ef34826be 100644 --- a/src/mumble/main.cpp +++ b/src/mumble/main.cpp @@ -11,12 +11,13 @@ #include "AudioWizard.h" #include "Cert.h" #include "Database.h" +#include "Log.h" +#include "LogEmitter.h" #include "DeveloperConsole.h" #include "LCD.h" #include "Log.h" #include "LogEmitter.h" #include "MainWindow.h" -#include "Plugins.h" #include "ServerHandler.h" #ifdef USE_ZEROCONF # include "Zeroconf.h" @@ -43,6 +44,9 @@ #include "Themes.h" #include "UserLockFile.h" #include "VersionCheck.h" +#include "PluginInstaller.h" +#include "PluginManager.h" +#include "Global.h" #include #include @@ -58,7 +62,6 @@ # include #endif -#include "Global.h" #ifdef BOOST_NO_EXCEPTIONS namespace boost { @@ -229,6 +232,7 @@ int main(int argc, char **argv) { QStringList extraTranslationDirs; QString localeOverwrite; + QStringList pluginsToBeInstalled; if (a.arguments().count() > 1) { for (int i = 1; i < args.count(); ++i) { if (args.at(i) == QLatin1String("-h") || args.at(i) == QLatin1String("--help") @@ -237,13 +241,15 @@ int main(int argc, char **argv) { #endif ) { QString helpMessage = - MainWindow::tr("Usage: mumble [options] []\n" + MainWindow::tr("Usage: mumble [options] [ | ]\n" "\n" " specifies a URL to connect to after startup instead of showing\n" "the connection window, and has the following form:\n" "mumble://[[:]@][:][/[/" "...]][?version=]\n" "\n" + " is a list of plugin files that shall be installed" + "\n" "The version query parameter has to be set in order to invoke the\n" "correct client version. It currently defaults to 1.2.0.\n" "\n" @@ -399,14 +405,18 @@ int main(int argc, char **argv) { return 1; } } else { - if (!bRpcMode) { - QUrl u = QUrl::fromEncoded(args.at(i).toUtf8()); - if (u.isValid() && (u.scheme() == QLatin1String("mumble"))) { - url = u; - } else { - QFile f(args.at(i)); - if (f.exists()) { - url = QUrl::fromLocalFile(f.fileName()); + if (PluginInstaller::canBePluginFile(args.at(i))) { + pluginsToBeInstalled << args.at(i); + } else { + if (!bRpcMode) { + QUrl u = QUrl::fromEncoded(args.at(i).toUtf8()); + if (u.isValid() && (u.scheme() == QLatin1String("mumble"))) { + url = u; + } else { + QFile f(args.at(i)); + if (f.exists()) { + url = QUrl::fromLocalFile(f.fileName()); + } } } } @@ -583,6 +593,22 @@ int main(int argc, char **argv) { Global::get().s.qsLanguage = settingsLocale.nativeLanguageName(); } + if (!pluginsToBeInstalled.isEmpty()) { + + foreach(QString currentPlugin, pluginsToBeInstalled) { + + try { + PluginInstaller installer(currentPlugin); + installer.exec(); + } catch(const PluginInstallException& e) { + qCritical() << qUtf8Printable(e.getMessage()); + } + + } + + return 0; + } + qWarning("Locale is \"%s\" (System: \"%s\")", qUtf8Printable(settingsLocale.name()), qUtf8Printable(systemLocale.name())); Mumble::Translations::LifetimeGuard translationGuard = Mumble::Translations::installTranslators(settingsLocale, a, extraTranslationDirs); @@ -605,6 +631,10 @@ int main(int argc, char **argv) { // Initialize zeroconf Global::get().zeroconf = new Zeroconf(); #endif + + // PluginManager + Global::get().pluginManager = new PluginManager(); + Global::get().pluginManager->rescanPlugins(); #ifdef USE_OVERLAY Global::get().o = new Overlay(); @@ -661,10 +691,6 @@ int main(int argc, char **argv) { Global::get().l->log(Log::Information, MainWindow::tr("Welcome to Mumble.")); - // Plugins - Global::get().p = new Plugins(nullptr); - Global::get().p->rescanPlugins(); - Audio::start(); a.setQuitOnLastWindowClosed(false); @@ -736,12 +762,13 @@ int main(int argc, char **argv) { new VersionCheck(false, Global::get().mw, true); # endif } -#else - Global::get().mw->msgBox(MainWindow::tr("Skipping version check in debug mode.")); -#endif + if (Global::get().s.bPluginCheck) { - Global::get().p->checkUpdates(); + Global::get().pluginManager->checkForPluginUpdates(); } +#else // QT_NO_DEBUG + Global::get().mw->msgBox(MainWindow::tr("Skipping version check in debug mode.")); +#endif // QT_NO_DEBUG if (url.isValid()) { OpenURLEvent *oue = new OpenURLEvent(url); @@ -772,8 +799,20 @@ int main(int argc, char **argv) { // Wait for the ServerHandler thread to exit before proceeding shutting down. This is so that // all events that the ServerHandler might emit are enqueued into Qt's event loop before we // ask it to pocess all of them below. - if (!sh->wait(2000)) { - qCritical("main: ServerHandler did not exit within specified time interval"); + + // We iteratively probe whether the ServerHandler thread has finished yet. If it did + // not, we execute pending events in the main loop. This is because the ServerHandler + // could be stuck waiting for a function to complete in the main loop (e.g. a plugin + // uses the API in the disconnect callback). + // We assume that this entire process is done in way under a second. + int iterations = 0; + while (!sh->wait(10)) { + QCoreApplication::processEvents(); + iterations++; + + if (iterations > 200) { + qFatal("ServerHandler does not exit as expected"); + } } } @@ -785,20 +824,26 @@ int main(int argc, char **argv) { delete srpc; + delete Global::get().talkingUI; + // Delete the MainWindow before the ServerHandler gets reset in order to allow all callbacks + // trggered by this deletion to still access the ServerHandler (atm all these callbacks are in PluginManager.cpp) + delete Global::get().mw; + Global::get().mw = nullptr; // Make it clear to any destruction code, that MainWindow no longer exists + Global::get().sh.reset(); - while (sh && !sh.unique()) + + while (sh && ! sh.unique()) QThread::yieldCurrentThread(); sh.reset(); - delete Global::get().talkingUI; - delete Global::get().mw; - delete Global::get().nam; delete Global::get().lcd; delete Global::get().db; - delete Global::get().p; delete Global::get().l; + Global::get().l = nullptr; // Make it clear to any destruction code that Log no longer exists + + delete Global::get().pluginManager; #ifdef USE_ZEROCONF delete Global::get().zeroconf; diff --git a/src/murmur/Messages.cpp b/src/murmur/Messages.cpp index cfde87e2a98..6fb23e1418c 100644 --- a/src/murmur/Messages.cpp +++ b/src/murmur/Messages.cpp @@ -10,6 +10,7 @@ #include "Group.h" #include "Message.h" #include "Meta.h" +#include "MumbleConstants.h" #include "Server.h" #include "ServerDB.h" #include "ServerUser.h" @@ -2165,6 +2166,61 @@ void Server::msgServerConfig(ServerUser *, MumbleProto::ServerConfig &) { void Server::msgSuggestConfig(ServerUser *, MumbleProto::SuggestConfig &) { } +void Server::msgPluginDataTransmission(ServerUser *sender, MumbleProto::PluginDataTransmission &msg) { + // A client's plugin has sent us a message that we shall delegate to its receivers + + if (sender->m_pluginMessageBucket.ratelimit(1)) { + qWarning("Dropping plugin message sent from \"%s\" (%d)", qUtf8Printable(sender->qsName), sender->uiSession); + return; + } + + if (!msg.has_data() || !msg.has_dataid()) { + // Messages without data and/or without a data ID can't be used by the clients. Thus we don't even have to send them + return; + } + + if (msg.data().size() > Mumble::Plugins::PluginMessage::MAX_DATA_LENGTH) { + qWarning("Dropping plugin message sent from \"%s\" (%d) - data too large", qUtf8Printable(sender->qsName), sender->uiSession); + return; + } + if (msg.dataid().size() > Mumble::Plugins::PluginMessage::MAX_DATA_ID_LENGTH) { + qWarning("Dropping plugin message sent from \"%s\" (%d) - data ID too long", qUtf8Printable(sender->qsName), sender->uiSession); + return; + } + + // Always set the sender's session and don't rely on it being set correctly (would + // allow spoofing the sender's session) + msg.set_sendersession(sender->uiSession); + + // Copy needed data from message in order to be able to remove info about receivers from the message as this doesn't + // matter for the client + size_t receiverAmount = msg.receiversessions_size(); + const ::google::protobuf::RepeatedField< ::google::protobuf::uint32 > receiverSessions = msg.receiversessions(); + + msg.clear_receiversessions(); + + QSet uniqueReceivers; + uniqueReceivers.reserve(receiverSessions.size()); + + for(int i = 0; static_cast(i) < receiverAmount; i++) { + uint32_t userSession = receiverSessions.Get(i); + + if (!uniqueReceivers.contains(userSession)) { + uniqueReceivers.insert(userSession); + } else { + // Duplicate entry -> ignore + continue; + } + + ServerUser *receiver = qhUsers.value(receiverSessions.Get(i)); + + if (receiver) { + // We can simply redirect the message we have received to the clients + sendMessage(receiver, msg); + } + } +} + #undef RATELIMIT #undef MSG_SETUP #undef MSG_SETUP_NO_UNIDLE diff --git a/src/murmur/Meta.cpp b/src/murmur/Meta.cpp index 6ebaeb18d2b..5e145616729 100644 --- a/src/murmur/Meta.cpp +++ b/src/murmur/Meta.cpp @@ -104,6 +104,9 @@ MetaParams::MetaParams() { iMessageLimit = 1; iMessageBurst = 5; + iPluginMessageLimit = 4; + iPluginMessageBurst = 15; + qsCiphers = MumbleSSL::defaultOpenSSLCipherString(); bLogGroupChanges = false; @@ -402,6 +405,9 @@ void MetaParams::read(QString fname) { iMessageLimit = typeCheckedFromSettings("messagelimit", 1); iMessageBurst = typeCheckedFromSettings("messageburst", 5); + iPluginMessageLimit = typeCheckedFromSettings("pluginmessagelimit", 4); + iPluginMessageBurst = typeCheckedFromSettings("pluginmessageburst", 15); + bool bObfuscate = typeCheckedFromSettings("obfuscate", false); if (bObfuscate) { qWarning("IP address obfuscation enabled."); diff --git a/src/murmur/Meta.h b/src/murmur/Meta.h index bac90df5b37..91b48f53559 100644 --- a/src/murmur/Meta.h +++ b/src/murmur/Meta.h @@ -104,6 +104,9 @@ class MetaParams { unsigned int iMessageLimit; unsigned int iMessageBurst; + unsigned int iPluginMessageLimit; + unsigned int iPluginMessageBurst; + QSslCertificate qscCert; QSslKey qskKey; diff --git a/src/murmur/Server.cpp b/src/murmur/Server.cpp index 4c8dd093994..c0c97981470 100644 --- a/src/murmur/Server.cpp +++ b/src/murmur/Server.cpp @@ -418,6 +418,8 @@ void Server::readParams() { qrChannelName = Meta::mp.qrChannelName; iMessageLimit = Meta::mp.iMessageLimit; iMessageBurst = Meta::mp.iMessageBurst; + iPluginMessageLimit = Meta::mp.iPluginMessageLimit; + iPluginMessageBurst = Meta::mp.iPluginMessageBurst; qvSuggestVersion = Meta::mp.qvSuggestVersion; qvSuggestPositional = Meta::mp.qvSuggestPositional; qvSuggestPushToTalk = Meta::mp.qvSuggestPushToTalk; @@ -526,6 +528,15 @@ void Server::readParams() { if (iMessageBurst < 1) { // Prevent disabling messages entirely iMessageBurst = 1; } + + iPluginMessageLimit = getConf("mpluginessagelimit", iPluginMessageLimit).toUInt(); + if (iPluginMessageLimit < 1) { // Prevent disabling messages entirely + iPluginMessageLimit = 1; + } + iPluginMessageBurst = getConf("pluginmessageburst", iPluginMessageBurst).toUInt(); + if (iPluginMessageBurst < 1) { // Prevent disabling messages entirely + iPluginMessageBurst = 1; + } } void Server::setLiveConf(const QString &key, const QString &value) { diff --git a/src/murmur/Server.h b/src/murmur/Server.h index abebd0ffed8..ac854cea631 100644 --- a/src/murmur/Server.h +++ b/src/murmur/Server.h @@ -140,6 +140,9 @@ class Server : public QThread { unsigned int iMessageLimit; unsigned int iMessageBurst; + unsigned int iPluginMessageLimit; + unsigned int iPluginMessageBurst; + QVariant qvSuggestVersion; QVariant qvSuggestPositional; QVariant qvSuggestPushToTalk; diff --git a/src/murmur/ServerUser.cpp b/src/murmur/ServerUser.cpp index f2dac8abbb6..367889bee73 100644 --- a/src/murmur/ServerUser.cpp +++ b/src/murmur/ServerUser.cpp @@ -13,7 +13,8 @@ #endif ServerUser::ServerUser(Server *p, QSslSocket *socket) - : Connection(p, socket), User(), s(nullptr), leakyBucket(p->iMessageLimit, p->iMessageBurst) { + : Connection(p, socket), User(), s(nullptr), + leakyBucket(p->iMessageLimit, p->iMessageBurst), m_pluginMessageBucket(5, 20) { sState = ServerUser::Connected; sUdpSocket = INVALID_SOCKET; diff --git a/src/murmur/ServerUser.h b/src/murmur/ServerUser.h index fd40071ca82..c34ebeaf72e 100644 --- a/src/murmur/ServerUser.h +++ b/src/murmur/ServerUser.h @@ -147,6 +147,7 @@ class ServerUser : public Connection, public User { QMap< QString, QString > qmWhisperRedirect; LeakyBucket leakyBucket; + LeakyBucket m_pluginMessageBucket; int iLastPermissionCheck; QMap< int, unsigned int > qmPermissionSent; From 5db2400af55776b5034775c36c1cae033a478091 Mon Sep 17 00:00:00 2001 From: Robert Adam Date: Wed, 14 Apr 2021 19:56:45 +0200 Subject: [PATCH 2/2] TRANSLATION: Update translation files --- src/mumble/mumble_ar.ts | 191 +++++++++++++++++++++++++-- src/mumble/mumble_bg.ts | 193 +++++++++++++++++++++++++-- src/mumble/mumble_br.ts | 191 +++++++++++++++++++++++++-- src/mumble/mumble_ca.ts | 191 +++++++++++++++++++++++++-- src/mumble/mumble_cs.ts | 203 ++++++++++++++++++++++++++--- src/mumble/mumble_cy.ts | 193 +++++++++++++++++++++++++-- src/mumble/mumble_da.ts | 203 ++++++++++++++++++++++++++--- src/mumble/mumble_de.ts | 203 ++++++++++++++++++++++++++--- src/mumble/mumble_el.ts | 203 ++++++++++++++++++++++++++--- src/mumble/mumble_en.ts | 191 +++++++++++++++++++++++++-- src/mumble/mumble_en_GB.ts | 191 +++++++++++++++++++++++++-- src/mumble/mumble_eo.ts | 193 +++++++++++++++++++++++++-- src/mumble/mumble_es.ts | 203 ++++++++++++++++++++++++++--- src/mumble/mumble_et.ts | 195 ++++++++++++++++++++++++++-- src/mumble/mumble_eu.ts | 201 +++++++++++++++++++++++++--- src/mumble/mumble_fa_IR.ts | 191 +++++++++++++++++++++++++-- src/mumble/mumble_fi.ts | 203 ++++++++++++++++++++++++++--- src/mumble/mumble_fr.ts | 203 ++++++++++++++++++++++++++--- src/mumble/mumble_gl.ts | 191 +++++++++++++++++++++++++-- src/mumble/mumble_he.ts | 204 ++++++++++++++++++++++++++--- src/mumble/mumble_hu.ts | 203 ++++++++++++++++++++++++++--- src/mumble/mumble_it.ts | 257 ++++++++++++++++++++++++++---------- src/mumble/mumble_ja.ts | 203 ++++++++++++++++++++++++++--- src/mumble/mumble_ko.ts | 203 ++++++++++++++++++++++++++--- src/mumble/mumble_lt.ts | 199 +++++++++++++++++++++++++--- src/mumble/mumble_nl.ts | 258 +++++++++++++++++++++++++----------- src/mumble/mumble_no.ts | 203 ++++++++++++++++++++++++++--- src/mumble/mumble_oc.ts | 191 +++++++++++++++++++++++++-- src/mumble/mumble_pl.ts | 259 ++++++++++++++++++++++++++----------- src/mumble/mumble_pt_BR.ts | 203 ++++++++++++++++++++++++++--- src/mumble/mumble_pt_PT.ts | 203 ++++++++++++++++++++++++++--- src/mumble/mumble_ro.ts | 191 +++++++++++++++++++++++++-- src/mumble/mumble_ru.ts | 203 ++++++++++++++++++++++++++--- src/mumble/mumble_si.ts | 197 +++++++++++++++++++++++++--- src/mumble/mumble_sv.ts | 259 ++++++++++++++++++++++++++----------- src/mumble/mumble_te.ts | 191 +++++++++++++++++++++++++-- src/mumble/mumble_th.ts | 191 +++++++++++++++++++++++++-- src/mumble/mumble_tr.ts | 203 ++++++++++++++++++++++++++--- src/mumble/mumble_uk.ts | 191 +++++++++++++++++++++++++-- src/mumble/mumble_zh_CN.ts | 252 ++++++++++++++++++++++++++---------- src/mumble/mumble_zh_HK.ts | 203 ++++++++++++++++++++++++++--- src/mumble/mumble_zh_TW.ts | 203 ++++++++++++++++++++++++++--- 42 files changed, 7678 insertions(+), 925 deletions(-) diff --git a/src/mumble/mumble_ar.ts b/src/mumble/mumble_ar.ts index 26cd9ddb586..450778ba509 100644 --- a/src/mumble/mumble_ar.ts +++ b/src/mumble/mumble_ar.ts @@ -3782,6 +3782,10 @@ This field describes the size of an LCD device. The size is given either in pixe User stopped listening to channel المستخدم توقف عن الاستماع الى القناة + + Plugin message + + LogConfig @@ -6125,12 +6129,13 @@ Valid actions are: - Usage: mumble [options] [<url>] + Usage: mumble [options] [<url> | <plugin_list>] <url> specifies a URL to connect to after startup instead of showing the connection window, and has the following form: mumble://[<username>[:<password>]@]<host>[:<port>][/<channel>[/<subchannel>...]][?version=<x.y.z>] +<plugin_list> is a list of plugin files that shall be installed The version query parameter has to be set in order to invoke the correct client version. It currently defaults to 1.2.0. @@ -6449,10 +6454,6 @@ Valid options are: Reconnect to last server on startup - - Download plugin and overlay updates on startup - - Privacy @@ -6478,6 +6479,14 @@ Prevents the client from sending potentially identifying information about the o Hide public server list + + Check for plugin updates on startup + + + + Automatically download and install plugin updates + + Overlay @@ -7028,30 +7037,188 @@ To upgrade these files to their latest versions, click the button below.اسم - Enabled + Enable + + + + PA + + + + KeyEvents + + + + Install a plugin from a local file + + + + Install plugin... + + + + Unload the currently selected plugin. This will remove it from the plugin list for the current session. + + + + Unload + + + + The plugin was installed successfully + + + + Unable to deactivate all requested features for plugin "%1" + + + + Unable to load plugin "%1" + + + + Whether the positional audio feature of this plugin should be enabled + + + + This plugin does not provide support for positional audio + + + + Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus + + + + Whether this plugin should be enabled + + + + + PluginInstaller + + PluginInstaller + + + + You are about to install the plugin listed below. Do you wish to proceed? + + + + <html><head/><body><p><span style=" font-weight:600;">Name:</span></p></body></html> + + + + <html><head/><body><p><span style=" font-weight:600;">Version:</span></p></body></html> + + + + <html><head/><body><p><span style=" font-weight:600;">Author(s):</span></p></body></html> + + + + <html><head/><body><p><span style=" font-weight:600;">Description:</span></p></body></html> + + + + &No + + + + &Yes + + + + The file "%1" is not a valid plugin file! + + + + Found more than one plugin library for the current OS in "%1" ("%2" and "%3")! + + + + Unable to find a plugin for the current OS in "%1" + + + + Unable to load plugin "%1" - check the plugin interface! + + + + Unable to delete old plugin at "%1" + + + + Unable to copy plugin library from "%1" to "%2" + + + + Unable to move plugin library to "%1" - Plugins + PluginManager + + %1 lost link + + + + %1 linked + + + + Plugin "%1" encountered a permanent error in positional data gathering + + + + Non-plugin found in plugin directory: "%1" + + + + Failed at loading manual plugin: %1 + + + + + PluginUpdater + + PluginUpdater + + + + The following plugins can be updated. + + + + Select all + + + + Plugin + + + + Download-URL + + - Skipping plugin update in debug mode. + Do you want to update the selected plugins? - Downloaded new or updated plugin to %1. + Unable to download plugin update for "%1" from "%2" (%3) - Failed to install new plugin to %1. + Update for plugin "%1" failed due to too many redirects - %1 lost link. + Unable to download plugin update for "%1" from "%2" (HTTP status code %3) - %1 linked. + Successfully updated plugin "%1" diff --git a/src/mumble/mumble_bg.ts b/src/mumble/mumble_bg.ts index c4c627535b3..a65aa7f8967 100644 --- a/src/mumble/mumble_bg.ts +++ b/src/mumble/mumble_bg.ts @@ -3779,6 +3779,10 @@ This field describes the size of an LCD device. The size is given either in pixe User stopped listening to channel + + Plugin message + + LogConfig @@ -6122,12 +6126,13 @@ Valid actions are: - Usage: mumble [options] [<url>] + Usage: mumble [options] [<url> | <plugin_list>] <url> specifies a URL to connect to after startup instead of showing the connection window, and has the following form: mumble://[<username>[:<password>]@]<host>[:<port>][/<channel>[/<subchannel>...]][?version=<x.y.z>] +<plugin_list> is a list of plugin files that shall be installed The version query parameter has to be set in order to invoke the correct client version. It currently defaults to 1.2.0. @@ -6446,10 +6451,6 @@ Valid options are: Reconnect to last server on startup Свързване към последния сървър при пускане на програмата - - Download plugin and overlay updates on startup - Сваляне на обновления за слоя и приставките при пускане на програмата - Privacy Поверителност @@ -6475,6 +6476,14 @@ Prevents the client from sending potentially identifying information about the o Hide public server list + + Check for plugin updates on startup + + + + Automatically download and install plugin updates + + Overlay @@ -7025,30 +7034,188 @@ To upgrade these files to their latest versions, click the button below.Име - Enabled - Включено + Enable + + + + PA + + + + KeyEvents + + + + Install a plugin from a local file + + + + Install plugin... + + + + Unload the currently selected plugin. This will remove it from the plugin list for the current session. + + + + Unload + + + + The plugin was installed successfully + + + + Unable to deactivate all requested features for plugin "%1" + + + + Unable to load plugin "%1" + + + + Whether the positional audio feature of this plugin should be enabled + + + + This plugin does not provide support for positional audio + + + + Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus + + + + Whether this plugin should be enabled + - Plugins + PluginInstaller + + PluginInstaller + + + + You are about to install the plugin listed below. Do you wish to proceed? + + + + <html><head/><body><p><span style=" font-weight:600;">Name:</span></p></body></html> + + + + <html><head/><body><p><span style=" font-weight:600;">Version:</span></p></body></html> + + + + <html><head/><body><p><span style=" font-weight:600;">Author(s):</span></p></body></html> + + + + <html><head/><body><p><span style=" font-weight:600;">Description:</span></p></body></html> + + + + &No + + + + &Yes + + + + The file "%1" is not a valid plugin file! + + + + Found more than one plugin library for the current OS in "%1" ("%2" and "%3")! + + + + Unable to find a plugin for the current OS in "%1" + + + + Unable to load plugin "%1" - check the plugin interface! + + + + Unable to delete old plugin at "%1" + + + + Unable to copy plugin library from "%1" to "%2" + + + + Unable to move plugin library to "%1" + + + + + PluginManager + + %1 lost link + + + + %1 linked + + + + Plugin "%1" encountered a permanent error in positional data gathering + + + + Non-plugin found in plugin directory: "%1" + + + + Failed at loading manual plugin: %1 + + + + + PluginUpdater + + PluginUpdater + + + + The following plugins can be updated. + + + + Select all + + + + Plugin + + + + Download-URL + + - Skipping plugin update in debug mode. + Do you want to update the selected plugins? - Downloaded new or updated plugin to %1. + Unable to download plugin update for "%1" from "%2" (%3) - Failed to install new plugin to %1. + Update for plugin "%1" failed due to too many redirects - %1 lost link. + Unable to download plugin update for "%1" from "%2" (HTTP status code %3) - %1 linked. + Successfully updated plugin "%1" diff --git a/src/mumble/mumble_br.ts b/src/mumble/mumble_br.ts index 58e8a1748d8..8383361b91e 100644 --- a/src/mumble/mumble_br.ts +++ b/src/mumble/mumble_br.ts @@ -3778,6 +3778,10 @@ This field describes the size of an LCD device. The size is given either in pixe User stopped listening to channel + + Plugin message + + LogConfig @@ -6121,12 +6125,13 @@ Valid actions are: - Usage: mumble [options] [<url>] + Usage: mumble [options] [<url> | <plugin_list>] <url> specifies a URL to connect to after startup instead of showing the connection window, and has the following form: mumble://[<username>[:<password>]@]<host>[:<port>][/<channel>[/<subchannel>...]][?version=<x.y.z>] +<plugin_list> is a list of plugin files that shall be installed The version query parameter has to be set in order to invoke the correct client version. It currently defaults to 1.2.0. @@ -6445,10 +6450,6 @@ Valid options are: Reconnect to last server on startup - - Download plugin and overlay updates on startup - - Privacy @@ -6474,6 +6475,14 @@ Prevents the client from sending potentially identifying information about the o Hide public server list + + Check for plugin updates on startup + + + + Automatically download and install plugin updates + + Overlay @@ -7024,30 +7033,188 @@ To upgrade these files to their latest versions, click the button below.Anv - Enabled + Enable + + + + PA + + + + KeyEvents + + + + Install a plugin from a local file + + + + Install plugin... + + + + Unload the currently selected plugin. This will remove it from the plugin list for the current session. + + + + Unload + + + + The plugin was installed successfully + + + + Unable to deactivate all requested features for plugin "%1" + + + + Unable to load plugin "%1" + + + + Whether the positional audio feature of this plugin should be enabled + + + + This plugin does not provide support for positional audio + + + + Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus + + + + Whether this plugin should be enabled + + + + + PluginInstaller + + PluginInstaller + + + + You are about to install the plugin listed below. Do you wish to proceed? + + + + <html><head/><body><p><span style=" font-weight:600;">Name:</span></p></body></html> + + + + <html><head/><body><p><span style=" font-weight:600;">Version:</span></p></body></html> + + + + <html><head/><body><p><span style=" font-weight:600;">Author(s):</span></p></body></html> + + + + <html><head/><body><p><span style=" font-weight:600;">Description:</span></p></body></html> + + + + &No + + + + &Yes + + + + The file "%1" is not a valid plugin file! + + + + Found more than one plugin library for the current OS in "%1" ("%2" and "%3")! + + + + Unable to find a plugin for the current OS in "%1" + + + + Unable to load plugin "%1" - check the plugin interface! + + + + Unable to delete old plugin at "%1" + + + + Unable to copy plugin library from "%1" to "%2" + + + + Unable to move plugin library to "%1" - Plugins + PluginManager + + %1 lost link + + + + %1 linked + + + + Plugin "%1" encountered a permanent error in positional data gathering + + + + Non-plugin found in plugin directory: "%1" + + + + Failed at loading manual plugin: %1 + + + + + PluginUpdater + + PluginUpdater + + + + The following plugins can be updated. + + + + Select all + + + + Plugin + + + + Download-URL + + - Skipping plugin update in debug mode. + Do you want to update the selected plugins? - Downloaded new or updated plugin to %1. + Unable to download plugin update for "%1" from "%2" (%3) - Failed to install new plugin to %1. + Update for plugin "%1" failed due to too many redirects - %1 lost link. + Unable to download plugin update for "%1" from "%2" (HTTP status code %3) - %1 linked. + Successfully updated plugin "%1" diff --git a/src/mumble/mumble_ca.ts b/src/mumble/mumble_ca.ts index 8996eab60e7..5d33ca64f08 100644 --- a/src/mumble/mumble_ca.ts +++ b/src/mumble/mumble_ca.ts @@ -3784,6 +3784,10 @@ This field describes the size of an LCD device. The size is given either in pixe User stopped listening to channel + + Plugin message + + LogConfig @@ -6127,12 +6131,13 @@ Valid actions are: - Usage: mumble [options] [<url>] + Usage: mumble [options] [<url> | <plugin_list>] <url> specifies a URL to connect to after startup instead of showing the connection window, and has the following form: mumble://[<username>[:<password>]@]<host>[:<port>][/<channel>[/<subchannel>...]][?version=<x.y.z>] +<plugin_list> is a list of plugin files that shall be installed The version query parameter has to be set in order to invoke the correct client version. It currently defaults to 1.2.0. @@ -6451,10 +6456,6 @@ Valid options are: Reconnect to last server on startup - - Download plugin and overlay updates on startup - - Privacy @@ -6480,6 +6481,14 @@ Prevents the client from sending potentially identifying information about the o Hide public server list + + Check for plugin updates on startup + + + + Automatically download and install plugin updates + + Overlay @@ -7030,30 +7039,188 @@ To upgrade these files to their latest versions, click the button below.Nom - Enabled + Enable + + + + PA + + + + KeyEvents + + + + Install a plugin from a local file + + + + Install plugin... + + + + Unload the currently selected plugin. This will remove it from the plugin list for the current session. + + + + Unload + + + + The plugin was installed successfully + + + + Unable to deactivate all requested features for plugin "%1" + + + + Unable to load plugin "%1" + + + + Whether the positional audio feature of this plugin should be enabled + + + + This plugin does not provide support for positional audio + + + + Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus + + + + Whether this plugin should be enabled + + + + + PluginInstaller + + PluginInstaller + + + + You are about to install the plugin listed below. Do you wish to proceed? + + + + <html><head/><body><p><span style=" font-weight:600;">Name:</span></p></body></html> + + + + <html><head/><body><p><span style=" font-weight:600;">Version:</span></p></body></html> + + + + <html><head/><body><p><span style=" font-weight:600;">Author(s):</span></p></body></html> + + + + <html><head/><body><p><span style=" font-weight:600;">Description:</span></p></body></html> + + + + &No + + + + &Yes + + + + The file "%1" is not a valid plugin file! + + + + Found more than one plugin library for the current OS in "%1" ("%2" and "%3")! + + + + Unable to find a plugin for the current OS in "%1" + + + + Unable to load plugin "%1" - check the plugin interface! + + + + Unable to delete old plugin at "%1" + + + + Unable to copy plugin library from "%1" to "%2" + + + + Unable to move plugin library to "%1" - Plugins + PluginManager + + %1 lost link + + + + %1 linked + + + + Plugin "%1" encountered a permanent error in positional data gathering + + + + Non-plugin found in plugin directory: "%1" + + + + Failed at loading manual plugin: %1 + + + + + PluginUpdater + + PluginUpdater + + + + The following plugins can be updated. + + + + Select all + + + + Plugin + + + + Download-URL + + - Skipping plugin update in debug mode. + Do you want to update the selected plugins? - Downloaded new or updated plugin to %1. + Unable to download plugin update for "%1" from "%2" (%3) - Failed to install new plugin to %1. + Update for plugin "%1" failed due to too many redirects - %1 lost link. + Unable to download plugin update for "%1" from "%2" (HTTP status code %3) - %1 linked. + Successfully updated plugin "%1" diff --git a/src/mumble/mumble_cs.ts b/src/mumble/mumble_cs.ts index 42502f9eeba..314fae9012d 100644 --- a/src/mumble/mumble_cs.ts +++ b/src/mumble/mumble_cs.ts @@ -3833,6 +3833,10 @@ Toto pole popisuje velikost LCD zařízení. Velikost je udávána buď v pixele User stopped listening to channel + + Plugin message + + LogConfig @@ -6181,12 +6185,13 @@ Valid actions are: - Usage: mumble [options] [<url>] + Usage: mumble [options] [<url> | <plugin_list>] <url> specifies a URL to connect to after startup instead of showing the connection window, and has the following form: mumble://[<username>[:<password>]@]<host>[:<port>][/<channel>[/<subchannel>...]][?version=<x.y.z>] +<plugin_list> is a list of plugin files that shall be installed The version query parameter has to be set in order to invoke the correct client version. It currently defaults to 1.2.0. @@ -6505,10 +6510,6 @@ Valid options are: Reconnect to last server on startup Při startu se znovu připojit na poslední server - - Download plugin and overlay updates on startup - Při startu stáhnout aktualizace zásuvných modulů a překryvů - Privacy @@ -6534,6 +6535,14 @@ Prevents the client from sending potentially identifying information about the o Hide public server list + + Check for plugin updates on startup + + + + Automatically download and install plugin updates + + Overlay @@ -7088,31 +7097,189 @@ Pro aktualizaci těchto souborů na jejich poslední verzi, klikněte na tlačí Jméno - Enabled - Povoleno + Enable + + + + PA + + + + KeyEvents + + + + Install a plugin from a local file + + + + Install plugin... + + + + Unload the currently selected plugin. This will remove it from the plugin list for the current session. + + + + Unload + + + + The plugin was installed successfully + + + + Unable to deactivate all requested features for plugin "%1" + + + + Unable to load plugin "%1" + + + + Whether the positional audio feature of this plugin should be enabled + + + + This plugin does not provide support for positional audio + + + + Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus + + + + Whether this plugin should be enabled + + + + + PluginInstaller + + PluginInstaller + + + + You are about to install the plugin listed below. Do you wish to proceed? + + + + <html><head/><body><p><span style=" font-weight:600;">Name:</span></p></body></html> + + + + <html><head/><body><p><span style=" font-weight:600;">Version:</span></p></body></html> + + + + <html><head/><body><p><span style=" font-weight:600;">Author(s):</span></p></body></html> + + + + <html><head/><body><p><span style=" font-weight:600;">Description:</span></p></body></html> + + + + &No + + + + &Yes + + + + The file "%1" is not a valid plugin file! + + + + Found more than one plugin library for the current OS in "%1" ("%2" and "%3")! + + + + Unable to find a plugin for the current OS in "%1" + + + + Unable to load plugin "%1" - check the plugin interface! + + + + Unable to delete old plugin at "%1" + + + + Unable to copy plugin library from "%1" to "%2" + + + + Unable to move plugin library to "%1" + - Plugins + PluginManager - Skipping plugin update in debug mode. - V režimu ladění přeskakuji aktualizaci zásuvných modulů. + %1 lost link + - Downloaded new or updated plugin to %1. - Aktualizován nebo stáhnut nový zásuvný modul do %1. + %1 linked + - Failed to install new plugin to %1. - Nelze instalovat nový zásuvný modul do %1. + Plugin "%1" encountered a permanent error in positional data gathering + - %1 lost link. - %1 ztraceno propojení. + Non-plugin found in plugin directory: "%1" + + + + Failed at loading manual plugin: %1 + + + + + PluginUpdater + + PluginUpdater + + + + The following plugins can be updated. + + + + Select all + + + + Plugin + - %1 linked. - %1 propojen. + Download-URL + + + + Do you want to update the selected plugins? + + + + Unable to download plugin update for "%1" from "%2" (%3) + + + + Update for plugin "%1" failed due to too many redirects + + + + Unable to download plugin update for "%1" from "%2" (HTTP status code %3) + + + + Successfully updated plugin "%1" + diff --git a/src/mumble/mumble_cy.ts b/src/mumble/mumble_cy.ts index c11f4b4e497..aec00f2a087 100644 --- a/src/mumble/mumble_cy.ts +++ b/src/mumble/mumble_cy.ts @@ -3782,6 +3782,10 @@ This field describes the size of an LCD device. The size is given either in pixe User stopped listening to channel + + Plugin message + + LogConfig @@ -6125,12 +6129,13 @@ Valid actions are: - Usage: mumble [options] [<url>] + Usage: mumble [options] [<url> | <plugin_list>] <url> specifies a URL to connect to after startup instead of showing the connection window, and has the following form: mumble://[<username>[:<password>]@]<host>[:<port>][/<channel>[/<subchannel>...]][?version=<x.y.z>] +<plugin_list> is a list of plugin files that shall be installed The version query parameter has to be set in order to invoke the correct client version. It currently defaults to 1.2.0. @@ -6449,10 +6454,6 @@ Valid options are: Reconnect to last server on startup Ailgysylltu i'r gweinydd olaf ar gychwyn - - Download plugin and overlay updates on startup - - Privacy @@ -6478,6 +6479,14 @@ Prevents the client from sending potentially identifying information about the o Hide public server list + + Check for plugin updates on startup + + + + Automatically download and install plugin updates + + Overlay @@ -7028,30 +7037,188 @@ To upgrade these files to their latest versions, click the button below.Enw - Enabled - Wedi'i alluogi + Enable + + + + PA + + + + KeyEvents + + + + Install a plugin from a local file + + + + Install plugin... + + + + Unload the currently selected plugin. This will remove it from the plugin list for the current session. + + + + Unload + + + + The plugin was installed successfully + + + + Unable to deactivate all requested features for plugin "%1" + + + + Unable to load plugin "%1" + + + + Whether the positional audio feature of this plugin should be enabled + + + + This plugin does not provide support for positional audio + + + + Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus + + + + Whether this plugin should be enabled + - Plugins + PluginInstaller + + PluginInstaller + + + + You are about to install the plugin listed below. Do you wish to proceed? + + + + <html><head/><body><p><span style=" font-weight:600;">Name:</span></p></body></html> + + + + <html><head/><body><p><span style=" font-weight:600;">Version:</span></p></body></html> + + + + <html><head/><body><p><span style=" font-weight:600;">Author(s):</span></p></body></html> + + + + <html><head/><body><p><span style=" font-weight:600;">Description:</span></p></body></html> + + + + &No + + + + &Yes + + + + The file "%1" is not a valid plugin file! + + + + Found more than one plugin library for the current OS in "%1" ("%2" and "%3")! + + + + Unable to find a plugin for the current OS in "%1" + + + + Unable to load plugin "%1" - check the plugin interface! + + + + Unable to delete old plugin at "%1" + + + + Unable to copy plugin library from "%1" to "%2" + + + + Unable to move plugin library to "%1" + + + + + PluginManager + + %1 lost link + + + + %1 linked + + + + Plugin "%1" encountered a permanent error in positional data gathering + + + + Non-plugin found in plugin directory: "%1" + + + + Failed at loading manual plugin: %1 + + + + + PluginUpdater + + PluginUpdater + + + + The following plugins can be updated. + + + + Select all + + + + Plugin + + + + Download-URL + + - Skipping plugin update in debug mode. + Do you want to update the selected plugins? - Downloaded new or updated plugin to %1. + Unable to download plugin update for "%1" from "%2" (%3) - Failed to install new plugin to %1. + Update for plugin "%1" failed due to too many redirects - %1 lost link. + Unable to download plugin update for "%1" from "%2" (HTTP status code %3) - %1 linked. + Successfully updated plugin "%1" diff --git a/src/mumble/mumble_da.ts b/src/mumble/mumble_da.ts index 30e9c1a6369..ba172287936 100644 --- a/src/mumble/mumble_da.ts +++ b/src/mumble/mumble_da.ts @@ -3831,6 +3831,10 @@ Dette felt beskriver størrelsen af en LCD-enhed. Størrelsen er enten opgivet i User stopped listening to channel + + Plugin message + + LogConfig @@ -6177,12 +6181,13 @@ Valid actions are: - Usage: mumble [options] [<url>] + Usage: mumble [options] [<url> | <plugin_list>] <url> specifies a URL to connect to after startup instead of showing the connection window, and has the following form: mumble://[<username>[:<password>]@]<host>[:<port>][/<channel>[/<subchannel>...]][?version=<x.y.z>] +<plugin_list> is a list of plugin files that shall be installed The version query parameter has to be set in order to invoke the correct client version. It currently defaults to 1.2.0. @@ -6501,10 +6506,6 @@ Valid options are: Reconnect to last server on startup Forbind igen til sidst anvendte server ved opstart - - Download plugin and overlay updates on startup - Download plugin- og overlægningsopdateringer ved opstart - Privacy @@ -6530,6 +6531,14 @@ Prevents the client from sending potentially identifying information about the o Hide public server list + + Check for plugin updates on startup + + + + Automatically download and install plugin updates + + Overlay @@ -7084,31 +7093,189 @@ For at opgradere disse filer til deres nyeste version, klik på knappen nedenfor Navn - Enabled - Aktiveret + Enable + + + + PA + + + + KeyEvents + + + + Install a plugin from a local file + + + + Install plugin... + + + + Unload the currently selected plugin. This will remove it from the plugin list for the current session. + + + + Unload + + + + The plugin was installed successfully + + + + Unable to deactivate all requested features for plugin "%1" + + + + Unable to load plugin "%1" + + + + Whether the positional audio feature of this plugin should be enabled + + + + This plugin does not provide support for positional audio + + + + Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus + + + + Whether this plugin should be enabled + + + + + PluginInstaller + + PluginInstaller + + + + You are about to install the plugin listed below. Do you wish to proceed? + + + + <html><head/><body><p><span style=" font-weight:600;">Name:</span></p></body></html> + + + + <html><head/><body><p><span style=" font-weight:600;">Version:</span></p></body></html> + + + + <html><head/><body><p><span style=" font-weight:600;">Author(s):</span></p></body></html> + + + + <html><head/><body><p><span style=" font-weight:600;">Description:</span></p></body></html> + + + + &No + + + + &Yes + + + + The file "%1" is not a valid plugin file! + + + + Found more than one plugin library for the current OS in "%1" ("%2" and "%3")! + + + + Unable to find a plugin for the current OS in "%1" + + + + Unable to load plugin "%1" - check the plugin interface! + + + + Unable to delete old plugin at "%1" + + + + Unable to copy plugin library from "%1" to "%2" + + + + Unable to move plugin library to "%1" + - Plugins + PluginManager - Skipping plugin update in debug mode. - Springer over pluginopdateringer i fejlfindingstilstand. + %1 lost link + - Downloaded new or updated plugin to %1. - Downloadede nyt eller opdateret plugin til %1. + %1 linked + - Failed to install new plugin to %1. - Installation af nyt plugin til %1 mislykkedes. + Plugin "%1" encountered a permanent error in positional data gathering + - %1 lost link. - %1 mistede link. + Non-plugin found in plugin directory: "%1" + + + + Failed at loading manual plugin: %1 + + + + + PluginUpdater + + PluginUpdater + + + + The following plugins can be updated. + + + + Select all + + + + Plugin + - %1 linked. - %1 er linket. + Download-URL + + + + Do you want to update the selected plugins? + + + + Unable to download plugin update for "%1" from "%2" (%3) + + + + Update for plugin "%1" failed due to too many redirects + + + + Unable to download plugin update for "%1" from "%2" (HTTP status code %3) + + + + Successfully updated plugin "%1" + diff --git a/src/mumble/mumble_de.ts b/src/mumble/mumble_de.ts index 7edbf414c40..84890cd96a9 100644 --- a/src/mumble/mumble_de.ts +++ b/src/mumble/mumble_de.ts @@ -3882,6 +3882,10 @@ This field describes the size of an LCD device. The size is given either in pixe User stopped listening to channel Benutzer hört dem Kanal nicht mehr zu + + Plugin message + + LogConfig @@ -6239,12 +6243,13 @@ Valid actions are: - Usage: mumble [options] [<url>] + Usage: mumble [options] [<url> | <plugin_list>] <url> specifies a URL to connect to after startup instead of showing the connection window, and has the following form: mumble://[<username>[:<password>]@]<host>[:<port>][/<channel>[/<subchannel>...]][?version=<x.y.z>] +<plugin_list> is a list of plugin files that shall be installed The version query parameter has to be set in order to invoke the correct client version. It currently defaults to 1.2.0. @@ -6566,10 +6571,6 @@ Mumble hat ein kleines Entwickler-Team. Deshalb muss die verfügbare Zeit auf di Reconnect to last server on startup Beim Start zum zuletzt benutzten Server verbinden - - Download plugin and overlay updates on startup - Plugin und Overlay Updates beim Starten herunterladen - Privacy Datenschutz @@ -6596,6 +6597,14 @@ Verhindert, dass potenziell identifizierende Informationen über das Betriebssys Hide public server list Öffentliche Serverliste verstecken + + Check for plugin updates on startup + + + + Automatically download and install plugin updates + + Overlay @@ -7151,31 +7160,189 @@ Um diese Dateien zu aktualisieren, klicken Sie unten den Button. Name - Enabled - Aktiviert + Enable + Aktivieren + + + PA + + + + KeyEvents + + + + Install a plugin from a local file + + + + Install plugin... + + + + Unload the currently selected plugin. This will remove it from the plugin list for the current session. + + + + Unload + + + + The plugin was installed successfully + + + + Unable to deactivate all requested features for plugin "%1" + + + + Unable to load plugin "%1" + + + + Whether the positional audio feature of this plugin should be enabled + + + + This plugin does not provide support for positional audio + + + + Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus + + + + Whether this plugin should be enabled + - Plugins + PluginInstaller - Skipping plugin update in debug mode. - Überspringe Plugin-Aktualisierung im Debug-Modus. + PluginInstaller + - Downloaded new or updated plugin to %1. - Neues oder aktualisiertes Plugin nach %1 heruntergeladen. + You are about to install the plugin listed below. Do you wish to proceed? + - Failed to install new plugin to %1. - Installation eines neuen Plugins nach %1 fehlgeschlagen. + <html><head/><body><p><span style=" font-weight:600;">Name:</span></p></body></html> + - %1 lost link. - %1 hat Verbindung verloren. + <html><head/><body><p><span style=" font-weight:600;">Version:</span></p></body></html> + + + + <html><head/><body><p><span style=" font-weight:600;">Author(s):</span></p></body></html> + + + + <html><head/><body><p><span style=" font-weight:600;">Description:</span></p></body></html> + + + + &No + + + + &Yes + - %1 linked. - %1 verbunden. + The file "%1" is not a valid plugin file! + + + + Found more than one plugin library for the current OS in "%1" ("%2" and "%3")! + + + + Unable to find a plugin for the current OS in "%1" + + + + Unable to load plugin "%1" - check the plugin interface! + + + + Unable to delete old plugin at "%1" + + + + Unable to copy plugin library from "%1" to "%2" + + + + Unable to move plugin library to "%1" + + + + + PluginManager + + %1 lost link + + + + %1 linked + + + + Plugin "%1" encountered a permanent error in positional data gathering + + + + Non-plugin found in plugin directory: "%1" + + + + Failed at loading manual plugin: %1 + + + + + PluginUpdater + + PluginUpdater + + + + The following plugins can be updated. + + + + Select all + + + + Plugin + + + + Download-URL + + + + Do you want to update the selected plugins? + + + + Unable to download plugin update for "%1" from "%2" (%3) + + + + Update for plugin "%1" failed due to too many redirects + + + + Unable to download plugin update for "%1" from "%2" (HTTP status code %3) + + + + Successfully updated plugin "%1" + diff --git a/src/mumble/mumble_el.ts b/src/mumble/mumble_el.ts index 7811bf35992..cf9b317c365 100644 --- a/src/mumble/mumble_el.ts +++ b/src/mumble/mumble_el.ts @@ -3835,6 +3835,10 @@ This field describes the size of an LCD device. The size is given either in pixe User stopped listening to channel + + Plugin message + + LogConfig @@ -6186,12 +6190,13 @@ Valid actions are: - Usage: mumble [options] [<url>] + Usage: mumble [options] [<url> | <plugin_list>] <url> specifies a URL to connect to after startup instead of showing the connection window, and has the following form: mumble://[<username>[:<password>]@]<host>[:<port>][/<channel>[/<subchannel>...]][?version=<x.y.z>] +<plugin_list> is a list of plugin files that shall be installed The version query parameter has to be set in order to invoke the correct client version. It currently defaults to 1.2.0. @@ -6510,10 +6515,6 @@ Valid options are: Reconnect to last server on startup Επανασύνδεση με τον τελευταίο διακομιστή που χρησιμοποιήθηκε κατά την εκκίνηση. - - Download plugin and overlay updates on startup - Να γίνεται λήψη ενημερώσεων για τα πρόσθετα και το overlay κατά την εκκίνηση - Privacy Ιδιωτικότητα @@ -6540,6 +6541,14 @@ Prevents the client from sending potentially identifying information about the o Hide public server list + + Check for plugin updates on startup + + + + Automatically download and install plugin updates + + Overlay @@ -7094,31 +7103,189 @@ To upgrade these files to their latest versions, click the button below.Όνομα - Enabled - Ενεργοποιημένο + Enable + + + + PA + + + + KeyEvents + + + + Install a plugin from a local file + + + + Install plugin... + + + + Unload the currently selected plugin. This will remove it from the plugin list for the current session. + + + + Unload + + + + The plugin was installed successfully + + + + Unable to deactivate all requested features for plugin "%1" + + + + Unable to load plugin "%1" + + + + Whether the positional audio feature of this plugin should be enabled + + + + This plugin does not provide support for positional audio + + + + Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus + + + + Whether this plugin should be enabled + + + + + PluginInstaller + + PluginInstaller + + + + You are about to install the plugin listed below. Do you wish to proceed? + + + + <html><head/><body><p><span style=" font-weight:600;">Name:</span></p></body></html> + + + + <html><head/><body><p><span style=" font-weight:600;">Version:</span></p></body></html> + + + + <html><head/><body><p><span style=" font-weight:600;">Author(s):</span></p></body></html> + + + + <html><head/><body><p><span style=" font-weight:600;">Description:</span></p></body></html> + + + + &No + + + + &Yes + + + + The file "%1" is not a valid plugin file! + + + + Found more than one plugin library for the current OS in "%1" ("%2" and "%3")! + + + + Unable to find a plugin for the current OS in "%1" + + + + Unable to load plugin "%1" - check the plugin interface! + + + + Unable to delete old plugin at "%1" + + + + Unable to copy plugin library from "%1" to "%2" + + + + Unable to move plugin library to "%1" + - Plugins + PluginManager - Skipping plugin update in debug mode. - Παράλειψη της ενημέρωσης των πρόσθετων στη λειτουργία εντοπισμού σφαλμάτων. + %1 lost link + - Downloaded new or updated plugin to %1. - Έγινε λήψη νέου ή ενημερωμένου πρόσθετου στο %1. + %1 linked + - Failed to install new plugin to %1. - Απέτυχε η εγκατάσταση του νέου πρόσθετου στο %1. + Plugin "%1" encountered a permanent error in positional data gathering + - %1 lost link. - %1 χαμένος σύνδεσμος. + Non-plugin found in plugin directory: "%1" + + + + Failed at loading manual plugin: %1 + + + + + PluginUpdater + + PluginUpdater + + + + The following plugins can be updated. + + + + Select all + + + + Plugin + - %1 linked. - %1 σύνδεσμος. + Download-URL + + + + Do you want to update the selected plugins? + + + + Unable to download plugin update for "%1" from "%2" (%3) + + + + Update for plugin "%1" failed due to too many redirects + + + + Unable to download plugin update for "%1" from "%2" (HTTP status code %3) + + + + Successfully updated plugin "%1" + diff --git a/src/mumble/mumble_en.ts b/src/mumble/mumble_en.ts index 7e1a89f066a..613dbc04f26 100644 --- a/src/mumble/mumble_en.ts +++ b/src/mumble/mumble_en.ts @@ -3777,6 +3777,10 @@ This field describes the size of an LCD device. The size is given either in pixe User stopped listening to channel + + Plugin message + + LogConfig @@ -6120,12 +6124,13 @@ Valid actions are: - Usage: mumble [options] [<url>] + Usage: mumble [options] [<url> | <plugin_list>] <url> specifies a URL to connect to after startup instead of showing the connection window, and has the following form: mumble://[<username>[:<password>]@]<host>[:<port>][/<channel>[/<subchannel>...]][?version=<x.y.z>] +<plugin_list> is a list of plugin files that shall be installed The version query parameter has to be set in order to invoke the correct client version. It currently defaults to 1.2.0. @@ -6444,10 +6449,6 @@ Valid options are: Reconnect to last server on startup - - Download plugin and overlay updates on startup - - Privacy @@ -6473,6 +6474,14 @@ Prevents the client from sending potentially identifying information about the o Hide public server list + + Check for plugin updates on startup + + + + Automatically download and install plugin updates + + Overlay @@ -7023,30 +7032,188 @@ To upgrade these files to their latest versions, click the button below. - Enabled + Enable + + + + PA + + + + KeyEvents + + + + Install a plugin from a local file + + + + Install plugin... + + + + Unload the currently selected plugin. This will remove it from the plugin list for the current session. + + + + Unload + + + + The plugin was installed successfully + + + + Unable to deactivate all requested features for plugin "%1" + + + + Unable to load plugin "%1" + + + + Whether the positional audio feature of this plugin should be enabled + + + + This plugin does not provide support for positional audio + + + + Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus + + + + Whether this plugin should be enabled + + + + + PluginInstaller + + PluginInstaller + + + + You are about to install the plugin listed below. Do you wish to proceed? + + + + <html><head/><body><p><span style=" font-weight:600;">Name:</span></p></body></html> + + + + <html><head/><body><p><span style=" font-weight:600;">Version:</span></p></body></html> + + + + <html><head/><body><p><span style=" font-weight:600;">Author(s):</span></p></body></html> + + + + <html><head/><body><p><span style=" font-weight:600;">Description:</span></p></body></html> + + + + &No + + + + &Yes + + + + The file "%1" is not a valid plugin file! + + + + Found more than one plugin library for the current OS in "%1" ("%2" and "%3")! + + + + Unable to find a plugin for the current OS in "%1" + + + + Unable to load plugin "%1" - check the plugin interface! + + + + Unable to delete old plugin at "%1" + + + + Unable to copy plugin library from "%1" to "%2" + + + + Unable to move plugin library to "%1" - Plugins + PluginManager + + %1 lost link + + + + %1 linked + + + + Plugin "%1" encountered a permanent error in positional data gathering + + + + Non-plugin found in plugin directory: "%1" + + + + Failed at loading manual plugin: %1 + + + + + PluginUpdater + + PluginUpdater + + + + The following plugins can be updated. + + + + Select all + + + + Plugin + + + + Download-URL + + - Skipping plugin update in debug mode. + Do you want to update the selected plugins? - Downloaded new or updated plugin to %1. + Unable to download plugin update for "%1" from "%2" (%3) - Failed to install new plugin to %1. + Update for plugin "%1" failed due to too many redirects - %1 lost link. + Unable to download plugin update for "%1" from "%2" (HTTP status code %3) - %1 linked. + Successfully updated plugin "%1" diff --git a/src/mumble/mumble_en_GB.ts b/src/mumble/mumble_en_GB.ts index da6fb0e53a5..57316ee31a4 100644 --- a/src/mumble/mumble_en_GB.ts +++ b/src/mumble/mumble_en_GB.ts @@ -3814,6 +3814,10 @@ This field describes the size of an LCD device. The size is given either in pixe User stopped listening to channel + + Plugin message + + LogConfig @@ -6157,12 +6161,13 @@ Valid actions are: - Usage: mumble [options] [<url>] + Usage: mumble [options] [<url> | <plugin_list>] <url> specifies a URL to connect to after startup instead of showing the connection window, and has the following form: mumble://[<username>[:<password>]@]<host>[:<port>][/<channel>[/<subchannel>...]][?version=<x.y.z>] +<plugin_list> is a list of plugin files that shall be installed The version query parameter has to be set in order to invoke the correct client version. It currently defaults to 1.2.0. @@ -6481,10 +6486,6 @@ Valid options are: Reconnect to last server on startup - - Download plugin and overlay updates on startup - - Privacy @@ -6510,6 +6511,14 @@ Prevents the client from sending potentially identifying information about the o Hide public server list + + Check for plugin updates on startup + + + + Automatically download and install plugin updates + + Overlay @@ -7060,30 +7069,188 @@ To upgrade these files to their latest versions, click the button below.Name - Enabled + Enable + + + + PA + + + + KeyEvents + + + + Install a plugin from a local file + + + + Install plugin... + + + + Unload the currently selected plugin. This will remove it from the plugin list for the current session. + + + + Unload + + + + The plugin was installed successfully + + + + Unable to deactivate all requested features for plugin "%1" + + + + Unable to load plugin "%1" + + + + Whether the positional audio feature of this plugin should be enabled + + + + This plugin does not provide support for positional audio + + + + Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus + + + + Whether this plugin should be enabled + + + + + PluginInstaller + + PluginInstaller + + + + You are about to install the plugin listed below. Do you wish to proceed? + + + + <html><head/><body><p><span style=" font-weight:600;">Name:</span></p></body></html> + + + + <html><head/><body><p><span style=" font-weight:600;">Version:</span></p></body></html> + + + + <html><head/><body><p><span style=" font-weight:600;">Author(s):</span></p></body></html> + + + + <html><head/><body><p><span style=" font-weight:600;">Description:</span></p></body></html> + + + + &No + + + + &Yes + + + + The file "%1" is not a valid plugin file! + + + + Found more than one plugin library for the current OS in "%1" ("%2" and "%3")! + + + + Unable to find a plugin for the current OS in "%1" + + + + Unable to load plugin "%1" - check the plugin interface! + + + + Unable to delete old plugin at "%1" + + + + Unable to copy plugin library from "%1" to "%2" + + + + Unable to move plugin library to "%1" - Plugins + PluginManager + + %1 lost link + + + + %1 linked + + + + Plugin "%1" encountered a permanent error in positional data gathering + + + + Non-plugin found in plugin directory: "%1" + + + + Failed at loading manual plugin: %1 + + + + + PluginUpdater + + PluginUpdater + + + + The following plugins can be updated. + + + + Select all + + + + Plugin + + + + Download-URL + + - Skipping plugin update in debug mode. + Do you want to update the selected plugins? - Downloaded new or updated plugin to %1. + Unable to download plugin update for "%1" from "%2" (%3) - Failed to install new plugin to %1. + Update for plugin "%1" failed due to too many redirects - %1 lost link. + Unable to download plugin update for "%1" from "%2" (HTTP status code %3) - %1 linked. + Successfully updated plugin "%1" diff --git a/src/mumble/mumble_eo.ts b/src/mumble/mumble_eo.ts index a1464936ec6..a890df51c26 100644 --- a/src/mumble/mumble_eo.ts +++ b/src/mumble/mumble_eo.ts @@ -3786,6 +3786,10 @@ This field describes the size of an LCD device. The size is given either in pixe User stopped listening to channel + + Plugin message + + LogConfig @@ -6130,12 +6134,13 @@ Valid actions are: - Usage: mumble [options] [<url>] + Usage: mumble [options] [<url> | <plugin_list>] <url> specifies a URL to connect to after startup instead of showing the connection window, and has the following form: mumble://[<username>[:<password>]@]<host>[:<port>][/<channel>[/<subchannel>...]][?version=<x.y.z>] +<plugin_list> is a list of plugin files that shall be installed The version query parameter has to be set in order to invoke the correct client version. It currently defaults to 1.2.0. @@ -6454,10 +6459,6 @@ Valid options are: Reconnect to last server on startup - - Download plugin and overlay updates on startup - - Privacy Privateco @@ -6483,6 +6484,14 @@ Prevents the client from sending potentially identifying information about the o Hide public server list Kaŝi la publikan servilaliston + + Check for plugin updates on startup + + + + Automatically download and install plugin updates + + Overlay @@ -7033,30 +7042,188 @@ To upgrade these files to their latest versions, click the button below.Nomo - Enabled - Enŝaltite + Enable + + + + PA + + + + KeyEvents + + + + Install a plugin from a local file + + + + Install plugin... + + + + Unload the currently selected plugin. This will remove it from the plugin list for the current session. + + + + Unload + + + + The plugin was installed successfully + + + + Unable to deactivate all requested features for plugin "%1" + + + + Unable to load plugin "%1" + + + + Whether the positional audio feature of this plugin should be enabled + + + + This plugin does not provide support for positional audio + + + + Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus + + + + Whether this plugin should be enabled + - Plugins + PluginInstaller + + PluginInstaller + + + + You are about to install the plugin listed below. Do you wish to proceed? + + + + <html><head/><body><p><span style=" font-weight:600;">Name:</span></p></body></html> + + + + <html><head/><body><p><span style=" font-weight:600;">Version:</span></p></body></html> + + + + <html><head/><body><p><span style=" font-weight:600;">Author(s):</span></p></body></html> + + + + <html><head/><body><p><span style=" font-weight:600;">Description:</span></p></body></html> + + + + &No + + + + &Yes + + + + The file "%1" is not a valid plugin file! + + + + Found more than one plugin library for the current OS in "%1" ("%2" and "%3")! + + + + Unable to find a plugin for the current OS in "%1" + + + + Unable to load plugin "%1" - check the plugin interface! + + + + Unable to delete old plugin at "%1" + + + + Unable to copy plugin library from "%1" to "%2" + + + + Unable to move plugin library to "%1" + + + + + PluginManager + + %1 lost link + + + + %1 linked + + + + Plugin "%1" encountered a permanent error in positional data gathering + + + + Non-plugin found in plugin directory: "%1" + + + + Failed at loading manual plugin: %1 + + + + + PluginUpdater + + PluginUpdater + + + + The following plugins can be updated. + + + + Select all + + + + Plugin + + + + Download-URL + + - Skipping plugin update in debug mode. + Do you want to update the selected plugins? - Downloaded new or updated plugin to %1. + Unable to download plugin update for "%1" from "%2" (%3) - Failed to install new plugin to %1. + Update for plugin "%1" failed due to too many redirects - %1 lost link. + Unable to download plugin update for "%1" from "%2" (HTTP status code %3) - %1 linked. + Successfully updated plugin "%1" diff --git a/src/mumble/mumble_es.ts b/src/mumble/mumble_es.ts index e5b038651da..6c5a81e0489 100644 --- a/src/mumble/mumble_es.ts +++ b/src/mumble/mumble_es.ts @@ -3838,6 +3838,10 @@ Este campo describe el tamaño de un dispositivo LCD. El tamaño se da, o bien e User stopped listening to channel El usuario dejó de escuchar en su canal + + Plugin message + + LogConfig @@ -6191,12 +6195,13 @@ Valid actions are: - Usage: mumble [options] [<url>] + Usage: mumble [options] [<url> | <plugin_list>] <url> specifies a URL to connect to after startup instead of showing the connection window, and has the following form: mumble://[<username>[:<password>]@]<host>[:<port>][/<channel>[/<subchannel>...]][?version=<x.y.z>] +<plugin_list> is a list of plugin files that shall be installed The version query parameter has to be set in order to invoke the correct client version. It currently defaults to 1.2.0. @@ -6515,10 +6520,6 @@ Valid options are: Reconnect to last server on startup Reconectar al último servidor al inicio - - Download plugin and overlay updates on startup - Descargar actualizaciones de los complementos y la superposición al inicio - Privacy Privacidad @@ -6545,6 +6546,14 @@ Impide que el cliente envíe información potencialmente identificable sobre el Hide public server list Esconder la lista de servidores públicos + + Check for plugin updates on startup + + + + Automatically download and install plugin updates + + Overlay @@ -7099,31 +7108,189 @@ Para actualizar estos ficheros a la última versión, haga clic en el botón inf Nombre - Enabled - Habilitado + Enable + Habilitar + + + PA + + + + KeyEvents + + + + Install a plugin from a local file + + + + Install plugin... + + + + Unload the currently selected plugin. This will remove it from the plugin list for the current session. + + + + Unload + + + + The plugin was installed successfully + + + + Unable to deactivate all requested features for plugin "%1" + + + + Unable to load plugin "%1" + + + + Whether the positional audio feature of this plugin should be enabled + + + + This plugin does not provide support for positional audio + + + + Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus + + + + Whether this plugin should be enabled + - Plugins + PluginInstaller - Skipping plugin update in debug mode. - Omitiendo la actualización de los complementos en el modo de depuración. + PluginInstaller + - Downloaded new or updated plugin to %1. - Se ha descargado un complemento nuevo o actualizado para %1. + You are about to install the plugin listed below. Do you wish to proceed? + - Failed to install new plugin to %1. - No se pudo instalar un nuevo complemento para %1. + <html><head/><body><p><span style=" font-weight:600;">Name:</span></p></body></html> + - %1 lost link. - %1 perdió el vínculo. + <html><head/><body><p><span style=" font-weight:600;">Version:</span></p></body></html> + + + + <html><head/><body><p><span style=" font-weight:600;">Author(s):</span></p></body></html> + + + + <html><head/><body><p><span style=" font-weight:600;">Description:</span></p></body></html> + + + + &No + + + + &Yes + - %1 linked. - %1 vinculado. + The file "%1" is not a valid plugin file! + + + + Found more than one plugin library for the current OS in "%1" ("%2" and "%3")! + + + + Unable to find a plugin for the current OS in "%1" + + + + Unable to load plugin "%1" - check the plugin interface! + + + + Unable to delete old plugin at "%1" + + + + Unable to copy plugin library from "%1" to "%2" + + + + Unable to move plugin library to "%1" + + + + + PluginManager + + %1 lost link + + + + %1 linked + + + + Plugin "%1" encountered a permanent error in positional data gathering + + + + Non-plugin found in plugin directory: "%1" + + + + Failed at loading manual plugin: %1 + + + + + PluginUpdater + + PluginUpdater + + + + The following plugins can be updated. + + + + Select all + + + + Plugin + + + + Download-URL + + + + Do you want to update the selected plugins? + + + + Unable to download plugin update for "%1" from "%2" (%3) + + + + Update for plugin "%1" failed due to too many redirects + + + + Unable to download plugin update for "%1" from "%2" (HTTP status code %3) + + + + Successfully updated plugin "%1" + diff --git a/src/mumble/mumble_et.ts b/src/mumble/mumble_et.ts index 433d76db2fb..d9018dc3462 100644 --- a/src/mumble/mumble_et.ts +++ b/src/mumble/mumble_et.ts @@ -3779,6 +3779,10 @@ This field describes the size of an LCD device. The size is given either in pixe User stopped listening to channel + + Plugin message + + LogConfig @@ -6122,12 +6126,13 @@ Valid actions are: - Usage: mumble [options] [<url>] + Usage: mumble [options] [<url> | <plugin_list>] <url> specifies a URL to connect to after startup instead of showing the connection window, and has the following form: mumble://[<username>[:<password>]@]<host>[:<port>][/<channel>[/<subchannel>...]][?version=<x.y.z>] +<plugin_list> is a list of plugin files that shall be installed The version query parameter has to be set in order to invoke the correct client version. It currently defaults to 1.2.0. @@ -6446,10 +6451,6 @@ Valid options are: Reconnect to last server on startup - - Download plugin and overlay updates on startup - - Privacy Privaatsus @@ -6475,6 +6476,14 @@ Prevents the client from sending potentially identifying information about the o Hide public server list + + Check for plugin updates on startup + + + + Automatically download and install plugin updates + + Overlay @@ -7025,31 +7034,189 @@ To upgrade these files to their latest versions, click the button below.Nimi - Enabled - Lubatud + Enable + Luba + + + PA + + + + KeyEvents + + + + Install a plugin from a local file + + + + Install plugin... + + + + Unload the currently selected plugin. This will remove it from the plugin list for the current session. + + + + Unload + + + + The plugin was installed successfully + + + + Unable to deactivate all requested features for plugin "%1" + + + + Unable to load plugin "%1" + + + + Whether the positional audio feature of this plugin should be enabled + + + + This plugin does not provide support for positional audio + + + + Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus + + + + Whether this plugin should be enabled + + + + + PluginInstaller + + PluginInstaller + + + + You are about to install the plugin listed below. Do you wish to proceed? + + + + <html><head/><body><p><span style=" font-weight:600;">Name:</span></p></body></html> + + + + <html><head/><body><p><span style=" font-weight:600;">Version:</span></p></body></html> + + + + <html><head/><body><p><span style=" font-weight:600;">Author(s):</span></p></body></html> + + + + <html><head/><body><p><span style=" font-weight:600;">Description:</span></p></body></html> + + + + &No + + + + &Yes + + + + The file "%1" is not a valid plugin file! + + + + Found more than one plugin library for the current OS in "%1" ("%2" and "%3")! + + + + Unable to find a plugin for the current OS in "%1" + + + + Unable to load plugin "%1" - check the plugin interface! + + + + Unable to delete old plugin at "%1" + + + + Unable to copy plugin library from "%1" to "%2" + + + + Unable to move plugin library to "%1" + - Plugins + PluginManager - Skipping plugin update in debug mode. + %1 lost link - Downloaded new or updated plugin to %1. + %1 linked - Failed to install new plugin to %1. + Plugin "%1" encountered a permanent error in positional data gathering - %1 lost link. + Non-plugin found in plugin directory: "%1" - %1 linked. - %1 lingitud. + Failed at loading manual plugin: %1 + + + + + PluginUpdater + + PluginUpdater + + + + The following plugins can be updated. + + + + Select all + + + + Plugin + + + + Download-URL + + + + Do you want to update the selected plugins? + + + + Unable to download plugin update for "%1" from "%2" (%3) + + + + Update for plugin "%1" failed due to too many redirects + + + + Unable to download plugin update for "%1" from "%2" (HTTP status code %3) + + + + Successfully updated plugin "%1" + diff --git a/src/mumble/mumble_eu.ts b/src/mumble/mumble_eu.ts index 064227e18cc..0226d765b2f 100644 --- a/src/mumble/mumble_eu.ts +++ b/src/mumble/mumble_eu.ts @@ -3796,6 +3796,10 @@ This field describes the size of an LCD device. The size is given either in pixe User stopped listening to channel + + Plugin message + + LogConfig @@ -6141,12 +6145,13 @@ Valid actions are: - Usage: mumble [options] [<url>] + Usage: mumble [options] [<url> | <plugin_list>] <url> specifies a URL to connect to after startup instead of showing the connection window, and has the following form: mumble://[<username>[:<password>]@]<host>[:<port>][/<channel>[/<subchannel>...]][?version=<x.y.z>] +<plugin_list> is a list of plugin files that shall be installed The version query parameter has to be set in order to invoke the correct client version. It currently defaults to 1.2.0. @@ -6465,10 +6470,6 @@ Valid options are: Reconnect to last server on startup Birkonektatu azken zerbitzarira hastean - - Download plugin and overlay updates on startup - - Privacy @@ -6494,6 +6495,14 @@ Prevents the client from sending potentially identifying information about the o Hide public server list + + Check for plugin updates on startup + + + + Automatically download and install plugin updates + + Overlay @@ -7044,31 +7053,189 @@ To upgrade these files to their latest versions, click the button below.Izena - Enabled - Gaituta + Enable + + + + PA + + + + KeyEvents + + + + Install a plugin from a local file + + + + Install plugin... + + + + Unload the currently selected plugin. This will remove it from the plugin list for the current session. + + + + Unload + + + + The plugin was installed successfully + + + + Unable to deactivate all requested features for plugin "%1" + + + + Unable to load plugin "%1" + + + + Whether the positional audio feature of this plugin should be enabled + + + + This plugin does not provide support for positional audio + + + + Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus + + + + Whether this plugin should be enabled + + + + + PluginInstaller + + PluginInstaller + + + + You are about to install the plugin listed below. Do you wish to proceed? + + + + <html><head/><body><p><span style=" font-weight:600;">Name:</span></p></body></html> + + + + <html><head/><body><p><span style=" font-weight:600;">Version:</span></p></body></html> + + + + <html><head/><body><p><span style=" font-weight:600;">Author(s):</span></p></body></html> + + + + <html><head/><body><p><span style=" font-weight:600;">Description:</span></p></body></html> + + + + &No + + + + &Yes + + + + The file "%1" is not a valid plugin file! + + + + Found more than one plugin library for the current OS in "%1" ("%2" and "%3")! + + + + Unable to find a plugin for the current OS in "%1" + + + + Unable to load plugin "%1" - check the plugin interface! + + + + Unable to delete old plugin at "%1" + + + + Unable to copy plugin library from "%1" to "%2" + + + + Unable to move plugin library to "%1" + - Plugins + PluginManager - Skipping plugin update in debug mode. + %1 lost link - Downloaded new or updated plugin to %1. - Eguneratutako edo berria den gehigarria deskargatuta %1 -era. + %1 linked + - Failed to install new plugin to %1. - %1-era gehigarri berria instalatzean errorea. + Plugin "%1" encountered a permanent error in positional data gathering + - %1 lost link. - %1 lotura galdua. + Non-plugin found in plugin directory: "%1" + + + + Failed at loading manual plugin: %1 + + + + + PluginUpdater + + PluginUpdater + + + + The following plugins can be updated. + + + + Select all + + + + Plugin + - %1 linked. - %1 lotuta. + Download-URL + + + + Do you want to update the selected plugins? + + + + Unable to download plugin update for "%1" from "%2" (%3) + + + + Update for plugin "%1" failed due to too many redirects + + + + Unable to download plugin update for "%1" from "%2" (HTTP status code %3) + + + + Successfully updated plugin "%1" + diff --git a/src/mumble/mumble_fa_IR.ts b/src/mumble/mumble_fa_IR.ts index 4e99022cb83..c4265df793f 100644 --- a/src/mumble/mumble_fa_IR.ts +++ b/src/mumble/mumble_fa_IR.ts @@ -3777,6 +3777,10 @@ This field describes the size of an LCD device. The size is given either in pixe User stopped listening to channel + + Plugin message + + LogConfig @@ -6120,12 +6124,13 @@ Valid actions are: - Usage: mumble [options] [<url>] + Usage: mumble [options] [<url> | <plugin_list>] <url> specifies a URL to connect to after startup instead of showing the connection window, and has the following form: mumble://[<username>[:<password>]@]<host>[:<port>][/<channel>[/<subchannel>...]][?version=<x.y.z>] +<plugin_list> is a list of plugin files that shall be installed The version query parameter has to be set in order to invoke the correct client version. It currently defaults to 1.2.0. @@ -6444,10 +6449,6 @@ Valid options are: Reconnect to last server on startup - - Download plugin and overlay updates on startup - - Privacy @@ -6473,6 +6474,14 @@ Prevents the client from sending potentially identifying information about the o Hide public server list + + Check for plugin updates on startup + + + + Automatically download and install plugin updates + + Overlay @@ -7023,30 +7032,188 @@ To upgrade these files to their latest versions, click the button below.نام - Enabled + Enable + + + + PA + + + + KeyEvents + + + + Install a plugin from a local file + + + + Install plugin... + + + + Unload the currently selected plugin. This will remove it from the plugin list for the current session. + + + + Unload + + + + The plugin was installed successfully + + + + Unable to deactivate all requested features for plugin "%1" + + + + Unable to load plugin "%1" + + + + Whether the positional audio feature of this plugin should be enabled + + + + This plugin does not provide support for positional audio + + + + Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus + + + + Whether this plugin should be enabled + + + + + PluginInstaller + + PluginInstaller + + + + You are about to install the plugin listed below. Do you wish to proceed? + + + + <html><head/><body><p><span style=" font-weight:600;">Name:</span></p></body></html> + + + + <html><head/><body><p><span style=" font-weight:600;">Version:</span></p></body></html> + + + + <html><head/><body><p><span style=" font-weight:600;">Author(s):</span></p></body></html> + + + + <html><head/><body><p><span style=" font-weight:600;">Description:</span></p></body></html> + + + + &No + + + + &Yes + + + + The file "%1" is not a valid plugin file! + + + + Found more than one plugin library for the current OS in "%1" ("%2" and "%3")! + + + + Unable to find a plugin for the current OS in "%1" + + + + Unable to load plugin "%1" - check the plugin interface! + + + + Unable to delete old plugin at "%1" + + + + Unable to copy plugin library from "%1" to "%2" + + + + Unable to move plugin library to "%1" - Plugins + PluginManager + + %1 lost link + + + + %1 linked + + + + Plugin "%1" encountered a permanent error in positional data gathering + + + + Non-plugin found in plugin directory: "%1" + + + + Failed at loading manual plugin: %1 + + + + + PluginUpdater + + PluginUpdater + + + + The following plugins can be updated. + + + + Select all + + + + Plugin + + + + Download-URL + + - Skipping plugin update in debug mode. + Do you want to update the selected plugins? - Downloaded new or updated plugin to %1. + Unable to download plugin update for "%1" from "%2" (%3) - Failed to install new plugin to %1. + Update for plugin "%1" failed due to too many redirects - %1 lost link. + Unable to download plugin update for "%1" from "%2" (HTTP status code %3) - %1 linked. + Successfully updated plugin "%1" diff --git a/src/mumble/mumble_fi.ts b/src/mumble/mumble_fi.ts index 4ca19111ab1..cf8299c65e2 100644 --- a/src/mumble/mumble_fi.ts +++ b/src/mumble/mumble_fi.ts @@ -3838,6 +3838,10 @@ Kenttä kuvaa LCD-laitteen koon. Koko annetaan joko pikseleinä (graafinen LCD) User stopped listening to channel Käyttäjä lopetti kanavan kuuntelun + + Plugin message + + LogConfig @@ -6231,12 +6235,13 @@ Päteviä valintoja ovat: - Usage: mumble [options] [<url>] + Usage: mumble [options] [<url> | <plugin_list>] <url> specifies a URL to connect to after startup instead of showing the connection window, and has the following form: mumble://[<username>[:<password>]@]<host>[:<port>][/<channel>[/<subchannel>...]][?version=<x.y.z>] +<plugin_list> is a list of plugin files that shall be installed The version query parameter has to be set in order to invoke the correct client version. It currently defaults to 1.2.0. @@ -6555,10 +6560,6 @@ Valid options are: Reconnect to last server on startup Yhdistä viimeisimpään palvelimeen käynnistymisen yhteydessä - - Download plugin and overlay updates on startup - Lataa liitännäisten ja overlayn päivitykset ohjelman käynnistymisen yhteydessä - Privacy Yksityisyys @@ -6585,6 +6586,14 @@ Estää mahdollisesti tunnistamista helpottavien tietojen, koskien käyttöjärj Hide public server list Piilota julkisten palvelinten lista + + Check for plugin updates on startup + + + + Automatically download and install plugin updates + + Overlay @@ -7139,31 +7148,189 @@ Paina alapuolen napista päivittääksesi Overlayn tiedostot viimeisimpään ver Nimi - Enabled - Käytössä + Enable + Käytä + + + PA + + + + KeyEvents + + + + Install a plugin from a local file + + + + Install plugin... + + + + Unload the currently selected plugin. This will remove it from the plugin list for the current session. + + + + Unload + + + + The plugin was installed successfully + + + + Unable to deactivate all requested features for plugin "%1" + + + + Unable to load plugin "%1" + + + + Whether the positional audio feature of this plugin should be enabled + + + + This plugin does not provide support for positional audio + + + + Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus + + + + Whether this plugin should be enabled + - Plugins + PluginInstaller - Skipping plugin update in debug mode. - Liitännäispäivitys ohitetaan debug-tilassa. + PluginInstaller + - Downloaded new or updated plugin to %1. - Ladattiin uusi tai päivitettiin liitännäinnen %1. + You are about to install the plugin listed below. Do you wish to proceed? + - Failed to install new plugin to %1. - Uuden liitännäisen asennus epäonnistui %1. + <html><head/><body><p><span style=" font-weight:600;">Name:</span></p></body></html> + - %1 lost link. - %1 yhteys kadotettu. + <html><head/><body><p><span style=" font-weight:600;">Version:</span></p></body></html> + + + + <html><head/><body><p><span style=" font-weight:600;">Author(s):</span></p></body></html> + + + + <html><head/><body><p><span style=" font-weight:600;">Description:</span></p></body></html> + + + + &No + + + + &Yes + - %1 linked. - %1 yhdistetty. + The file "%1" is not a valid plugin file! + + + + Found more than one plugin library for the current OS in "%1" ("%2" and "%3")! + + + + Unable to find a plugin for the current OS in "%1" + + + + Unable to load plugin "%1" - check the plugin interface! + + + + Unable to delete old plugin at "%1" + + + + Unable to copy plugin library from "%1" to "%2" + + + + Unable to move plugin library to "%1" + + + + + PluginManager + + %1 lost link + + + + %1 linked + + + + Plugin "%1" encountered a permanent error in positional data gathering + + + + Non-plugin found in plugin directory: "%1" + + + + Failed at loading manual plugin: %1 + + + + + PluginUpdater + + PluginUpdater + + + + The following plugins can be updated. + + + + Select all + + + + Plugin + + + + Download-URL + + + + Do you want to update the selected plugins? + + + + Unable to download plugin update for "%1" from "%2" (%3) + + + + Update for plugin "%1" failed due to too many redirects + + + + Unable to download plugin update for "%1" from "%2" (HTTP status code %3) + + + + Successfully updated plugin "%1" + diff --git a/src/mumble/mumble_fr.ts b/src/mumble/mumble_fr.ts index e7f7b56269b..b3588e8eda3 100644 --- a/src/mumble/mumble_fr.ts +++ b/src/mumble/mumble_fr.ts @@ -3837,6 +3837,10 @@ This field describes the size of an LCD device. The size is given either in pixe User stopped listening to channel L'utilisateur a cessé d'écouter le salon + + Plugin message + + LogConfig @@ -6191,12 +6195,13 @@ Valid actions are: - Usage: mumble [options] [<url>] + Usage: mumble [options] [<url> | <plugin_list>] <url> specifies a URL to connect to after startup instead of showing the connection window, and has the following form: mumble://[<username>[:<password>]@]<host>[:<port>][/<channel>[/<subchannel>...]][?version=<x.y.z>] +<plugin_list> is a list of plugin files that shall be installed The version query parameter has to be set in order to invoke the correct client version. It currently defaults to 1.2.0. @@ -6515,10 +6520,6 @@ Valid options are: Reconnect to last server on startup Se reconnecter au dernier serveur utilisé au démarrage - - Download plugin and overlay updates on startup - Télécharger les mises à jour des plugins et de l'overlay au démarrage - Privacy Confidentialité @@ -6545,6 +6546,14 @@ Empêche le client d'envoyer des informations pouvant identifier le systèm Hide public server list Cacher la liste des serveurs publics + + Check for plugin updates on startup + + + + Automatically download and install plugin updates + + Overlay @@ -7099,31 +7108,189 @@ Pour mettre à jour l'overlay, cliquez sur le bouton ci-dessous.Nom - Enabled - Activé + Enable + Activer + + + PA + + + + KeyEvents + + + + Install a plugin from a local file + + + + Install plugin... + + + + Unload the currently selected plugin. This will remove it from the plugin list for the current session. + + + + Unload + + + + The plugin was installed successfully + + + + Unable to deactivate all requested features for plugin "%1" + + + + Unable to load plugin "%1" + + + + Whether the positional audio feature of this plugin should be enabled + + + + This plugin does not provide support for positional audio + + + + Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus + + + + Whether this plugin should be enabled + - Plugins + PluginInstaller - Skipping plugin update in debug mode. - Passe la mise à jour des plugins en mode débogage. + PluginInstaller + - Downloaded new or updated plugin to %1. - Téléchargé le nouveau plugin ou mis à jour vers %1. + You are about to install the plugin listed below. Do you wish to proceed? + - Failed to install new plugin to %1. - Échec de l'installation du nouveau plugin dans %1. + <html><head/><body><p><span style=" font-weight:600;">Name:</span></p></body></html> + - %1 lost link. - %1 est désactivé. + <html><head/><body><p><span style=" font-weight:600;">Version:</span></p></body></html> + + + + <html><head/><body><p><span style=" font-weight:600;">Author(s):</span></p></body></html> + + + + <html><head/><body><p><span style=" font-weight:600;">Description:</span></p></body></html> + + + + &No + + + + &Yes + - %1 linked. - %1 est activé. + The file "%1" is not a valid plugin file! + + + + Found more than one plugin library for the current OS in "%1" ("%2" and "%3")! + + + + Unable to find a plugin for the current OS in "%1" + + + + Unable to load plugin "%1" - check the plugin interface! + + + + Unable to delete old plugin at "%1" + + + + Unable to copy plugin library from "%1" to "%2" + + + + Unable to move plugin library to "%1" + + + + + PluginManager + + %1 lost link + + + + %1 linked + + + + Plugin "%1" encountered a permanent error in positional data gathering + + + + Non-plugin found in plugin directory: "%1" + + + + Failed at loading manual plugin: %1 + + + + + PluginUpdater + + PluginUpdater + + + + The following plugins can be updated. + + + + Select all + + + + Plugin + + + + Download-URL + + + + Do you want to update the selected plugins? + + + + Unable to download plugin update for "%1" from "%2" (%3) + + + + Update for plugin "%1" failed due to too many redirects + + + + Unable to download plugin update for "%1" from "%2" (HTTP status code %3) + + + + Successfully updated plugin "%1" + diff --git a/src/mumble/mumble_gl.ts b/src/mumble/mumble_gl.ts index 1ec28ac1a26..ea07d1be1d7 100644 --- a/src/mumble/mumble_gl.ts +++ b/src/mumble/mumble_gl.ts @@ -3780,6 +3780,10 @@ This field describes the size of an LCD device. The size is given either in pixe User stopped listening to channel + + Plugin message + + LogConfig @@ -6123,12 +6127,13 @@ Valid actions are: - Usage: mumble [options] [<url>] + Usage: mumble [options] [<url> | <plugin_list>] <url> specifies a URL to connect to after startup instead of showing the connection window, and has the following form: mumble://[<username>[:<password>]@]<host>[:<port>][/<channel>[/<subchannel>...]][?version=<x.y.z>] +<plugin_list> is a list of plugin files that shall be installed The version query parameter has to be set in order to invoke the correct client version. It currently defaults to 1.2.0. @@ -6447,10 +6452,6 @@ Valid options are: Reconnect to last server on startup - - Download plugin and overlay updates on startup - - Privacy @@ -6476,6 +6477,14 @@ Prevents the client from sending potentially identifying information about the o Hide public server list + + Check for plugin updates on startup + + + + Automatically download and install plugin updates + + Overlay @@ -7026,30 +7035,188 @@ To upgrade these files to their latest versions, click the button below.Nome - Enabled + Enable + + + + PA + + + + KeyEvents + + + + Install a plugin from a local file + + + + Install plugin... + + + + Unload the currently selected plugin. This will remove it from the plugin list for the current session. + + + + Unload + + + + The plugin was installed successfully + + + + Unable to deactivate all requested features for plugin "%1" + + + + Unable to load plugin "%1" + + + + Whether the positional audio feature of this plugin should be enabled + + + + This plugin does not provide support for positional audio + + + + Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus + + + + Whether this plugin should be enabled + + + + + PluginInstaller + + PluginInstaller + + + + You are about to install the plugin listed below. Do you wish to proceed? + + + + <html><head/><body><p><span style=" font-weight:600;">Name:</span></p></body></html> + + + + <html><head/><body><p><span style=" font-weight:600;">Version:</span></p></body></html> + + + + <html><head/><body><p><span style=" font-weight:600;">Author(s):</span></p></body></html> + + + + <html><head/><body><p><span style=" font-weight:600;">Description:</span></p></body></html> + + + + &No + + + + &Yes + + + + The file "%1" is not a valid plugin file! + + + + Found more than one plugin library for the current OS in "%1" ("%2" and "%3")! + + + + Unable to find a plugin for the current OS in "%1" + + + + Unable to load plugin "%1" - check the plugin interface! + + + + Unable to delete old plugin at "%1" + + + + Unable to copy plugin library from "%1" to "%2" + + + + Unable to move plugin library to "%1" - Plugins + PluginManager + + %1 lost link + + + + %1 linked + + + + Plugin "%1" encountered a permanent error in positional data gathering + + + + Non-plugin found in plugin directory: "%1" + + + + Failed at loading manual plugin: %1 + + + + + PluginUpdater + + PluginUpdater + + + + The following plugins can be updated. + + + + Select all + + + + Plugin + + + + Download-URL + + - Skipping plugin update in debug mode. + Do you want to update the selected plugins? - Downloaded new or updated plugin to %1. + Unable to download plugin update for "%1" from "%2" (%3) - Failed to install new plugin to %1. + Update for plugin "%1" failed due to too many redirects - %1 lost link. + Unable to download plugin update for "%1" from "%2" (HTTP status code %3) - %1 linked. + Successfully updated plugin "%1" diff --git a/src/mumble/mumble_he.ts b/src/mumble/mumble_he.ts index 454417f24c8..4b1d04a8932 100644 --- a/src/mumble/mumble_he.ts +++ b/src/mumble/mumble_he.ts @@ -3829,6 +3829,10 @@ This field describes the size of an LCD device. The size is given either in pixe User stopped listening to channel + + Plugin message + + LogConfig @@ -6174,12 +6178,13 @@ Valid actions are: - Usage: mumble [options] [<url>] + Usage: mumble [options] [<url> | <plugin_list>] <url> specifies a URL to connect to after startup instead of showing the connection window, and has the following form: mumble://[<username>[:<password>]@]<host>[:<port>][/<channel>[/<subchannel>...]][?version=<x.y.z>] +<plugin_list> is a list of plugin files that shall be installed The version query parameter has to be set in order to invoke the correct client version. It currently defaults to 1.2.0. @@ -6498,10 +6503,6 @@ Valid options are: Reconnect to last server on startup התחבר שוב אל שרת אחרון בעת הפעלה - - Download plugin and overlay updates on startup - הורד עדכונים עבור תוספים וממשק-המשחק - Privacy @@ -6527,6 +6528,14 @@ Prevents the client from sending potentially identifying information about the o Hide public server list + + Check for plugin updates on startup + + + + Automatically download and install plugin updates + + Overlay @@ -7081,32 +7090,189 @@ To upgrade these files to their latest versions, click the button below.שם - Enabled - מאופשרת + Enable + + + + PA + + + + KeyEvents + + + + Install a plugin from a local file + + + + Install plugin... + + + + Unload the currently selected plugin. This will remove it from the plugin list for the current session. + + + + Unload + + + + The plugin was installed successfully + + + + Unable to deactivate all requested features for plugin "%1" + + + + Unable to load plugin "%1" + + + + Whether the positional audio feature of this plugin should be enabled + + + + This plugin does not provide support for positional audio + + + + Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus + + + + Whether this plugin should be enabled + - Plugins + PluginInstaller - Skipping plugin update in debug mode. - מדלג על עדכון תוספים במצב ניפוי-שגיאות. + PluginInstaller + - Downloaded new or updated plugin to %1. - מוריד או מעדכן תוסף עבור %1. - + You are about to install the plugin listed below. Do you wish to proceed? + + + + <html><head/><body><p><span style=" font-weight:600;">Name:</span></p></body></html> + + + + <html><head/><body><p><span style=" font-weight:600;">Version:</span></p></body></html> + + + + <html><head/><body><p><span style=" font-weight:600;">Author(s):</span></p></body></html> + + + + <html><head/><body><p><span style=" font-weight:600;">Description:</span></p></body></html> + + + + &No + + + + &Yes + + + + The file "%1" is not a valid plugin file! + + + + Found more than one plugin library for the current OS in "%1" ("%2" and "%3")! + - Failed to install new plugin to %1. - נכשל בהתקנת תוסף חדש ל-%1. + Unable to find a plugin for the current OS in "%1" + - %1 lost link. - %1 איבד קישור. + Unable to load plugin "%1" - check the plugin interface! + - %1 linked. - %1 קושר. + Unable to delete old plugin at "%1" + + + + Unable to copy plugin library from "%1" to "%2" + + + + Unable to move plugin library to "%1" + + + + + PluginManager + + %1 lost link + + + + %1 linked + + + + Plugin "%1" encountered a permanent error in positional data gathering + + + + Non-plugin found in plugin directory: "%1" + + + + Failed at loading manual plugin: %1 + + + + + PluginUpdater + + PluginUpdater + + + + The following plugins can be updated. + + + + Select all + + + + Plugin + + + + Download-URL + + + + Do you want to update the selected plugins? + + + + Unable to download plugin update for "%1" from "%2" (%3) + + + + Update for plugin "%1" failed due to too many redirects + + + + Unable to download plugin update for "%1" from "%2" (HTTP status code %3) + + + + Successfully updated plugin "%1" + diff --git a/src/mumble/mumble_hu.ts b/src/mumble/mumble_hu.ts index 051aa0e5100..8b596df7033 100644 --- a/src/mumble/mumble_hu.ts +++ b/src/mumble/mumble_hu.ts @@ -3826,6 +3826,10 @@ Ez a mező mutatja egy LCD eszköz méretét. A méret vagy pixelben (a grafikus User stopped listening to channel User stopped listening to channel + + Plugin message + + LogConfig @@ -6171,12 +6175,13 @@ Valid actions are: - Usage: mumble [options] [<url>] + Usage: mumble [options] [<url> | <plugin_list>] <url> specifies a URL to connect to after startup instead of showing the connection window, and has the following form: mumble://[<username>[:<password>]@]<host>[:<port>][/<channel>[/<subchannel>...]][?version=<x.y.z>] +<plugin_list> is a list of plugin files that shall be installed The version query parameter has to be set in order to invoke the correct client version. It currently defaults to 1.2.0. @@ -6495,10 +6500,6 @@ Valid options are: Reconnect to last server on startup Újracsatlakozás az utoljára használt kiszolgálóhoz indításkor - - Download plugin and overlay updates on startup - Bővítmények és képátfedés frissítése indításkor - Privacy Adatvédelem @@ -6525,6 +6526,14 @@ Ez a beállítás meggátolja, hogy a Mumble érzékeny adatokat továbbítson a Hide public server list Nyilvános kiszolgálók listájának elrejtése + + Check for plugin updates on startup + + + + Automatically download and install plugin updates + + Overlay @@ -7075,31 +7084,189 @@ To upgrade these files to their latest versions, click the button below.Név - Enabled - Engedélyezett + Enable + + + + PA + + + + KeyEvents + + + + Install a plugin from a local file + + + + Install plugin... + + + + Unload the currently selected plugin. This will remove it from the plugin list for the current session. + + + + Unload + + + + The plugin was installed successfully + + + + Unable to deactivate all requested features for plugin "%1" + + + + Unable to load plugin "%1" + + + + Whether the positional audio feature of this plugin should be enabled + + + + This plugin does not provide support for positional audio + + + + Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus + + + + Whether this plugin should be enabled + - Plugins + PluginInstaller - Skipping plugin update in debug mode. - Bővítmény frissítésének mellőzése hibakeresési módban. + PluginInstaller + - Downloaded new or updated plugin to %1. - Új plugin letöltve vagy frissítve: %1. + You are about to install the plugin listed below. Do you wish to proceed? + - Failed to install new plugin to %1. - Nem sikerült új plugin telepítése: %1. + <html><head/><body><p><span style=" font-weight:600;">Name:</span></p></body></html> + - %1 lost link. - %1 elveszett kapcsolat. + <html><head/><body><p><span style=" font-weight:600;">Version:</span></p></body></html> + + + + <html><head/><body><p><span style=" font-weight:600;">Author(s):</span></p></body></html> + + + + <html><head/><body><p><span style=" font-weight:600;">Description:</span></p></body></html> + + + + &No + + + + &Yes + - %1 linked. - %1 kapcsolva. + The file "%1" is not a valid plugin file! + + + + Found more than one plugin library for the current OS in "%1" ("%2" and "%3")! + + + + Unable to find a plugin for the current OS in "%1" + + + + Unable to load plugin "%1" - check the plugin interface! + + + + Unable to delete old plugin at "%1" + + + + Unable to copy plugin library from "%1" to "%2" + + + + Unable to move plugin library to "%1" + + + + + PluginManager + + %1 lost link + + + + %1 linked + + + + Plugin "%1" encountered a permanent error in positional data gathering + + + + Non-plugin found in plugin directory: "%1" + + + + Failed at loading manual plugin: %1 + + + + + PluginUpdater + + PluginUpdater + + + + The following plugins can be updated. + + + + Select all + + + + Plugin + + + + Download-URL + + + + Do you want to update the selected plugins? + + + + Unable to download plugin update for "%1" from "%2" (%3) + + + + Update for plugin "%1" failed due to too many redirects + + + + Unable to download plugin update for "%1" from "%2" (HTTP status code %3) + + + + Successfully updated plugin "%1" + diff --git a/src/mumble/mumble_it.ts b/src/mumble/mumble_it.ts index 6e4c0a0d713..0e2cfb27975 100644 --- a/src/mumble/mumble_it.ts +++ b/src/mumble/mumble_it.ts @@ -3838,6 +3838,10 @@ Questo campo descrive la dimensione di un dispositivo LCD. La dimensione è espr User stopped listening to channel Un utente ha smesso di ascoltare il tuo canale + + Plugin message + + LogConfig @@ -6215,12 +6219,13 @@ Azioni valide: - Usage: mumble [options] [<url>] + Usage: mumble [options] [<url> | <plugin_list>] <url> specifies a URL to connect to after startup instead of showing the connection window, and has the following form: mumble://[<username>[:<password>]@]<host>[:<port>][/<channel>[/<subchannel>...]][?version=<x.y.z>] +<plugin_list> is a list of plugin files that shall be installed The version query parameter has to be set in order to invoke the correct client version. It currently defaults to 1.2.0. @@ -6270,59 +6275,7 @@ Valid options are: Otherwise the locale will be permanently saved to Mumble's settings. - Utilizzo: mumble [opzioni] [<url>] - -<url> specifica un URL a cui connettersi dopo l'avvio invece di visualizzare la finestra di connessione e ha la seguente forma: -mumble: // [<username> [: <password>] @] <host> [: <port>] [/ <channel> [/ <subchannel> ...]] [? version = <x.y.z>] - -Il parametro di query della versione deve essere impostato per richiamare la versione client corretta. Attualmente il valore predefinito è 1.2.0. - -Le opzioni valide sono: - -h, --help Mostra questo testo di aiuto ed esce. - -m, --multiple - Consenti l'avvio di più istanze del client. - -c, --config - Specificare un file di configurazione alternativo. - Se lo usi per eseguire più istanze di Mumble contemporaneamente, - assicurati di impostare un valore "database" alternativo nel file config. - -n, --noidentity - Sopprimi il caricamento dei file di identità (ad esempio, certificati). - -jn, --jackname <arg> - Imposta il nome del client Jack personalizzato. - --licenza - Mostra la licenza di Mumble. - --autori - Mostra gli autori di Mumble. - - licenze di terze parti - Mostra le licenze per il software di terze parti utilizzato da Mumble. - --window-title-ext <arg> - Imposta un'estensione del titolo della finestra personalizzata. - --dump-input-stream - Esegui il dump dei flussi PCM in varie parti della catena di input - (utile per scopi di debug) - - ingresso microfono grezzo - - rilettura dell'altoparlante per la cancellazione dell'eco - - ingresso microfono elaborato - --print-echocancel-queue - Stampa su stdout lo stato della coda di cancellazione dell'eco - (utile per scopi di debug) - --translation-dir <dir> - Specifica una traduzione aggiuntiva fir <dir> in cui - Mumble cercherà i file di traduzione che sovrascrivono - quelli in bundle - Le directory aggiunte in questo modo hanno una priorità maggiore delle - posizioni predefinite utilizzate altrimenti - --print-translation-dirs - Stampa i percorsi in cui Mumble cercherà - file di traduzione che sovrascrivono quelli in bundle. - (Utile per i traduttori che testano le loro traduzioni) - --locale <locale> - Sovrascrivi le impostazioni locali nelle impostazioni di Mumble con un file - locale che corrisponde alla stringa di locale specificata. - Se il formato non è valido, Mumble genererà un errore. - In caso contrario, la locale verrà salvata in modo permanente nelle - impostazioni di Mumble. - + @@ -6591,10 +6544,6 @@ Le opzioni valide sono: Reconnect to last server on startup All'avvio connettiti all'ultimo server visitato - - Download plugin and overlay updates on startup - Aggiorna plugin e sovrapposizione all'avvio - Privacy Privacy @@ -6621,6 +6570,14 @@ Previene l'invio da parte del client di informazioni potenzialmente identif Hide public server list Nascondi lista server pubblici + + Check for plugin updates on startup + + + + Automatically download and install plugin updates + + Overlay @@ -7175,31 +7132,189 @@ Per aggiornare questi file all'ultima versione, premi il pulsante sottostan Nome - Enabled - Abilitato + Enable + Abilita + + + PA + + + + KeyEvents + + + + Install a plugin from a local file + + + + Install plugin... + + + + Unload the currently selected plugin. This will remove it from the plugin list for the current session. + + + + Unload + + + + The plugin was installed successfully + + + + Unable to deactivate all requested features for plugin "%1" + + + + Unable to load plugin "%1" + + + + Whether the positional audio feature of this plugin should be enabled + + + + This plugin does not provide support for positional audio + + + + Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus + + + + Whether this plugin should be enabled + - Plugins + PluginInstaller - Skipping plugin update in debug mode. - I plugin non verranno aggiornati in modalità Debug. + PluginInstaller + - Downloaded new or updated plugin to %1. - Aggiunto o aggiornato il seguente plugin: %1. + You are about to install the plugin listed below. Do you wish to proceed? + - Failed to install new plugin to %1. - Installazione del seguente plugin fallita: %1. + <html><head/><body><p><span style=" font-weight:600;">Name:</span></p></body></html> + - %1 lost link. - Collegamento perso con %1. + <html><head/><body><p><span style=" font-weight:600;">Version:</span></p></body></html> + - %1 linked. - collegato con %1. + <html><head/><body><p><span style=" font-weight:600;">Author(s):</span></p></body></html> + + + + <html><head/><body><p><span style=" font-weight:600;">Description:</span></p></body></html> + + + + &No + + + + &Yes + + + + The file "%1" is not a valid plugin file! + + + + Found more than one plugin library for the current OS in "%1" ("%2" and "%3")! + + + + Unable to find a plugin for the current OS in "%1" + + + + Unable to load plugin "%1" - check the plugin interface! + + + + Unable to delete old plugin at "%1" + + + + Unable to copy plugin library from "%1" to "%2" + + + + Unable to move plugin library to "%1" + + + + + PluginManager + + %1 lost link + + + + %1 linked + + + + Plugin "%1" encountered a permanent error in positional data gathering + + + + Non-plugin found in plugin directory: "%1" + + + + Failed at loading manual plugin: %1 + + + + + PluginUpdater + + PluginUpdater + + + + The following plugins can be updated. + + + + Select all + + + + Plugin + + + + Download-URL + + + + Do you want to update the selected plugins? + + + + Unable to download plugin update for "%1" from "%2" (%3) + + + + Update for plugin "%1" failed due to too many redirects + + + + Unable to download plugin update for "%1" from "%2" (HTTP status code %3) + + + + Successfully updated plugin "%1" + diff --git a/src/mumble/mumble_ja.ts b/src/mumble/mumble_ja.ts index c4d6cabf141..6dfd98e5fa2 100644 --- a/src/mumble/mumble_ja.ts +++ b/src/mumble/mumble_ja.ts @@ -3826,6 +3826,10 @@ This field describes the size of an LCD device. The size is given either in pixe User stopped listening to channel + + Plugin message + + LogConfig @@ -6170,12 +6174,13 @@ Valid actions are: - Usage: mumble [options] [<url>] + Usage: mumble [options] [<url> | <plugin_list>] <url> specifies a URL to connect to after startup instead of showing the connection window, and has the following form: mumble://[<username>[:<password>]@]<host>[:<port>][/<channel>[/<subchannel>...]][?version=<x.y.z>] +<plugin_list> is a list of plugin files that shall be installed The version query parameter has to be set in order to invoke the correct client version. It currently defaults to 1.2.0. @@ -6494,10 +6499,6 @@ Valid options are: Reconnect to last server on startup 起動時に最後に接続したサーバに再接続する - - Download plugin and overlay updates on startup - 開始時にプラグインとオーバレイの更新をダウンロードする - Privacy @@ -6523,6 +6524,14 @@ Prevents the client from sending potentially identifying information about the o Hide public server list + + Check for plugin updates on startup + + + + Automatically download and install plugin updates + + Overlay @@ -7077,31 +7086,189 @@ To upgrade these files to their latest versions, click the button below.名前 - Enabled - 有効化 + Enable + + + + PA + + + + KeyEvents + + + + Install a plugin from a local file + + + + Install plugin... + + + + Unload the currently selected plugin. This will remove it from the plugin list for the current session. + + + + Unload + + + + The plugin was installed successfully + + + + Unable to deactivate all requested features for plugin "%1" + + + + Unable to load plugin "%1" + + + + Whether the positional audio feature of this plugin should be enabled + + + + This plugin does not provide support for positional audio + + + + Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus + + + + Whether this plugin should be enabled + - Plugins + PluginInstaller - Skipping plugin update in debug mode. - デバッグモードではプラグインのアップデートはスキップします。 + PluginInstaller + - Downloaded new or updated plugin to %1. - 更新されたプラグインを %1 にダウンロードしました。 + You are about to install the plugin listed below. Do you wish to proceed? + - Failed to install new plugin to %1. - %1 に新しいプラグインをインストールできません。 + <html><head/><body><p><span style=" font-weight:600;">Name:</span></p></body></html> + - %1 lost link. - %1 はリンクを失いました。 + <html><head/><body><p><span style=" font-weight:600;">Version:</span></p></body></html> + + + + <html><head/><body><p><span style=" font-weight:600;">Author(s):</span></p></body></html> + + + + <html><head/><body><p><span style=" font-weight:600;">Description:</span></p></body></html> + + + + &No + + + + &Yes + - %1 linked. - %1 はリンクされました。 + The file "%1" is not a valid plugin file! + + + + Found more than one plugin library for the current OS in "%1" ("%2" and "%3")! + + + + Unable to find a plugin for the current OS in "%1" + + + + Unable to load plugin "%1" - check the plugin interface! + + + + Unable to delete old plugin at "%1" + + + + Unable to copy plugin library from "%1" to "%2" + + + + Unable to move plugin library to "%1" + + + + + PluginManager + + %1 lost link + + + + %1 linked + + + + Plugin "%1" encountered a permanent error in positional data gathering + + + + Non-plugin found in plugin directory: "%1" + + + + Failed at loading manual plugin: %1 + + + + + PluginUpdater + + PluginUpdater + + + + The following plugins can be updated. + + + + Select all + + + + Plugin + + + + Download-URL + + + + Do you want to update the selected plugins? + + + + Unable to download plugin update for "%1" from "%2" (%3) + + + + Update for plugin "%1" failed due to too many redirects + + + + Unable to download plugin update for "%1" from "%2" (HTTP status code %3) + + + + Successfully updated plugin "%1" + diff --git a/src/mumble/mumble_ko.ts b/src/mumble/mumble_ko.ts index 0f57970181e..51346d0888f 100644 --- a/src/mumble/mumble_ko.ts +++ b/src/mumble/mumble_ko.ts @@ -3809,6 +3809,10 @@ This field describes the size of an LCD device. The size is given either in pixe User stopped listening to channel + + Plugin message + + LogConfig @@ -6152,12 +6156,13 @@ Valid actions are: - Usage: mumble [options] [<url>] + Usage: mumble [options] [<url> | <plugin_list>] <url> specifies a URL to connect to after startup instead of showing the connection window, and has the following form: mumble://[<username>[:<password>]@]<host>[:<port>][/<channel>[/<subchannel>...]][?version=<x.y.z>] +<plugin_list> is a list of plugin files that shall be installed The version query parameter has to be set in order to invoke the correct client version. It currently defaults to 1.2.0. @@ -6477,10 +6482,6 @@ Valid options are: Reconnect to last server on startup 시작시 마지막으로 접속한 서버에 다시 접속한다 - - Download plugin and overlay updates on startup - 시작시 플러그 인과 오버레이의 업데이트를 다운로드한다. - Privacy @@ -6506,6 +6507,14 @@ Prevents the client from sending potentially identifying information about the o Hide public server list + + Check for plugin updates on startup + + + + Automatically download and install plugin updates + + Overlay @@ -7060,31 +7069,189 @@ To upgrade these files to their latest versions, click the button below.이름 - Enabled - 유효 + Enable + + + + PA + + + + KeyEvents + + + + Install a plugin from a local file + + + + Install plugin... + + + + Unload the currently selected plugin. This will remove it from the plugin list for the current session. + + + + Unload + + + + The plugin was installed successfully + + + + Unable to deactivate all requested features for plugin "%1" + + + + Unable to load plugin "%1" + + + + Whether the positional audio feature of this plugin should be enabled + + + + This plugin does not provide support for positional audio + + + + Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus + + + + Whether this plugin should be enabled + - Plugins + PluginInstaller - Skipping plugin update in debug mode. - 디버깅 모드에서는 플러그인의 업데이트가 스킵 됩니다. + PluginInstaller + - Downloaded new or updated plugin to %1. - 업데이트된 플러그인을 %1에 다운 받았습니다. + You are about to install the plugin listed below. Do you wish to proceed? + - Failed to install new plugin to %1. - %1에 새로운 플러그인을 설치할 수 없습니다. + <html><head/><body><p><span style=" font-weight:600;">Name:</span></p></body></html> + - %1 lost link. - %1은 링크가 소실 되었습니다. + <html><head/><body><p><span style=" font-weight:600;">Version:</span></p></body></html> + + + + <html><head/><body><p><span style=" font-weight:600;">Author(s):</span></p></body></html> + + + + <html><head/><body><p><span style=" font-weight:600;">Description:</span></p></body></html> + + + + &No + + + + &Yes + - %1 linked. - %1은 링크되었습니다. + The file "%1" is not a valid plugin file! + + + + Found more than one plugin library for the current OS in "%1" ("%2" and "%3")! + + + + Unable to find a plugin for the current OS in "%1" + + + + Unable to load plugin "%1" - check the plugin interface! + + + + Unable to delete old plugin at "%1" + + + + Unable to copy plugin library from "%1" to "%2" + + + + Unable to move plugin library to "%1" + + + + + PluginManager + + %1 lost link + + + + %1 linked + + + + Plugin "%1" encountered a permanent error in positional data gathering + + + + Non-plugin found in plugin directory: "%1" + + + + Failed at loading manual plugin: %1 + + + + + PluginUpdater + + PluginUpdater + + + + The following plugins can be updated. + + + + Select all + + + + Plugin + + + + Download-URL + + + + Do you want to update the selected plugins? + + + + Unable to download plugin update for "%1" from "%2" (%3) + + + + Update for plugin "%1" failed due to too many redirects + + + + Unable to download plugin update for "%1" from "%2" (HTTP status code %3) + + + + Successfully updated plugin "%1" + diff --git a/src/mumble/mumble_lt.ts b/src/mumble/mumble_lt.ts index 40ac3d86001..ef8e80f9bc3 100644 --- a/src/mumble/mumble_lt.ts +++ b/src/mumble/mumble_lt.ts @@ -3809,6 +3809,10 @@ This field describes the size of an LCD device. The size is given either in pixe User stopped listening to channel + + Plugin message + + LogConfig @@ -6154,12 +6158,13 @@ Valid actions are: - Usage: mumble [options] [<url>] + Usage: mumble [options] [<url> | <plugin_list>] <url> specifies a URL to connect to after startup instead of showing the connection window, and has the following form: mumble://[<username>[:<password>]@]<host>[:<port>][/<channel>[/<subchannel>...]][?version=<x.y.z>] +<plugin_list> is a list of plugin files that shall be installed The version query parameter has to be set in order to invoke the correct client version. It currently defaults to 1.2.0. @@ -6478,10 +6483,6 @@ Valid options are: Reconnect to last server on startup Paleidus programą, iš naujo prisijungti prie paskutinio serverio - - Download plugin and overlay updates on startup - - Privacy Privatumas @@ -6507,6 +6508,14 @@ Prevents the client from sending potentially identifying information about the o Hide public server list + + Check for plugin updates on startup + + + + Automatically download and install plugin updates + + Overlay @@ -7061,30 +7070,188 @@ Norėdami naujinti šiuos failus į naujausią versiją, spustelėkite mygtuką Pavadinimas - Enabled - Įjungta + Enable + Įjungti + + + PA + + + + KeyEvents + + + + Install a plugin from a local file + + + + Install plugin... + + + + Unload the currently selected plugin. This will remove it from the plugin list for the current session. + + + + Unload + + + + The plugin was installed successfully + + + + Unable to deactivate all requested features for plugin "%1" + + + + Unable to load plugin "%1" + + + + Whether the positional audio feature of this plugin should be enabled + + + + This plugin does not provide support for positional audio + + + + Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus + + + + Whether this plugin should be enabled + + + + + PluginInstaller + + PluginInstaller + + + + You are about to install the plugin listed below. Do you wish to proceed? + + + + <html><head/><body><p><span style=" font-weight:600;">Name:</span></p></body></html> + + + + <html><head/><body><p><span style=" font-weight:600;">Version:</span></p></body></html> + + + + <html><head/><body><p><span style=" font-weight:600;">Author(s):</span></p></body></html> + + + + <html><head/><body><p><span style=" font-weight:600;">Description:</span></p></body></html> + + + + &No + + + + &Yes + + + + The file "%1" is not a valid plugin file! + + + + Found more than one plugin library for the current OS in "%1" ("%2" and "%3")! + + + + Unable to find a plugin for the current OS in "%1" + + + + Unable to load plugin "%1" - check the plugin interface! + + + + Unable to delete old plugin at "%1" + + + + Unable to copy plugin library from "%1" to "%2" + + + + Unable to move plugin library to "%1" + + + + + PluginManager + + %1 lost link + + + + %1 linked + + + + Plugin "%1" encountered a permanent error in positional data gathering + + + + Non-plugin found in plugin directory: "%1" + + + + Failed at loading manual plugin: %1 + - Plugins + PluginUpdater + + PluginUpdater + + - Skipping plugin update in debug mode. - Derinimo veiksenoje, praleidžiamas įskiepio atnaujinimas. + The following plugins can be updated. + - Downloaded new or updated plugin to %1. - Atsisiųstas naujas ar atnaujintas įskiepis į %1. + Select all + - Failed to install new plugin to %1. - Nepavyko įdiegti naujo įskiepio į %1. + Plugin + + + + Download-URL + + + + Do you want to update the selected plugins? + + + + Unable to download plugin update for "%1" from "%2" (%3) + + + + Update for plugin "%1" failed due to too many redirects + - %1 lost link. + Unable to download plugin update for "%1" from "%2" (HTTP status code %3) - %1 linked. + Successfully updated plugin "%1" diff --git a/src/mumble/mumble_nl.ts b/src/mumble/mumble_nl.ts index 721f5a3152c..8dcad3453eb 100644 --- a/src/mumble/mumble_nl.ts +++ b/src/mumble/mumble_nl.ts @@ -3838,6 +3838,10 @@ Veld beschrijft LCD-apparaatgrootte aangeduid in pixels (voor Grafische LCD&apos User stopped listening to channel Gebruiker luistert niet meer naar kanaal + + Plugin message + + LogConfig @@ -6214,12 +6218,13 @@ Valide acties zijn: - Usage: mumble [options] [<url>] + Usage: mumble [options] [<url> | <plugin_list>] <url> specifies a URL to connect to after startup instead of showing the connection window, and has the following form: mumble://[<username>[:<password>]@]<host>[:<port>][/<channel>[/<subchannel>...]][?version=<x.y.z>] +<plugin_list> is a list of plugin files that shall be installed The version query parameter has to be set in order to invoke the correct client version. It currently defaults to 1.2.0. @@ -6269,60 +6274,7 @@ Valid options are: Otherwise the locale will be permanently saved to Mumble's settings. - Gebruik: mumble [opties] [<url>] - -<url> specifieert een URL om mee te verbinden bij het opstarten i.p.v. -het venster om te verbinden te tonen, en heeft het volgende formaat: -mumble://[<gebruikersnaam>[:<wachtwoord>]@]<serveradres>[:<poort>][/<kanaal>[/<subkanaal>...]][?versie=<x.y.z>] - -De versie-parameter moet ingesteld worden om de correcte versie -van Mumble te kiezen. Momenteel is dit standaard 1.2.0. - -Valide opties zijn: - -h, --help Toont deze hulptekst en sluit vervolgens af. - -m, --multiple - Laat toe dat meerdere instanties van de applicatie tegelijk draaien. - -c, --config - Geef een alternatief configuratiebestand op. - Als je dit gebruikt om meerdere instanties van Mumble tegelijk te draaien, - vergeet dan niet in dit bestand een aparte waarde voor 'database' op te geven. - -n, --noidentity - Voorkom het laden van identiteitsbestanden (bv. certificaten). - -jn, --jackname <argument> - Stel een zelfgekozen client-naam in voor Jack. - --license - Toon de licentie van Mumble. - --authors - Toon een overzicht van de auteurs van Mumble. - --third-party-licenses - Toon licenties van software van derde partijen die gebruikt wordt door Mumble. - --window-title-ext <argument> - Stelt een zelfgekozen achtervoegsel in voor de titel van het venster. - --dump-input-streams - Dump PCM streams op verschillende plaatsen gedurende de invoerverwerking - (nuttig bij het opsporen van bugs) - - Onverwerkte microfooninvoer - - Teruglezen van spraak bij echo-opheffing - - Verwerkte microfooninvoer - --print-echocancel-queue - Stuur de staat van de wachtrij die gebruikt wordt voor echo-opheffing naar stdout - (nuttig bij het opsporen van bugs) - --translation-dir <map> - Geeft een bijkomende vertalingsmap <map> op waarin Mumble moet zoeken op - vertalingsbestanden, die vervolgens voorrang krijgen op de ingebouwde. - Deze mappen krijgen een hogere prioriteit dan de standaardlocaties die anders - gebruikt worden. - --print-translation-dirs - Print de paden die Mumble doorzoekt voor vertalingsbestanden die voorrang - krijgen op de ingebouwde vertalingen af. - (Nuttig voor vertalers die hun vertalingen willen testen) - --locale <taalgebied> - Overschrijf het taalgebied uit de instellingen van Mumble met een taalgebied - dat overeenkomt met de opgegeven identificator. - Als het formaat ongeldig is, zal Mumble een fout geven. - Indien er geen fout is, zal dit taalgebied permanent opgeslagen worden in de - instellingen van Mumble. - + @@ -6591,10 +6543,6 @@ Valide opties zijn: Reconnect to last server on startup Verbind opnieuw met laatst bezochte server bij opstarten - - Download plugin and overlay updates on startup - Plug-in/nieuwe overlay-updates downloaden bij starten - Privacy Privacy @@ -6621,6 +6569,14 @@ Voorkomt dat Mumble potentieel identificerende informatie over het besturingssys Hide public server list Verberg publieke server-lijst + + Check for plugin updates on startup + + + + Automatically download and install plugin updates + + Overlay @@ -7175,31 +7131,189 @@ Klik op de onderstaande knop om deze bestanden naar de laatste versie bij te wer Naam - Enabled - Geactiveerd + Enable + Activeren + + + PA + + + + KeyEvents + + + + Install a plugin from a local file + + + + Install plugin... + + + + Unload the currently selected plugin. This will remove it from the plugin list for the current session. + + + + Unload + + + + The plugin was installed successfully + + + + Unable to deactivate all requested features for plugin "%1" + + + + Unable to load plugin "%1" + + + + Whether the positional audio feature of this plugin should be enabled + + + + This plugin does not provide support for positional audio + + + + Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus + + + + Whether this plugin should be enabled + + + + + PluginInstaller + + PluginInstaller + + + + You are about to install the plugin listed below. Do you wish to proceed? + + + + <html><head/><body><p><span style=" font-weight:600;">Name:</span></p></body></html> + + + + <html><head/><body><p><span style=" font-weight:600;">Version:</span></p></body></html> + + + + <html><head/><body><p><span style=" font-weight:600;">Author(s):</span></p></body></html> + + + + <html><head/><body><p><span style=" font-weight:600;">Description:</span></p></body></html> + + + + &No + + + + &Yes + + + + The file "%1" is not a valid plugin file! + + + + Found more than one plugin library for the current OS in "%1" ("%2" and "%3")! + + + + Unable to find a plugin for the current OS in "%1" + + + + Unable to load plugin "%1" - check the plugin interface! + + + + Unable to delete old plugin at "%1" + + + + Unable to copy plugin library from "%1" to "%2" + + + + Unable to move plugin library to "%1" + - Plugins + PluginManager - Skipping plugin update in debug mode. - Plugins bijwerken overslaan in foutopsporende-modus. + %1 lost link + - Downloaded new or updated plugin to %1. - Nieuwe of bijgewerkte plug-in naar %1 gedownload. + %1 linked + + + + Plugin "%1" encountered a permanent error in positional data gathering + - Failed to install new plugin to %1. - Nieuwe plug-in naar %1 installeren mislukt. + Non-plugin found in plugin directory: "%1" + - %1 lost link. - %1 verloor verbinding. + Failed at loading manual plugin: %1 + + + + PluginUpdater - %1 linked. - %1 verbonden. + PluginUpdater + + + + The following plugins can be updated. + + + + Select all + + + + Plugin + + + + Download-URL + + + + Do you want to update the selected plugins? + + + + Unable to download plugin update for "%1" from "%2" (%3) + + + + Update for plugin "%1" failed due to too many redirects + + + + Unable to download plugin update for "%1" from "%2" (HTTP status code %3) + + + + Successfully updated plugin "%1" + diff --git a/src/mumble/mumble_no.ts b/src/mumble/mumble_no.ts index 7c1f949c720..a7ba989abfe 100644 --- a/src/mumble/mumble_no.ts +++ b/src/mumble/mumble_no.ts @@ -3850,6 +3850,10 @@ Dette feltet beskriver størrelsen på en LCD-enhet. Enten gitt i piksler (for g User stopped listening to channel Bruker stoppet å lytte til kanalen + + Plugin message + + LogConfig @@ -6204,12 +6208,13 @@ Valid actions are: - Usage: mumble [options] [<url>] + Usage: mumble [options] [<url> | <plugin_list>] <url> specifies a URL to connect to after startup instead of showing the connection window, and has the following form: mumble://[<username>[:<password>]@]<host>[:<port>][/<channel>[/<subchannel>...]][?version=<x.y.z>] +<plugin_list> is a list of plugin files that shall be installed The version query parameter has to be set in order to invoke the correct client version. It currently defaults to 1.2.0. @@ -6528,10 +6533,6 @@ Valid options are: Reconnect to last server on startup Koble til tjeneren som sist ble brukt ved oppstart - - Download plugin and overlay updates on startup - Last ned programtillegg og forgrunnsinformasjonsoppdateringer ved oppstart - Privacy Personvern @@ -6558,6 +6559,14 @@ Forhindrer klienten fra å sende potensielt identifiserende informasjon om opera Hide public server list Skjul offentlig tjenerliste + + Check for plugin updates on startup + + + + Automatically download and install plugin updates + + Overlay @@ -7112,31 +7121,189 @@ Trykk på knappen nedefor for å oppgradere. Navn - Enabled - Påskrudd + Enable + Skru på + + + PA + + + + KeyEvents + + + + Install a plugin from a local file + + + + Install plugin... + + + + Unload the currently selected plugin. This will remove it from the plugin list for the current session. + + + + Unload + + + + The plugin was installed successfully + + + + Unable to deactivate all requested features for plugin "%1" + + + + Unable to load plugin "%1" + + + + Whether the positional audio feature of this plugin should be enabled + + + + This plugin does not provide support for positional audio + + + + Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus + + + + Whether this plugin should be enabled + + + + + PluginInstaller + + PluginInstaller + + + + You are about to install the plugin listed below. Do you wish to proceed? + + + + <html><head/><body><p><span style=" font-weight:600;">Name:</span></p></body></html> + + + + <html><head/><body><p><span style=" font-weight:600;">Version:</span></p></body></html> + + + + <html><head/><body><p><span style=" font-weight:600;">Author(s):</span></p></body></html> + + + + <html><head/><body><p><span style=" font-weight:600;">Description:</span></p></body></html> + + + + &No + + + + &Yes + + + + The file "%1" is not a valid plugin file! + + + + Found more than one plugin library for the current OS in "%1" ("%2" and "%3")! + + + + Unable to find a plugin for the current OS in "%1" + + + + Unable to load plugin "%1" - check the plugin interface! + + + + Unable to delete old plugin at "%1" + + + + Unable to copy plugin library from "%1" to "%2" + + + + Unable to move plugin library to "%1" + + + + + PluginManager + + %1 lost link + + + + %1 linked + + + + Plugin "%1" encountered a permanent error in positional data gathering + + + + Non-plugin found in plugin directory: "%1" + + + + Failed at loading manual plugin: %1 + - Plugins + PluginUpdater + + PluginUpdater + + - Skipping plugin update in debug mode. - Hopper over programoppdatering i feilrettingsmodus. + The following plugins can be updated. + - Downloaded new or updated plugin to %1. - Programtillegg lastet ned eller oppdatert til %1. + Select all + - Failed to install new plugin to %1. - Klarte ikke å installere nytt programtillegg til %1. + Plugin + - %1 lost link. - %1 mistet lenke. + Download-URL + - %1 linked. - %1 er lenket. + Do you want to update the selected plugins? + + + + Unable to download plugin update for "%1" from "%2" (%3) + + + + Update for plugin "%1" failed due to too many redirects + + + + Unable to download plugin update for "%1" from "%2" (HTTP status code %3) + + + + Successfully updated plugin "%1" + diff --git a/src/mumble/mumble_oc.ts b/src/mumble/mumble_oc.ts index a9b7118fdd9..1fe2f1861f3 100644 --- a/src/mumble/mumble_oc.ts +++ b/src/mumble/mumble_oc.ts @@ -3779,6 +3779,10 @@ This field describes the size of an LCD device. The size is given either in pixe User stopped listening to channel + + Plugin message + + LogConfig @@ -6122,12 +6126,13 @@ Valid actions are: - Usage: mumble [options] [<url>] + Usage: mumble [options] [<url> | <plugin_list>] <url> specifies a URL to connect to after startup instead of showing the connection window, and has the following form: mumble://[<username>[:<password>]@]<host>[:<port>][/<channel>[/<subchannel>...]][?version=<x.y.z>] +<plugin_list> is a list of plugin files that shall be installed The version query parameter has to be set in order to invoke the correct client version. It currently defaults to 1.2.0. @@ -6446,10 +6451,6 @@ Valid options are: Reconnect to last server on startup - - Download plugin and overlay updates on startup - - Privacy @@ -6475,6 +6476,14 @@ Prevents the client from sending potentially identifying information about the o Hide public server list + + Check for plugin updates on startup + + + + Automatically download and install plugin updates + + Overlay @@ -7025,30 +7034,188 @@ To upgrade these files to their latest versions, click the button below.Nom - Enabled + Enable + + + + PA + + + + KeyEvents + + + + Install a plugin from a local file + + + + Install plugin... + + + + Unload the currently selected plugin. This will remove it from the plugin list for the current session. + + + + Unload + + + + The plugin was installed successfully + + + + Unable to deactivate all requested features for plugin "%1" + + + + Unable to load plugin "%1" + + + + Whether the positional audio feature of this plugin should be enabled + + + + This plugin does not provide support for positional audio + + + + Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus + + + + Whether this plugin should be enabled + + + + + PluginInstaller + + PluginInstaller + + + + You are about to install the plugin listed below. Do you wish to proceed? + + + + <html><head/><body><p><span style=" font-weight:600;">Name:</span></p></body></html> + + + + <html><head/><body><p><span style=" font-weight:600;">Version:</span></p></body></html> + + + + <html><head/><body><p><span style=" font-weight:600;">Author(s):</span></p></body></html> + + + + <html><head/><body><p><span style=" font-weight:600;">Description:</span></p></body></html> + + + + &No + + + + &Yes + + + + The file "%1" is not a valid plugin file! + + + + Found more than one plugin library for the current OS in "%1" ("%2" and "%3")! + + + + Unable to find a plugin for the current OS in "%1" + + + + Unable to load plugin "%1" - check the plugin interface! + + + + Unable to delete old plugin at "%1" + + + + Unable to copy plugin library from "%1" to "%2" + + + + Unable to move plugin library to "%1" - Plugins + PluginManager + + %1 lost link + + + + %1 linked + + + + Plugin "%1" encountered a permanent error in positional data gathering + + + + Non-plugin found in plugin directory: "%1" + + + + Failed at loading manual plugin: %1 + + + + + PluginUpdater + + PluginUpdater + + + + The following plugins can be updated. + + + + Select all + + + + Plugin + + + + Download-URL + + - Skipping plugin update in debug mode. + Do you want to update the selected plugins? - Downloaded new or updated plugin to %1. + Unable to download plugin update for "%1" from "%2" (%3) - Failed to install new plugin to %1. + Update for plugin "%1" failed due to too many redirects - %1 lost link. + Unable to download plugin update for "%1" from "%2" (HTTP status code %3) - %1 linked. + Successfully updated plugin "%1" diff --git a/src/mumble/mumble_pl.ts b/src/mumble/mumble_pl.ts index b115ba05a86..3db57064583 100644 --- a/src/mumble/mumble_pl.ts +++ b/src/mumble/mumble_pl.ts @@ -3839,6 +3839,10 @@ Te pole opisuje rozmiar urządzenia LCD. Rozmiar jest podany w pikselach lub w z User stopped listening to channel Użytkownik przestał słuchać kanału + + Plugin message + + LogConfig @@ -6215,12 +6219,13 @@ toggledeaf - Usage: mumble [options] [<url>] + Usage: mumble [options] [<url> | <plugin_list>] <url> specifies a URL to connect to after startup instead of showing the connection window, and has the following form: mumble://[<username>[:<password>]@]<host>[:<port>][/<channel>[/<subchannel>...]][?version=<x.y.z>] +<plugin_list> is a list of plugin files that shall be installed The version query parameter has to be set in order to invoke the correct client version. It currently defaults to 1.2.0. @@ -6270,61 +6275,7 @@ Valid options are: Otherwise the locale will be permanently saved to Mumble's settings. - Użycie: mumble [opcje] [<url>] - -<url> określa adres URL, z którym należy się połączyć po uruchomieniu zamiast pokazywać -okno połączenia, ma następującą postać: -mumble://[<nazwa użytkownika>[:<hasło>]@]<host>[:<port>][/<kanał>[/<podkanał>...]][?wersja=<x.y.z>] - -Aby wywołać poprawną wersję klienta, należy ustawić parametr -zapytania o wersję. Obecnie domyślnie jest to 1.2.0. - -Prawidłowe opcje to: - -h, --help Pokaż ten tekst pomocy i zakończ. - -m, --multiple - Zezwalaj na uruchamianie wielu instancji klienta. - -c, --config - Określ alternatywny plik konfiguracyjny. - Jeśli używasz tego do uruchamiania wielu wystąpień Mumble jednocześnie, - upewnij się, że ustawiono alternatywną wartość „bazy danych” w konfiguracji. - -n, --noidentity - Blokuj ładowanie plików tożsamości (tj. certyfikatów). - -jn, --jackname <arg> - Ustaw niestandardową nazwę klienta Jack. - --license - Pokaż licencję Mumble. - --authors - Pokaż autorów Mumble. - --third-party-licenses - Pokaż licencje na oprogramowanie innych firm używane przez Mumble. - --window-title-ext <arg> - Ustawia niestandardowe rozszerzenie tytułu okna. - --dump-input-stream - Zrzuca strumienie PCM w różnych częściach łańcucha wejściowego - (przydatne do debugowania) - - surowe wejście mikrofonowe - - odczyt głośnika w celu usunięcia echa - - przetworzone wejście mikrofonowe - --print-echocancel-queue - Wyświetl na stdout stan kolejki anulowania echa - (przydatne do debugowania) - --translation-dir <dir> - Określa dodatkowe tłumaczenie dla <kat>, w którym - Mumble wyszuka pliki tłumaczeń, które nadpiszą - dołączone - Katalogi dodane w ten sposób mają wyższy priorytet niż - domyślne lokalizacje używane w inny sposób - --print-translation-dirs - Wyświetl ścieżki, których będzie szukał Mumble - plików tłumaczeń, które zastępują dołączone pliki. - (Przydatne dla tłumaczy testujących swoje tłumaczenia) - --locale <ust. reg,> - Nadpisz ustawienia regionalne w ustawieniach Mumble za pomocą - ustawień reg., które odpowiada podanemu łańcuchowi ustawień reg. - Jeśli format jest nieprawidłowy, Mumble wyświetli błąd. - W przeciwnym razie ustawienia regionalne zostaną trwale zapisane w - ustawieniach Mumble. - + @@ -6593,10 +6544,6 @@ Prawidłowe opcje to: Reconnect to last server on startup Przy starcie połącz ponownie do ostatniego serwera - - Download plugin and overlay updates on startup - Pobieraj aktualizacje nakładki oraz wtyczek przy starcie - Privacy Prywatność @@ -6623,6 +6570,14 @@ Uniemożliwia klientowi wysyłanie potencjalnie identyfikujących informacji o s Hide public server list Ukryj listę serwerów publicznych + + Check for plugin updates on startup + + + + Automatically download and install plugin updates + + Overlay @@ -7177,31 +7132,189 @@ Aby uaktualnić pliki do najnowszych wersji, kliknij przycisk poniżej.Nazwa - Enabled - Włączona + Enable + Włącz + + + PA + + + + KeyEvents + + + + Install a plugin from a local file + + + + Install plugin... + + + + Unload the currently selected plugin. This will remove it from the plugin list for the current session. + + + + Unload + + + + The plugin was installed successfully + + + + Unable to deactivate all requested features for plugin "%1" + + + + Unable to load plugin "%1" + + + + Whether the positional audio feature of this plugin should be enabled + + + + This plugin does not provide support for positional audio + + + + Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus + + + + Whether this plugin should be enabled + - Plugins + PluginInstaller - Skipping plugin update in debug mode. - Pomijanie aktualizacji wtyczek w trybie debugowania. + PluginInstaller + - Downloaded new or updated plugin to %1. - Pobrano nowy lub uaktualniony plugin do %1. + You are about to install the plugin listed below. Do you wish to proceed? + - Failed to install new plugin to %1. - Nie udało się zainstalować nowego pluginu do %1. + <html><head/><body><p><span style=" font-weight:600;">Name:</span></p></body></html> + - %1 lost link. - %1 utracił połączenie. + <html><head/><body><p><span style=" font-weight:600;">Version:</span></p></body></html> + - %1 linked. - %1 połączony. + <html><head/><body><p><span style=" font-weight:600;">Author(s):</span></p></body></html> + + + + <html><head/><body><p><span style=" font-weight:600;">Description:</span></p></body></html> + + + + &No + + + + &Yes + + + + The file "%1" is not a valid plugin file! + + + + Found more than one plugin library for the current OS in "%1" ("%2" and "%3")! + + + + Unable to find a plugin for the current OS in "%1" + + + + Unable to load plugin "%1" - check the plugin interface! + + + + Unable to delete old plugin at "%1" + + + + Unable to copy plugin library from "%1" to "%2" + + + + Unable to move plugin library to "%1" + + + + + PluginManager + + %1 lost link + + + + %1 linked + + + + Plugin "%1" encountered a permanent error in positional data gathering + + + + Non-plugin found in plugin directory: "%1" + + + + Failed at loading manual plugin: %1 + + + + + PluginUpdater + + PluginUpdater + + + + The following plugins can be updated. + + + + Select all + + + + Plugin + + + + Download-URL + + + + Do you want to update the selected plugins? + + + + Unable to download plugin update for "%1" from "%2" (%3) + + + + Update for plugin "%1" failed due to too many redirects + + + + Unable to download plugin update for "%1" from "%2" (HTTP status code %3) + + + + Successfully updated plugin "%1" + diff --git a/src/mumble/mumble_pt_BR.ts b/src/mumble/mumble_pt_BR.ts index 44ca47bcb99..3e227bba190 100644 --- a/src/mumble/mumble_pt_BR.ts +++ b/src/mumble/mumble_pt_BR.ts @@ -3838,6 +3838,10 @@ Este campo descreve o tamanho de um dispositivo LCD. O tamanho é dado em pixels User stopped listening to channel Usuário parou de ouvir o canal + + Plugin message + + LogConfig @@ -6192,12 +6196,13 @@ Valid actions are: - Usage: mumble [options] [<url>] + Usage: mumble [options] [<url> | <plugin_list>] <url> specifies a URL to connect to after startup instead of showing the connection window, and has the following form: mumble://[<username>[:<password>]@]<host>[:<port>][/<channel>[/<subchannel>...]][?version=<x.y.z>] +<plugin_list> is a list of plugin files that shall be installed The version query parameter has to be set in order to invoke the correct client version. It currently defaults to 1.2.0. @@ -6516,10 +6521,6 @@ Valid options are: Reconnect to last server on startup Reconectar ao último servidor ao iniciar - - Download plugin and overlay updates on startup - Baixar atualizações de complementos e sobreimpressão ao iniciar - Privacy Privacidade @@ -6546,6 +6547,14 @@ Evita que o cliente envie informações potencialmente capazes de identificaçã Hide public server list Ocultar lista de servidores públicos + + Check for plugin updates on startup + + + + Automatically download and install plugin updates + + Overlay @@ -7100,31 +7109,189 @@ Para atualizar estes arquivos para suas últimas versões, clique no botão abai Nome - Enabled - Ativo + Enable + Ativar + + + PA + + + + KeyEvents + + + + Install a plugin from a local file + + + + Install plugin... + + + + Unload the currently selected plugin. This will remove it from the plugin list for the current session. + + + + Unload + + + + The plugin was installed successfully + + + + Unable to deactivate all requested features for plugin "%1" + + + + Unable to load plugin "%1" + + + + Whether the positional audio feature of this plugin should be enabled + + + + This plugin does not provide support for positional audio + + + + Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus + + + + Whether this plugin should be enabled + - Plugins + PluginInstaller - Skipping plugin update in debug mode. - Pulando atualização de complementos no modo de depuração. + PluginInstaller + - Downloaded new or updated plugin to %1. - Baixou complemento novo ou atualizado para %1. + You are about to install the plugin listed below. Do you wish to proceed? + - Failed to install new plugin to %1. - Falha ao instalar a novo complemento para %1. + <html><head/><body><p><span style=" font-weight:600;">Name:</span></p></body></html> + - %1 lost link. - %1 perdeu vínculo. + <html><head/><body><p><span style=" font-weight:600;">Version:</span></p></body></html> + + + + <html><head/><body><p><span style=" font-weight:600;">Author(s):</span></p></body></html> + + + + <html><head/><body><p><span style=" font-weight:600;">Description:</span></p></body></html> + + + + &No + + + + &Yes + - %1 linked. - %1 vinculado. + The file "%1" is not a valid plugin file! + + + + Found more than one plugin library for the current OS in "%1" ("%2" and "%3")! + + + + Unable to find a plugin for the current OS in "%1" + + + + Unable to load plugin "%1" - check the plugin interface! + + + + Unable to delete old plugin at "%1" + + + + Unable to copy plugin library from "%1" to "%2" + + + + Unable to move plugin library to "%1" + + + + + PluginManager + + %1 lost link + + + + %1 linked + + + + Plugin "%1" encountered a permanent error in positional data gathering + + + + Non-plugin found in plugin directory: "%1" + + + + Failed at loading manual plugin: %1 + + + + + PluginUpdater + + PluginUpdater + + + + The following plugins can be updated. + + + + Select all + + + + Plugin + + + + Download-URL + + + + Do you want to update the selected plugins? + + + + Unable to download plugin update for "%1" from "%2" (%3) + + + + Update for plugin "%1" failed due to too many redirects + + + + Unable to download plugin update for "%1" from "%2" (HTTP status code %3) + + + + Successfully updated plugin "%1" + diff --git a/src/mumble/mumble_pt_PT.ts b/src/mumble/mumble_pt_PT.ts index 411da98abbf..c44e73e40fe 100644 --- a/src/mumble/mumble_pt_PT.ts +++ b/src/mumble/mumble_pt_PT.ts @@ -3824,6 +3824,10 @@ Este campo descreve o tamanho de um dispositivo LCD. O tamanho é dado em pixels User stopped listening to channel + + Plugin message + + LogConfig @@ -6175,12 +6179,13 @@ Valid actions are: - Usage: mumble [options] [<url>] + Usage: mumble [options] [<url> | <plugin_list>] <url> specifies a URL to connect to after startup instead of showing the connection window, and has the following form: mumble://[<username>[:<password>]@]<host>[:<port>][/<channel>[/<subchannel>...]][?version=<x.y.z>] +<plugin_list> is a list of plugin files that shall be installed The version query parameter has to be set in order to invoke the correct client version. It currently defaults to 1.2.0. @@ -6499,10 +6504,6 @@ Valid options are: Reconnect to last server on startup Ligar novamente ao último servidor ao iniciar - - Download plugin and overlay updates on startup - Tranferir atualizações de plugins e sobreposição ao iniciar - Privacy @@ -6528,6 +6529,14 @@ Prevents the client from sending potentially identifying information about the o Hide public server list + + Check for plugin updates on startup + + + + Automatically download and install plugin updates + + Overlay @@ -7082,31 +7091,189 @@ Para atualizar estes ficheiros para suas últimas versões, clique no botão aba Nome - Enabled - Ativo + Enable + + + + PA + + + + KeyEvents + + + + Install a plugin from a local file + + + + Install plugin... + + + + Unload the currently selected plugin. This will remove it from the plugin list for the current session. + + + + Unload + + + + The plugin was installed successfully + + + + Unable to deactivate all requested features for plugin "%1" + + + + Unable to load plugin "%1" + + + + Whether the positional audio feature of this plugin should be enabled + + + + This plugin does not provide support for positional audio + + + + Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus + + + + Whether this plugin should be enabled + + + + + PluginInstaller + + PluginInstaller + + + + You are about to install the plugin listed below. Do you wish to proceed? + + + + <html><head/><body><p><span style=" font-weight:600;">Name:</span></p></body></html> + + + + <html><head/><body><p><span style=" font-weight:600;">Version:</span></p></body></html> + + + + <html><head/><body><p><span style=" font-weight:600;">Author(s):</span></p></body></html> + + + + <html><head/><body><p><span style=" font-weight:600;">Description:</span></p></body></html> + + + + &No + + + + &Yes + + + + The file "%1" is not a valid plugin file! + + + + Found more than one plugin library for the current OS in "%1" ("%2" and "%3")! + + + + Unable to find a plugin for the current OS in "%1" + + + + Unable to load plugin "%1" - check the plugin interface! + + + + Unable to delete old plugin at "%1" + + + + Unable to copy plugin library from "%1" to "%2" + + + + Unable to move plugin library to "%1" + - Plugins + PluginManager - Skipping plugin update in debug mode. - Saltar atualização de plugin no modo de depuração. + %1 lost link + - Downloaded new or updated plugin to %1. - Transferido plugin novo ou atualizado para %1. + %1 linked + - Failed to install new plugin to %1. - Falha ao instalar a novo plugin para %1. + Plugin "%1" encountered a permanent error in positional data gathering + - %1 lost link. - %1 perdeu ligação. + Non-plugin found in plugin directory: "%1" + + + + Failed at loading manual plugin: %1 + + + + + PluginUpdater + + PluginUpdater + + + + The following plugins can be updated. + + + + Select all + + + + Plugin + - %1 linked. - %1 ligado. + Download-URL + + + + Do you want to update the selected plugins? + + + + Unable to download plugin update for "%1" from "%2" (%3) + + + + Update for plugin "%1" failed due to too many redirects + + + + Unable to download plugin update for "%1" from "%2" (HTTP status code %3) + + + + Successfully updated plugin "%1" + diff --git a/src/mumble/mumble_ro.ts b/src/mumble/mumble_ro.ts index d3e39b698b6..d47c28d0efa 100644 --- a/src/mumble/mumble_ro.ts +++ b/src/mumble/mumble_ro.ts @@ -3783,6 +3783,10 @@ This field describes the size of an LCD device. The size is given either in pixe User stopped listening to channel + + Plugin message + + LogConfig @@ -6126,12 +6130,13 @@ Valid actions are: - Usage: mumble [options] [<url>] + Usage: mumble [options] [<url> | <plugin_list>] <url> specifies a URL to connect to after startup instead of showing the connection window, and has the following form: mumble://[<username>[:<password>]@]<host>[:<port>][/<channel>[/<subchannel>...]][?version=<x.y.z>] +<plugin_list> is a list of plugin files that shall be installed The version query parameter has to be set in order to invoke the correct client version. It currently defaults to 1.2.0. @@ -6450,10 +6455,6 @@ Valid options are: Reconnect to last server on startup - - Download plugin and overlay updates on startup - - Privacy @@ -6479,6 +6480,14 @@ Prevents the client from sending potentially identifying information about the o Hide public server list + + Check for plugin updates on startup + + + + Automatically download and install plugin updates + + Overlay @@ -7029,30 +7038,188 @@ To upgrade these files to their latest versions, click the button below.Nume - Enabled + Enable + + + + PA + + + + KeyEvents + + + + Install a plugin from a local file + + + + Install plugin... + + + + Unload the currently selected plugin. This will remove it from the plugin list for the current session. + + + + Unload + + + + The plugin was installed successfully + + + + Unable to deactivate all requested features for plugin "%1" + + + + Unable to load plugin "%1" + + + + Whether the positional audio feature of this plugin should be enabled + + + + This plugin does not provide support for positional audio + + + + Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus + + + + Whether this plugin should be enabled + + + + + PluginInstaller + + PluginInstaller + + + + You are about to install the plugin listed below. Do you wish to proceed? + + + + <html><head/><body><p><span style=" font-weight:600;">Name:</span></p></body></html> + + + + <html><head/><body><p><span style=" font-weight:600;">Version:</span></p></body></html> + + + + <html><head/><body><p><span style=" font-weight:600;">Author(s):</span></p></body></html> + + + + <html><head/><body><p><span style=" font-weight:600;">Description:</span></p></body></html> + + + + &No + + + + &Yes + + + + The file "%1" is not a valid plugin file! + + + + Found more than one plugin library for the current OS in "%1" ("%2" and "%3")! + + + + Unable to find a plugin for the current OS in "%1" + + + + Unable to load plugin "%1" - check the plugin interface! + + + + Unable to delete old plugin at "%1" + + + + Unable to copy plugin library from "%1" to "%2" + + + + Unable to move plugin library to "%1" - Plugins + PluginManager + + %1 lost link + + + + %1 linked + + + + Plugin "%1" encountered a permanent error in positional data gathering + + + + Non-plugin found in plugin directory: "%1" + + + + Failed at loading manual plugin: %1 + + + + + PluginUpdater + + PluginUpdater + + + + The following plugins can be updated. + + + + Select all + + + + Plugin + + + + Download-URL + + - Skipping plugin update in debug mode. + Do you want to update the selected plugins? - Downloaded new or updated plugin to %1. + Unable to download plugin update for "%1" from "%2" (%3) - Failed to install new plugin to %1. + Update for plugin "%1" failed due to too many redirects - %1 lost link. + Unable to download plugin update for "%1" from "%2" (HTTP status code %3) - %1 linked. + Successfully updated plugin "%1" diff --git a/src/mumble/mumble_ru.ts b/src/mumble/mumble_ru.ts index fc924cfdfce..57e639ae049 100644 --- a/src/mumble/mumble_ru.ts +++ b/src/mumble/mumble_ru.ts @@ -3792,6 +3792,10 @@ This field describes the size of an LCD device. The size is given either in pixe User stopped listening to channel Пользователь перестал слушать канал + + Plugin message + + LogConfig @@ -6168,12 +6172,13 @@ Valid actions are: - Usage: mumble [options] [<url>] + Usage: mumble [options] [<url> | <plugin_list>] <url> specifies a URL to connect to after startup instead of showing the connection window, and has the following form: mumble://[<username>[:<password>]@]<host>[:<port>][/<channel>[/<subchannel>...]][?version=<x.y.z>] +<plugin_list> is a list of plugin files that shall be installed The version query parameter has to be set in order to invoke the correct client version. It currently defaults to 1.2.0. @@ -6492,10 +6497,6 @@ Valid options are: Reconnect to last server on startup Подключаться к последнему серверу при запуске - - Download plugin and overlay updates on startup - Загружать обновления плагинов и табло при запуске - Privacy Конфиденциальность @@ -6521,6 +6522,14 @@ Prevents the client from sending potentially identifying information about the o Hide public server list Скрыть список публичных серверов + + Check for plugin updates on startup + + + + Automatically download and install plugin updates + + Overlay @@ -7075,31 +7084,189 @@ To upgrade these files to their latest versions, click the button below.Имя - Enabled - Включено + Enable + Включить + + + PA + + + + KeyEvents + + + + Install a plugin from a local file + + + + Install plugin... + + + + Unload the currently selected plugin. This will remove it from the plugin list for the current session. + + + + Unload + + + + The plugin was installed successfully + + + + Unable to deactivate all requested features for plugin "%1" + + + + Unable to load plugin "%1" + + + + Whether the positional audio feature of this plugin should be enabled + + + + This plugin does not provide support for positional audio + + + + Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus + + + + Whether this plugin should be enabled + - Plugins + PluginInstaller - Skipping plugin update in debug mode. - Пропустить обновление плагина в режиме отладки. + PluginInstaller + - Downloaded new or updated plugin to %1. - Загрузка нового или обновленного плагина в %1. + You are about to install the plugin listed below. Do you wish to proceed? + - Failed to install new plugin to %1. - Ошибка при установке нового плагина в %1. + <html><head/><body><p><span style=" font-weight:600;">Name:</span></p></body></html> + - %1 lost link. - %1 потерял связь. + <html><head/><body><p><span style=" font-weight:600;">Version:</span></p></body></html> + + + + <html><head/><body><p><span style=" font-weight:600;">Author(s):</span></p></body></html> + + + + <html><head/><body><p><span style=" font-weight:600;">Description:</span></p></body></html> + + + + &No + + + + &Yes + - %1 linked. - %1 подключен. + The file "%1" is not a valid plugin file! + + + + Found more than one plugin library for the current OS in "%1" ("%2" and "%3")! + + + + Unable to find a plugin for the current OS in "%1" + + + + Unable to load plugin "%1" - check the plugin interface! + + + + Unable to delete old plugin at "%1" + + + + Unable to copy plugin library from "%1" to "%2" + + + + Unable to move plugin library to "%1" + + + + + PluginManager + + %1 lost link + + + + %1 linked + + + + Plugin "%1" encountered a permanent error in positional data gathering + + + + Non-plugin found in plugin directory: "%1" + + + + Failed at loading manual plugin: %1 + + + + + PluginUpdater + + PluginUpdater + + + + The following plugins can be updated. + + + + Select all + + + + Plugin + + + + Download-URL + + + + Do you want to update the selected plugins? + + + + Unable to download plugin update for "%1" from "%2" (%3) + + + + Update for plugin "%1" failed due to too many redirects + + + + Unable to download plugin update for "%1" from "%2" (HTTP status code %3) + + + + Successfully updated plugin "%1" + diff --git a/src/mumble/mumble_si.ts b/src/mumble/mumble_si.ts index 841e4e3a7c1..f37f6307967 100644 --- a/src/mumble/mumble_si.ts +++ b/src/mumble/mumble_si.ts @@ -3753,6 +3753,10 @@ This field describes the size of an LCD device. The size is given either in pixe %1 link + + Plugin message + + LogConfig @@ -6084,12 +6088,13 @@ Otherwise abort and check your certificate and username. - Usage: mumble [options] [<url>] + Usage: mumble [options] [<url> | <plugin_list>] <url> specifies a URL to connect to after startup instead of showing the connection window, and has the following form: mumble://[<username>[:<password>]@]<host>[:<port>][/<channel>[/<subchannel>...]][?version=<x.y.z>] +<plugin_list> is a list of plugin files that shall be installed The version query parameter has to be set in order to invoke the correct client version. It currently defaults to 1.2.0. @@ -6408,10 +6413,6 @@ Prevents the client from sending potentially identifying information about the o This will check for new releases of plugins every time you start the program, and download them automatically. - - Download plugin and overlay updates on startup - - Submit anonymous statistics to the Mumble project @@ -6436,6 +6437,14 @@ Prevents the client from sending potentially identifying information about the o Network + + Check for plugin updates on startup + + + + Automatically download and install plugin updates + + Overlay @@ -6938,10 +6947,6 @@ To upgrade these files to their latest versions, click the button below.Name - - Enabled - - Reloads all plugins @@ -6986,27 +6991,189 @@ To upgrade these files to their latest versions, click the button below.Plugin has no about function. + + Enable + + + + PA + + + + KeyEvents + + + + Install a plugin from a local file + + + + Install plugin... + + + + Unload the currently selected plugin. This will remove it from the plugin list for the current session. + + + + Unload + + + + The plugin was installed successfully + + + + Unable to deactivate all requested features for plugin "%1" + + + + Unable to load plugin "%1" + + + + Whether the positional audio feature of this plugin should be enabled + + + + This plugin does not provide support for positional audio + + + + Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus + + + + Whether this plugin should be enabled + + + + + PluginInstaller + + PluginInstaller + + + + You are about to install the plugin listed below. Do you wish to proceed? + + + + <html><head/><body><p><span style=" font-weight:600;">Name:</span></p></body></html> + + + + <html><head/><body><p><span style=" font-weight:600;">Version:</span></p></body></html> + + + + <html><head/><body><p><span style=" font-weight:600;">Author(s):</span></p></body></html> + + + + <html><head/><body><p><span style=" font-weight:600;">Description:</span></p></body></html> + + + + &No + + + + &Yes + + + + The file "%1" is not a valid plugin file! + + + + Found more than one plugin library for the current OS in "%1" ("%2" and "%3")! + + + + Unable to find a plugin for the current OS in "%1" + + + + Unable to load plugin "%1" - check the plugin interface! + + + + Unable to delete old plugin at "%1" + + + + Unable to copy plugin library from "%1" to "%2" + + + + Unable to move plugin library to "%1" + + + + + PluginManager + + %1 lost link + + + + %1 linked + + + + Plugin "%1" encountered a permanent error in positional data gathering + + + + Non-plugin found in plugin directory: "%1" + + + + Failed at loading manual plugin: %1 + + - Plugins + PluginUpdater + + PluginUpdater + + + + The following plugins can be updated. + + + + Select all + + + + Plugin + + + + Download-URL + + - %1 lost link. + Do you want to update the selected plugins? - %1 linked. + Unable to download plugin update for "%1" from "%2" (%3) - Skipping plugin update in debug mode. + Update for plugin "%1" failed due to too many redirects - Downloaded new or updated plugin to %1. + Unable to download plugin update for "%1" from "%2" (HTTP status code %3) - Failed to install new plugin to %1. + Successfully updated plugin "%1" diff --git a/src/mumble/mumble_sv.ts b/src/mumble/mumble_sv.ts index 1e01456d007..8d006605df5 100644 --- a/src/mumble/mumble_sv.ts +++ b/src/mumble/mumble_sv.ts @@ -3838,6 +3838,10 @@ Detta fält beskriver storleken av en LCD-enhet. Storleken mäts i pixlar (för User stopped listening to channel Anändare slutade lyssna på kanalen + + Plugin message + + LogConfig @@ -6214,12 +6218,13 @@ Giltiga åtgärder är: - Usage: mumble [options] [<url>] + Usage: mumble [options] [<url> | <plugin_list>] <url> specifies a URL to connect to after startup instead of showing the connection window, and has the following form: mumble://[<username>[:<password>]@]<host>[:<port>][/<channel>[/<subchannel>...]][?version=<x.y.z>] +<plugin_list> is a list of plugin files that shall be installed The version query parameter has to be set in order to invoke the correct client version. It currently defaults to 1.2.0. @@ -6269,61 +6274,7 @@ Valid options are: Otherwise the locale will be permanently saved to Mumble's settings. - Användning: mumble [options] [<url>] - -<url> anger en URL som ska anslutas till efter uppstart istället för att visa -anslutningsfönstret, och har följande form: -mumble://[<användarnamn>[:<lösenord>]@]<host>[:<port>][/<kanal>[/<underkanal>...]][?version=<x.y.z>] - -Förfrågningsparametern version måste ställas in för att kunna åberopa -rätt klientversion. För närvarande är standardvärdet 1.2.0. - -Giltiga alternativ är: - -h, --help Visar denna hjälptext. - -m, --multiple - Tillåt att flera instanser av klienten startas. - -c, --config - Ange en alternativ konfigurationsfil. - Om du använder detta för att köra flera instanser av Mumble på en gång, - se till att ställa in ett alternativt "databas" -värde i konfigurationen. - -n, --noidentity - Dämpa inläsning av identitetsfiler (dvs. certifikat.) - -jn, --jackname <arg> - Ange anpassat Jack-klientnamn. - --license - Visa Mumble-licensen. - --authors - VIsa Mumble-skapare. - --third-party-licenses - Visa licenser för tredje-partmjukvara som används av Mumble. - --window-title-ext <arg> - Ställer in ett anpassat fönstertiteltillägg. - --dump-input-streams - Dumpa PCM-strömmar vid olika delar av ingångskedjan - (användbart för felsökningsändamål) - - rå mikrofoningång - - högtalaravläsning för ekodämpning - - bearbetad mikrofoningång - --print-echocancel-queue - Skriv ut på stdout ekoställningskön - (användbart för felsökningsändamål) ---translation-dir <dir> - Anger en ytterligare översättning för <dir> där - Mumble söker efter översättningsfiler som skrivs över - de medföljande - kataloger som läggs till på detta sätt har högre prioritet än - standardplatserna som används annars ---print-translation-dirs - Skriv ut de vägar som Mumble söker efter - översättningsfiler som skriver över de medföljande filerna. - (Användbart för översättare som testar sina översättningar) - --locale <locale> - Skriver över lokaladressen i Mumbles inställningar med en - lokal som motsvarar den angivna lokalsträngen. - Om formatet är ogiltigt kommer Mumble att göra ett fel. - Annars sparas språket permanent i - Mumbles inställningar. - + @@ -6592,10 +6543,6 @@ Giltiga alternativ är: Reconnect to last server on startup Återanslut till den senaste servern vid uppstart - - Download plugin and overlay updates on startup - Ladda ner uppdateringar för insticksmoduler och överlag vid uppstart - Privacy Sekretess @@ -6622,6 +6569,14 @@ Förhindrar klienten från att skicka potentiellt identifierande information om Hide public server list Dölj offentlig serverlista + + Check for plugin updates on startup + + + + Automatically download and install plugin updates + + Overlay @@ -7176,31 +7131,189 @@ Tryck på knappen nedan för att uppgradera dessa filer till de senaste versione Namn - Enabled - Aktiverad + Enable + Aktivera + + + PA + + + + KeyEvents + + + + Install a plugin from a local file + + + + Install plugin... + + + + Unload the currently selected plugin. This will remove it from the plugin list for the current session. + + + + Unload + + + + The plugin was installed successfully + + + + Unable to deactivate all requested features for plugin "%1" + + + + Unable to load plugin "%1" + + + + Whether the positional audio feature of this plugin should be enabled + + + + This plugin does not provide support for positional audio + + + + Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus + + + + Whether this plugin should be enabled + - Plugins + PluginInstaller - Skipping plugin update in debug mode. - Hoppar över uppdatering för insticksmoduler i felsökningsläge. + PluginInstaller + - Downloaded new or updated plugin to %1. - Laddade ner ny/uppdaterad insticksmodul till %1. + You are about to install the plugin listed below. Do you wish to proceed? + - Failed to install new plugin to %1. - Kude inte installera ny insticksmodul till %1. + <html><head/><body><p><span style=" font-weight:600;">Name:</span></p></body></html> + - %1 lost link. - %1 förlorade länk. + <html><head/><body><p><span style=" font-weight:600;">Version:</span></p></body></html> + - %1 linked. - %1 länkad. + <html><head/><body><p><span style=" font-weight:600;">Author(s):</span></p></body></html> + + + + <html><head/><body><p><span style=" font-weight:600;">Description:</span></p></body></html> + + + + &No + + + + &Yes + + + + The file "%1" is not a valid plugin file! + + + + Found more than one plugin library for the current OS in "%1" ("%2" and "%3")! + + + + Unable to find a plugin for the current OS in "%1" + + + + Unable to load plugin "%1" - check the plugin interface! + + + + Unable to delete old plugin at "%1" + + + + Unable to copy plugin library from "%1" to "%2" + + + + Unable to move plugin library to "%1" + + + + + PluginManager + + %1 lost link + + + + %1 linked + + + + Plugin "%1" encountered a permanent error in positional data gathering + + + + Non-plugin found in plugin directory: "%1" + + + + Failed at loading manual plugin: %1 + + + + + PluginUpdater + + PluginUpdater + + + + The following plugins can be updated. + + + + Select all + + + + Plugin + + + + Download-URL + + + + Do you want to update the selected plugins? + + + + Unable to download plugin update for "%1" from "%2" (%3) + + + + Update for plugin "%1" failed due to too many redirects + + + + Unable to download plugin update for "%1" from "%2" (HTTP status code %3) + + + + Successfully updated plugin "%1" + diff --git a/src/mumble/mumble_te.ts b/src/mumble/mumble_te.ts index 0b7f3618dde..e4968a7ae0a 100644 --- a/src/mumble/mumble_te.ts +++ b/src/mumble/mumble_te.ts @@ -3790,6 +3790,10 @@ This field describes the size of an LCD device. The size is given either in pixe User stopped listening to channel + + Plugin message + + LogConfig @@ -6133,12 +6137,13 @@ Valid actions are: - Usage: mumble [options] [<url>] + Usage: mumble [options] [<url> | <plugin_list>] <url> specifies a URL to connect to after startup instead of showing the connection window, and has the following form: mumble://[<username>[:<password>]@]<host>[:<port>][/<channel>[/<subchannel>...]][?version=<x.y.z>] +<plugin_list> is a list of plugin files that shall be installed The version query parameter has to be set in order to invoke the correct client version. It currently defaults to 1.2.0. @@ -6463,10 +6468,6 @@ Valid options are: Reconnect to last server on startup - - Download plugin and overlay updates on startup - - Privacy @@ -6492,6 +6493,14 @@ Prevents the client from sending potentially identifying information about the o Hide public server list + + Check for plugin updates on startup + + + + Automatically download and install plugin updates + + Overlay @@ -7042,30 +7051,188 @@ To upgrade these files to their latest versions, click the button below. నామము - Enabled + Enable + + + + PA + + + + KeyEvents + + + + Install a plugin from a local file + + + + Install plugin... + + + + Unload the currently selected plugin. This will remove it from the plugin list for the current session. + + + + Unload + + + + The plugin was installed successfully + + + + Unable to deactivate all requested features for plugin "%1" + + + + Unable to load plugin "%1" + + + + Whether the positional audio feature of this plugin should be enabled + + + + This plugin does not provide support for positional audio + + + + Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus + + + + Whether this plugin should be enabled + + + + + PluginInstaller + + PluginInstaller + + + + You are about to install the plugin listed below. Do you wish to proceed? + + + + <html><head/><body><p><span style=" font-weight:600;">Name:</span></p></body></html> + + + + <html><head/><body><p><span style=" font-weight:600;">Version:</span></p></body></html> + + + + <html><head/><body><p><span style=" font-weight:600;">Author(s):</span></p></body></html> + + + + <html><head/><body><p><span style=" font-weight:600;">Description:</span></p></body></html> + + + + &No + + + + &Yes + + + + The file "%1" is not a valid plugin file! + + + + Found more than one plugin library for the current OS in "%1" ("%2" and "%3")! + + + + Unable to find a plugin for the current OS in "%1" + + + + Unable to load plugin "%1" - check the plugin interface! + + + + Unable to delete old plugin at "%1" + + + + Unable to copy plugin library from "%1" to "%2" + + + + Unable to move plugin library to "%1" - Plugins + PluginManager + + %1 lost link + + + + %1 linked + + + + Plugin "%1" encountered a permanent error in positional data gathering + + + + Non-plugin found in plugin directory: "%1" + + + + Failed at loading manual plugin: %1 + + + + + PluginUpdater + + PluginUpdater + + + + The following plugins can be updated. + + + + Select all + + + + Plugin + + + + Download-URL + + - Skipping plugin update in debug mode. + Do you want to update the selected plugins? - Downloaded new or updated plugin to %1. + Unable to download plugin update for "%1" from "%2" (%3) - Failed to install new plugin to %1. + Update for plugin "%1" failed due to too many redirects - %1 lost link. + Unable to download plugin update for "%1" from "%2" (HTTP status code %3) - %1 linked. + Successfully updated plugin "%1" diff --git a/src/mumble/mumble_th.ts b/src/mumble/mumble_th.ts index 116a138917e..2669e5ff6c5 100644 --- a/src/mumble/mumble_th.ts +++ b/src/mumble/mumble_th.ts @@ -3777,6 +3777,10 @@ This field describes the size of an LCD device. The size is given either in pixe User stopped listening to channel + + Plugin message + + LogConfig @@ -6120,12 +6124,13 @@ Valid actions are: - Usage: mumble [options] [<url>] + Usage: mumble [options] [<url> | <plugin_list>] <url> specifies a URL to connect to after startup instead of showing the connection window, and has the following form: mumble://[<username>[:<password>]@]<host>[:<port>][/<channel>[/<subchannel>...]][?version=<x.y.z>] +<plugin_list> is a list of plugin files that shall be installed The version query parameter has to be set in order to invoke the correct client version. It currently defaults to 1.2.0. @@ -6444,10 +6449,6 @@ Valid options are: Reconnect to last server on startup - - Download plugin and overlay updates on startup - - Privacy @@ -6473,6 +6474,14 @@ Prevents the client from sending potentially identifying information about the o Hide public server list + + Check for plugin updates on startup + + + + Automatically download and install plugin updates + + Overlay @@ -7023,30 +7032,188 @@ To upgrade these files to their latest versions, click the button below.ชื่อ - Enabled + Enable + + + + PA + + + + KeyEvents + + + + Install a plugin from a local file + + + + Install plugin... + + + + Unload the currently selected plugin. This will remove it from the plugin list for the current session. + + + + Unload + + + + The plugin was installed successfully + + + + Unable to deactivate all requested features for plugin "%1" + + + + Unable to load plugin "%1" + + + + Whether the positional audio feature of this plugin should be enabled + + + + This plugin does not provide support for positional audio + + + + Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus + + + + Whether this plugin should be enabled + + + + + PluginInstaller + + PluginInstaller + + + + You are about to install the plugin listed below. Do you wish to proceed? + + + + <html><head/><body><p><span style=" font-weight:600;">Name:</span></p></body></html> + + + + <html><head/><body><p><span style=" font-weight:600;">Version:</span></p></body></html> + + + + <html><head/><body><p><span style=" font-weight:600;">Author(s):</span></p></body></html> + + + + <html><head/><body><p><span style=" font-weight:600;">Description:</span></p></body></html> + + + + &No + + + + &Yes + + + + The file "%1" is not a valid plugin file! + + + + Found more than one plugin library for the current OS in "%1" ("%2" and "%3")! + + + + Unable to find a plugin for the current OS in "%1" + + + + Unable to load plugin "%1" - check the plugin interface! + + + + Unable to delete old plugin at "%1" + + + + Unable to copy plugin library from "%1" to "%2" + + + + Unable to move plugin library to "%1" - Plugins + PluginManager + + %1 lost link + + + + %1 linked + + + + Plugin "%1" encountered a permanent error in positional data gathering + + + + Non-plugin found in plugin directory: "%1" + + + + Failed at loading manual plugin: %1 + + + + + PluginUpdater + + PluginUpdater + + + + The following plugins can be updated. + + + + Select all + + + + Plugin + + + + Download-URL + + - Skipping plugin update in debug mode. + Do you want to update the selected plugins? - Downloaded new or updated plugin to %1. + Unable to download plugin update for "%1" from "%2" (%3) - Failed to install new plugin to %1. + Update for plugin "%1" failed due to too many redirects - %1 lost link. + Unable to download plugin update for "%1" from "%2" (HTTP status code %3) - %1 linked. + Successfully updated plugin "%1" diff --git a/src/mumble/mumble_tr.ts b/src/mumble/mumble_tr.ts index 933d5ed6b9c..d8b3f65349c 100644 --- a/src/mumble/mumble_tr.ts +++ b/src/mumble/mumble_tr.ts @@ -3836,6 +3836,10 @@ Bu alan LCD aygıtın boyutunu belirtir. Boyut ya piksel olarak (Grafik LCD ekra User stopped listening to channel Kullanıcı kanalı dinlemeye son verdi + + Plugin message + + LogConfig @@ -6188,12 +6192,13 @@ Valid actions are: - Usage: mumble [options] [<url>] + Usage: mumble [options] [<url> | <plugin_list>] <url> specifies a URL to connect to after startup instead of showing the connection window, and has the following form: mumble://[<username>[:<password>]@]<host>[:<port>][/<channel>[/<subchannel>...]][?version=<x.y.z>] +<plugin_list> is a list of plugin files that shall be installed The version query parameter has to be set in order to invoke the correct client version. It currently defaults to 1.2.0. @@ -6512,10 +6517,6 @@ Valid options are: Reconnect to last server on startup Başladığında son sunucuya bağlan - - Download plugin and overlay updates on startup - Başladığında eklenti ve yerpaylaşan güncellemelerini indir - Privacy Gizlilik @@ -6542,6 +6543,14 @@ Prevents the client from sending potentially identifying information about the o Hide public server list + + Check for plugin updates on startup + + + + Automatically download and install plugin updates + + Overlay @@ -7096,31 +7105,189 @@ Bu dosyaları son sürümlerine güncellemek için aşağıdaki düğmeyi tıkla İsim - Enabled - Etkin + Enable + + + + PA + + + + KeyEvents + + + + Install a plugin from a local file + + + + Install plugin... + + + + Unload the currently selected plugin. This will remove it from the plugin list for the current session. + + + + Unload + + + + The plugin was installed successfully + + + + Unable to deactivate all requested features for plugin "%1" + + + + Unable to load plugin "%1" + + + + Whether the positional audio feature of this plugin should be enabled + + + + This plugin does not provide support for positional audio + + + + Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus + + + + Whether this plugin should be enabled + - Plugins + PluginInstaller - Skipping plugin update in debug mode. - Hata ayıklama kipinde eklenti güncellemesi atlanıyor. + PluginInstaller + - Downloaded new or updated plugin to %1. - Yeni ya da güncelleştirilmiş eklenti %1 konumuna indirildi. + You are about to install the plugin listed below. Do you wish to proceed? + - Failed to install new plugin to %1. - Yeni eklenti %1 konumuna kurulamadı. + <html><head/><body><p><span style=" font-weight:600;">Name:</span></p></body></html> + - %1 lost link. - %1 bağlantı kaybetti. + <html><head/><body><p><span style=" font-weight:600;">Version:</span></p></body></html> + - %1 linked. - %1 bağlandı. + <html><head/><body><p><span style=" font-weight:600;">Author(s):</span></p></body></html> + + + + <html><head/><body><p><span style=" font-weight:600;">Description:</span></p></body></html> + + + + &No + + + + &Yes + + + + The file "%1" is not a valid plugin file! + + + + Found more than one plugin library for the current OS in "%1" ("%2" and "%3")! + + + + Unable to find a plugin for the current OS in "%1" + + + + Unable to load plugin "%1" - check the plugin interface! + + + + Unable to delete old plugin at "%1" + + + + Unable to copy plugin library from "%1" to "%2" + + + + Unable to move plugin library to "%1" + + + + + PluginManager + + %1 lost link + + + + %1 linked + + + + Plugin "%1" encountered a permanent error in positional data gathering + + + + Non-plugin found in plugin directory: "%1" + + + + Failed at loading manual plugin: %1 + + + + + PluginUpdater + + PluginUpdater + + + + The following plugins can be updated. + + + + Select all + + + + Plugin + + + + Download-URL + + + + Do you want to update the selected plugins? + + + + Unable to download plugin update for "%1" from "%2" (%3) + + + + Update for plugin "%1" failed due to too many redirects + + + + Unable to download plugin update for "%1" from "%2" (HTTP status code %3) + + + + Successfully updated plugin "%1" + diff --git a/src/mumble/mumble_uk.ts b/src/mumble/mumble_uk.ts index a0fa2584f91..344a4af9a37 100644 --- a/src/mumble/mumble_uk.ts +++ b/src/mumble/mumble_uk.ts @@ -3779,6 +3779,10 @@ This field describes the size of an LCD device. The size is given either in pixe User stopped listening to channel + + Plugin message + + LogConfig @@ -6122,12 +6126,13 @@ Valid actions are: - Usage: mumble [options] [<url>] + Usage: mumble [options] [<url> | <plugin_list>] <url> specifies a URL to connect to after startup instead of showing the connection window, and has the following form: mumble://[<username>[:<password>]@]<host>[:<port>][/<channel>[/<subchannel>...]][?version=<x.y.z>] +<plugin_list> is a list of plugin files that shall be installed The version query parameter has to be set in order to invoke the correct client version. It currently defaults to 1.2.0. @@ -6446,10 +6451,6 @@ Valid options are: Reconnect to last server on startup - - Download plugin and overlay updates on startup - - Privacy @@ -6475,6 +6476,14 @@ Prevents the client from sending potentially identifying information about the o Hide public server list + + Check for plugin updates on startup + + + + Automatically download and install plugin updates + + Overlay @@ -7025,30 +7034,188 @@ To upgrade these files to their latest versions, click the button below.Ім'я - Enabled + Enable + + + + PA + + + + KeyEvents + + + + Install a plugin from a local file + + + + Install plugin... + + + + Unload the currently selected plugin. This will remove it from the plugin list for the current session. + + + + Unload + + + + The plugin was installed successfully + + + + Unable to deactivate all requested features for plugin "%1" + + + + Unable to load plugin "%1" + + + + Whether the positional audio feature of this plugin should be enabled + + + + This plugin does not provide support for positional audio + + + + Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus + + + + Whether this plugin should be enabled + + + + + PluginInstaller + + PluginInstaller + + + + You are about to install the plugin listed below. Do you wish to proceed? + + + + <html><head/><body><p><span style=" font-weight:600;">Name:</span></p></body></html> + + + + <html><head/><body><p><span style=" font-weight:600;">Version:</span></p></body></html> + + + + <html><head/><body><p><span style=" font-weight:600;">Author(s):</span></p></body></html> + + + + <html><head/><body><p><span style=" font-weight:600;">Description:</span></p></body></html> + + + + &No + + + + &Yes + + + + The file "%1" is not a valid plugin file! + + + + Found more than one plugin library for the current OS in "%1" ("%2" and "%3")! + + + + Unable to find a plugin for the current OS in "%1" + + + + Unable to load plugin "%1" - check the plugin interface! + + + + Unable to delete old plugin at "%1" + + + + Unable to copy plugin library from "%1" to "%2" + + + + Unable to move plugin library to "%1" - Plugins + PluginManager + + %1 lost link + + + + %1 linked + + + + Plugin "%1" encountered a permanent error in positional data gathering + + + + Non-plugin found in plugin directory: "%1" + + + + Failed at loading manual plugin: %1 + + + + + PluginUpdater + + PluginUpdater + + + + The following plugins can be updated. + + + + Select all + + + + Plugin + + + + Download-URL + + - Skipping plugin update in debug mode. + Do you want to update the selected plugins? - Downloaded new or updated plugin to %1. + Unable to download plugin update for "%1" from "%2" (%3) - Failed to install new plugin to %1. + Update for plugin "%1" failed due to too many redirects - %1 lost link. + Unable to download plugin update for "%1" from "%2" (HTTP status code %3) - %1 linked. + Successfully updated plugin "%1" diff --git a/src/mumble/mumble_zh_CN.ts b/src/mumble/mumble_zh_CN.ts index d74b34e8f20..20dc2397762 100644 --- a/src/mumble/mumble_zh_CN.ts +++ b/src/mumble/mumble_zh_CN.ts @@ -3837,6 +3837,10 @@ This field describes the size of an LCD device. The size is given either in pixe User stopped listening to channel 用户停止监听频道 + + Plugin message + + LogConfig @@ -6212,12 +6216,13 @@ Valid actions are: - Usage: mumble [options] [<url>] + Usage: mumble [options] [<url> | <plugin_list>] <url> specifies a URL to connect to after startup instead of showing the connection window, and has the following form: mumble://[<username>[:<password>]@]<host>[:<port>][/<channel>[/<subchannel>...]][?version=<x.y.z>] +<plugin_list> is a list of plugin files that shall be installed The version query parameter has to be set in order to invoke the correct client version. It currently defaults to 1.2.0. @@ -6267,54 +6272,7 @@ Valid options are: Otherwise the locale will be permanently saved to Mumble's settings. - 用法:mumble [选项] [<URL>] - -<URL> 指定启动时连接的 URL,而不是显示连接窗口,URL 的格式为: -mumble://[<用户名>[:<密码>]@]<主机名>[:<端口>][/<频道名>[/<子频道名>...]][?version=<x.y.z>] - -必须设置 version 请求参数以调用正确的客户端版本,当前的默认值为 1.2.0。 - -可用的选项: - -h, --help 显示此帮助信息并退出。 - -m, --multiple - 允许启动多个客户端实例。 - -c, --config - 指定替代配置文件。 - 如果您使用此参数同时运行多个 Mumble 实例, - 请确保在配置文件内设置替代 'database' 选项。 - -n, --noidentity - 禁止加载身份认证文件(即证书)。 - -jn, --jackname <参数> - 设置自定义 Jack 客户端名称。 - --license - 显示 Mumble 许可。 - --authors - 显示 Mumble 作者。 - --third-party-licenses - 显示 Mumble 使用的第三方软件的许可。 - --window-title-ext <参数> - 设置自定义窗口标题后缀名。 - --dump-input-streams - 转储输入链上各部分的 PCM 流。 - (适用于调试目的) - - 原始麦克风输入 - - 扬声器回声消除重读取 - - 已处理麦克风输入 - --print-echocancel-queue - 向标准输出打印回声消除队列状态。 - (适用于调试目的) - --translation-dir <目录> - 指定一个额外的目录,Mumble 会在其中搜索翻译文件 - 来覆盖内置的翻译。通过此方式添加的目录比其它情况 - 下的默认位置具有更高的优先级。 - --print-translation-dirs - 输出 Mumble 会在哪些目录搜索翻译文件以覆盖内置翻译。 - (适用于译者测试自己的翻译) - --locale <区域语言代码> - 用指定字符串对应的语言覆盖 Mumble 的语言设置。 - 如果字符串格式无效,Mumble 会出错。 - 否则,指定的语言会永久保存到 Mumble 设置中。 - + @@ -6583,10 +6541,6 @@ mumble://[<用户名>[:<密码>]@]<主机名>[:<端口>] Reconnect to last server on startup 启动时自动连接上次的服务器 - - Download plugin and overlay updates on startup - 启动时下载插件和游戏内界面更新 - Privacy 隐私 @@ -6613,6 +6567,14 @@ Prevents the client from sending potentially identifying information about the o Hide public server list 隐藏公共服务器列表 + + Check for plugin updates on startup + + + + Automatically download and install plugin updates + + Overlay @@ -7167,31 +7129,189 @@ To upgrade these files to their latest versions, click the button below.名称 - Enabled - 启用 + Enable + 启用 + + + PA + + + + KeyEvents + + + + Install a plugin from a local file + + + + Install plugin... + + + + Unload the currently selected plugin. This will remove it from the plugin list for the current session. + + + + Unload + + + + The plugin was installed successfully + + + + Unable to deactivate all requested features for plugin "%1" + + + + Unable to load plugin "%1" + + + + Whether the positional audio feature of this plugin should be enabled + + + + This plugin does not provide support for positional audio + + + + Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus + + + + Whether this plugin should be enabled + - Plugins + PluginInstaller - Skipping plugin update in debug mode. - 在调试模式跳过插件更新。 + PluginInstaller + - Downloaded new or updated plugin to %1. - 下载新版或升级插件到 %1。 + You are about to install the plugin listed below. Do you wish to proceed? + - Failed to install new plugin to %1. - 安装新插件到 %1 失败。 + <html><head/><body><p><span style=" font-weight:600;">Name:</span></p></body></html> + - %1 lost link. - %1 链接丢失。 + <html><head/><body><p><span style=" font-weight:600;">Version:</span></p></body></html> + - %1 linked. - %1 已链接。 + <html><head/><body><p><span style=" font-weight:600;">Author(s):</span></p></body></html> + + + + <html><head/><body><p><span style=" font-weight:600;">Description:</span></p></body></html> + + + + &No + + + + &Yes + + + + The file "%1" is not a valid plugin file! + + + + Found more than one plugin library for the current OS in "%1" ("%2" and "%3")! + + + + Unable to find a plugin for the current OS in "%1" + + + + Unable to load plugin "%1" - check the plugin interface! + + + + Unable to delete old plugin at "%1" + + + + Unable to copy plugin library from "%1" to "%2" + + + + Unable to move plugin library to "%1" + + + + + PluginManager + + %1 lost link + + + + %1 linked + + + + Plugin "%1" encountered a permanent error in positional data gathering + + + + Non-plugin found in plugin directory: "%1" + + + + Failed at loading manual plugin: %1 + + + + + PluginUpdater + + PluginUpdater + + + + The following plugins can be updated. + + + + Select all + + + + Plugin + + + + Download-URL + + + + Do you want to update the selected plugins? + + + + Unable to download plugin update for "%1" from "%2" (%3) + + + + Update for plugin "%1" failed due to too many redirects + + + + Unable to download plugin update for "%1" from "%2" (HTTP status code %3) + + + + Successfully updated plugin "%1" + diff --git a/src/mumble/mumble_zh_HK.ts b/src/mumble/mumble_zh_HK.ts index 7f1d33665ac..1d06fc57724 100644 --- a/src/mumble/mumble_zh_HK.ts +++ b/src/mumble/mumble_zh_HK.ts @@ -3777,6 +3777,10 @@ This field describes the size of an LCD device. The size is given either in pixe User stopped listening to channel + + Plugin message + + LogConfig @@ -6125,12 +6129,13 @@ Valid actions are: - Usage: mumble [options] [<url>] + Usage: mumble [options] [<url> | <plugin_list>] <url> specifies a URL to connect to after startup instead of showing the connection window, and has the following form: mumble://[<username>[:<password>]@]<host>[:<port>][/<channel>[/<subchannel>...]][?version=<x.y.z>] +<plugin_list> is a list of plugin files that shall be installed The version query parameter has to be set in order to invoke the correct client version. It currently defaults to 1.2.0. @@ -6449,10 +6454,6 @@ Valid options are: Reconnect to last server on startup 啟動時自動連接到最後使用的伺服器 - - Download plugin and overlay updates on startup - 啟動時下載外掛與浮動視窗的更新 - Privacy @@ -6478,6 +6479,14 @@ Prevents the client from sending potentially identifying information about the o Hide public server list + + Check for plugin updates on startup + + + + Automatically download and install plugin updates + + Overlay @@ -7032,31 +7041,189 @@ To upgrade these files to their latest versions, click the button below.名稱 - Enabled - 啟用 + Enable + + + + PA + + + + KeyEvents + + + + Install a plugin from a local file + + + + Install plugin... + + + + Unload the currently selected plugin. This will remove it from the plugin list for the current session. + + + + Unload + + + + The plugin was installed successfully + + + + Unable to deactivate all requested features for plugin "%1" + + + + Unable to load plugin "%1" + + + + Whether the positional audio feature of this plugin should be enabled + + + + This plugin does not provide support for positional audio + + + + Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus + + + + Whether this plugin should be enabled + + + + + PluginInstaller + + PluginInstaller + + + + You are about to install the plugin listed below. Do you wish to proceed? + + + + <html><head/><body><p><span style=" font-weight:600;">Name:</span></p></body></html> + + + + <html><head/><body><p><span style=" font-weight:600;">Version:</span></p></body></html> + + + + <html><head/><body><p><span style=" font-weight:600;">Author(s):</span></p></body></html> + + + + <html><head/><body><p><span style=" font-weight:600;">Description:</span></p></body></html> + + + + &No + + + + &Yes + + + + The file "%1" is not a valid plugin file! + + + + Found more than one plugin library for the current OS in "%1" ("%2" and "%3")! + + + + Unable to find a plugin for the current OS in "%1" + + + + Unable to load plugin "%1" - check the plugin interface! + + + + Unable to delete old plugin at "%1" + + + + Unable to copy plugin library from "%1" to "%2" + + + + Unable to move plugin library to "%1" + - Plugins + PluginManager - Skipping plugin update in debug mode. - 在除錯模式中忽略外掛更新訊息。 + %1 lost link + - Downloaded new or updated plugin to %1. - 下載或更新 %1 外掛。 + %1 linked + - Failed to install new plugin to %1. - 安裝 %1 外掛失敗。 + Plugin "%1" encountered a permanent error in positional data gathering + - %1 lost link. - %1 失去關聯。 + Non-plugin found in plugin directory: "%1" + + + + Failed at loading manual plugin: %1 + + + + + PluginUpdater + + PluginUpdater + + + + The following plugins can be updated. + + + + Select all + + + + Plugin + - %1 linked. - %1 已關聯。 + Download-URL + + + + Do you want to update the selected plugins? + + + + Unable to download plugin update for "%1" from "%2" (%3) + + + + Update for plugin "%1" failed due to too many redirects + + + + Unable to download plugin update for "%1" from "%2" (HTTP status code %3) + + + + Successfully updated plugin "%1" + diff --git a/src/mumble/mumble_zh_TW.ts b/src/mumble/mumble_zh_TW.ts index 1a54f82176d..7c0679b105e 100644 --- a/src/mumble/mumble_zh_TW.ts +++ b/src/mumble/mumble_zh_TW.ts @@ -3805,6 +3805,10 @@ This field describes the size of an LCD device. The size is given either in pixe User stopped listening to channel + + Plugin message + + LogConfig @@ -6148,12 +6152,13 @@ Valid actions are: - Usage: mumble [options] [<url>] + Usage: mumble [options] [<url> | <plugin_list>] <url> specifies a URL to connect to after startup instead of showing the connection window, and has the following form: mumble://[<username>[:<password>]@]<host>[:<port>][/<channel>[/<subchannel>...]][?version=<x.y.z>] +<plugin_list> is a list of plugin files that shall be installed The version query parameter has to be set in order to invoke the correct client version. It currently defaults to 1.2.0. @@ -6472,10 +6477,6 @@ Valid options are: Reconnect to last server on startup 啟動時自動連接到最後使用的伺服器 - - Download plugin and overlay updates on startup - 啟動時下載外掛與浮動視窗的更新 - Privacy @@ -6501,6 +6502,14 @@ Prevents the client from sending potentially identifying information about the o Hide public server list + + Check for plugin updates on startup + + + + Automatically download and install plugin updates + + Overlay @@ -7054,31 +7063,189 @@ To upgrade these files to their latest versions, click the button below.名稱 - Enabled - 已啟用 + Enable + + + + PA + + + + KeyEvents + + + + Install a plugin from a local file + + + + Install plugin... + + + + Unload the currently selected plugin. This will remove it from the plugin list for the current session. + + + + Unload + + + + The plugin was installed successfully + + + + Unable to deactivate all requested features for plugin "%1" + + + + Unable to load plugin "%1" + + + + Whether the positional audio feature of this plugin should be enabled + + + + This plugin does not provide support for positional audio + + + + Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus + + + + Whether this plugin should be enabled + + + + + PluginInstaller + + PluginInstaller + + + + You are about to install the plugin listed below. Do you wish to proceed? + + + + <html><head/><body><p><span style=" font-weight:600;">Name:</span></p></body></html> + + + + <html><head/><body><p><span style=" font-weight:600;">Version:</span></p></body></html> + + + + <html><head/><body><p><span style=" font-weight:600;">Author(s):</span></p></body></html> + + + + <html><head/><body><p><span style=" font-weight:600;">Description:</span></p></body></html> + + + + &No + + + + &Yes + + + + The file "%1" is not a valid plugin file! + + + + Found more than one plugin library for the current OS in "%1" ("%2" and "%3")! + + + + Unable to find a plugin for the current OS in "%1" + + + + Unable to load plugin "%1" - check the plugin interface! + + + + Unable to delete old plugin at "%1" + + + + Unable to copy plugin library from "%1" to "%2" + + + + Unable to move plugin library to "%1" + - Plugins + PluginManager - Skipping plugin update in debug mode. - 在除錯模式忽略外掛更新訊息。 + %1 lost link + - Downloaded new or updated plugin to %1. - 下載或更新 %1 外掛。 + %1 linked + - Failed to install new plugin to %1. - 安裝 %1 外掛失敗。 + Plugin "%1" encountered a permanent error in positional data gathering + - %1 lost link. - %1 連接遺失。 + Non-plugin found in plugin directory: "%1" + + + + Failed at loading manual plugin: %1 + + + + + PluginUpdater + + PluginUpdater + + + + The following plugins can be updated. + + + + Select all + + + + Plugin + - %1 linked. - %1 已連接。 + Download-URL + + + + Do you want to update the selected plugins? + + + + Unable to download plugin update for "%1" from "%2" (%3) + + + + Update for plugin "%1" failed due to too many redirects + + + + Unable to download plugin update for "%1" from "%2" (HTTP status code %3) + + + + Successfully updated plugin "%1" +