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..47433afece6 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,70 @@ target_include_directories(mumble "widgets" ${SHARED_SOURCE_DIR} "${3RDPARTY_DIR}/smallft" + "${PLUGINS_DIR}" ) +# Look for the Poco libraries +if(MINGW) + message(STATUS "Looking for Poco include dir on MinGW...") + + # These hints are for the paths on our CI + 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() + + 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 PocoFoundation REQUIRED) + find_library(POCO_LIB_UTIL PocoUtil REQUIRED) + find_library(POCO_LIB_XML PocoXML REQUIRED) + find_library(POCO_LIB_ZIP PocoZip 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} + ) + + # The Poco version on MinGW (MXE) is built statically, so we have to define POCO_STATIC in order for the + # header files to export their symbols fit for static linkage + target_compile_definitions(mumble + PUBLIC + POCO_STATIC + ) +else() + find_pkg(Poco REQUIRED COMPONENTS Zip) + + target_link_libraries(mumble + PRIVATE + Poco::Zip + ) +endif() + find_pkg("SndFile;LibSndFile;sndfile" REQUIRED) # Look for various targets as they are named differently on different platforms @@ -1020,3 +1101,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..c8ce70fbf8e --- /dev/null +++ b/src/mumble/LegacyPlugin.cpp @@ -0,0 +1,255 @@ +// 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 + } +} + +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..9834e765da7 --- /dev/null +++ b/src/mumble/LegacyPlugin.h @@ -0,0 +1,80 @@ +// 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; + + // 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..5280252b006 --- /dev/null +++ b/src/mumble/PluginManager.cpp @@ -0,0 +1,923 @@ +// 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_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); + } + + 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) { + // unlock the read-lock in order to allow selectActivePositionaldataPlugin to gain a write-lock + activePluginLock.unlock(); + + selectActivePositionalDataPlugin(); + + activePluginLock.relock(); + + 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(); + } +} diff --git a/src/mumble/PluginManager.h b/src/mumble/PluginManager.h new file mode 100644 index 00000000000..f2849384484 --- /dev/null +++ b/src/mumble/PluginManager.h @@ -0,0 +1,266 @@ +// 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 +#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; + + /// 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: + static constexpr int POSITIONAL_SERVER_SYNC_INTERVAL = 500; + + /// 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(); + + 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;