Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/mumble-voip/mumble.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorRobert Adam <dev@robert-adam.de>2021-04-14 20:47:00 +0300
committerRobert Adam <dev@robert-adam.de>2021-04-16 21:15:44 +0300
commit27dbee8e620877f9a1668b1d58c7269a48c7e229 (patch)
tree0d445574449cb224a2d4cc8043520ca66c6afe11 /src
parentc10d636cef79b07784318671df97dba6c1af2b83 (diff)
FEAT(client): Plugin framework
This commit introduces a new plugin framework into the codebase of the Mumble client. Note that "plugin" here really refers to a (more or less) general purpose plugin and is therefore not to be confused with the previously available positional data plugins (only responsible for fetching positional data from a running game and passing that to Mumble). The plugin interface is written in C, removing the compiler-dependence the old "plugins" had. Instead plugins can now be written in an arbitrary language as long as that language is capable of being compiled into a shared library and also being capable of being C-compatible. As already indicated a plugin is essentially a shared library that provides certain functions that allow Mumble to interface with it. Inside Mumble the so-called PluginManager is responsible for managing the plugins and relaying events to the respective callbacks. Plugins themselves can also interact with Mumble on their own initiative by using the provided API functions. Fixes #2455 Fixes #2148 Fixes #1594 Fixes #2051 Fixes #3742 Fixes #4575 Fixes #4751
Diffstat (limited to 'src')
-rw-r--r--src/CMakeLists.txt2
-rw-r--r--src/Channel.cpp19
-rw-r--r--src/Channel.h14
-rw-r--r--src/Message.h3
-rw-r--r--src/Mumble.proto13
-rw-r--r--src/MumbleConstants.h20
-rw-r--r--src/ProcessResolver.cpp307
-rw-r--r--src/ProcessResolver.h40
-rw-r--r--src/mumble/API.h188
-rw-r--r--src/mumble/API_v_1_0_x.cpp1980
-rw-r--r--src/mumble/Audio.cpp10
-rw-r--r--src/mumble/AudioInput.cpp21
-rw-r--r--src/mumble/AudioInput.h8
-rw-r--r--src/mumble/AudioOutput.cpp145
-rw-r--r--src/mumble/AudioOutput.h19
-rw-r--r--src/mumble/CMakeLists.txt107
-rw-r--r--src/mumble/ClientUser.cpp4
-rw-r--r--src/mumble/Global.cpp2
-rw-r--r--src/mumble/Global.h5
-rw-r--r--src/mumble/LegacyPlugin.cpp267
-rw-r--r--src/mumble/LegacyPlugin.h82
-rw-r--r--src/mumble/Log.cpp8
-rw-r--r--src/mumble/Log.h10
-rw-r--r--src/mumble/MainWindow.cpp37
-rw-r--r--src/mumble/MainWindow.h8
-rw-r--r--src/mumble/ManualPlugin.cpp39
-rw-r--r--src/mumble/ManualPlugin.h23
-rw-r--r--src/mumble/Messages.cpp21
-rw-r--r--src/mumble/NetworkConfig.cpp11
-rw-r--r--src/mumble/NetworkConfig.ui13
-rw-r--r--src/mumble/Plugin.cpp694
-rw-r--r--src/mumble/Plugin.h417
-rw-r--r--src/mumble/PluginConfig.cpp247
-rw-r--r--src/mumble/PluginConfig.h66
-rw-r--r--src/mumble/PluginConfig.ui (renamed from src/mumble/Plugins.ui)41
-rw-r--r--src/mumble/PluginInstaller.cpp200
-rw-r--r--src/mumble/PluginInstaller.h84
-rw-r--r--src/mumble/PluginInstaller.ui243
-rw-r--r--src/mumble/PluginManager.cpp933
-rw-r--r--src/mumble/PluginManager.h279
-rw-r--r--src/mumble/PluginUpdater.cpp379
-rw-r--r--src/mumble/PluginUpdater.h107
-rw-r--r--src/mumble/PluginUpdater.ui224
-rw-r--r--src/mumble/Plugins.cpp792
-rw-r--r--src/mumble/Plugins.h91
-rw-r--r--src/mumble/PositionalData.cpp242
-rw-r--r--src/mumble/PositionalData.h171
-rw-r--r--src/mumble/ServerHandler.cpp18
-rw-r--r--src/mumble/ServerHandler.h10
-rw-r--r--src/mumble/Settings.cpp86
-rw-r--r--src/mumble/Settings.h16
-rw-r--r--src/mumble/UserModel.cpp11
-rw-r--r--src/mumble/UserModel.h21
-rw-r--r--src/mumble/main.cpp97
-rw-r--r--src/murmur/Messages.cpp56
-rw-r--r--src/murmur/Meta.cpp6
-rw-r--r--src/murmur/Meta.h3
-rw-r--r--src/murmur/Server.cpp11
-rw-r--r--src/murmur/Server.h3
-rw-r--r--src/murmur/ServerUser.cpp3
-rw-r--r--src/murmur/ServerUser.h1
61 files changed, 7929 insertions, 1049 deletions
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 7affde98e..747473b70 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 749fa0865..2462b2edb 100644
--- a/src/Channel.cpp
+++ b/src/Channel.cpp
@@ -11,6 +11,9 @@
#include <QtCore/QStack>
#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 578e83919..95e6bf972 100644
--- a/src/Channel.h
+++ b/src/Channel.h
@@ -99,6 +99,20 @@ public:
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 72ed4c00d..32c8d5f5f 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 3075c6928..5132e7011 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 000000000..df4fe49e5
--- /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 <https://www.mumble.info/LICENSE>.
+
+#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 000000000..39011d4ba
--- /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 <https://www.mumble.info/LICENSE>.
+
+#include "ProcessResolver.h"
+#include <cstring>
+
+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<const char*>& ProcessResolver::getProcessNames() const {
+ return m_processNames;
+}
+
+const QVector<uint64_t>& 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<const char*>& 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 <windows.h>
+ #include <tlhelp32.h>
+ #include <limits>
+
+ 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 <QtCore/QFile>
+ #include <QtCore/QDir>
+ #include <QtCore/QStringList>
+ #include <QtCore/QFileInfo>
+ #include <QtCore/QByteArray>
+ #include <QtCore/QString>
+
+
+ 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<unsigned long long int>(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 <libproc.h>
+
+ 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 <libutil.h>
+ #include <sys/types.h>
+ #include <sys/user.h>
+
+ 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 <limits.h>
+
+ #include <fcntl.h>
+ #include <kvm.h>
+ #include <paths.h>
+ #include <sys/sysctl.h>
+ #include <sys/user.h>
+
+ 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 000000000..59ada3a1c
--- /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 <https://www.mumble.info/LICENSE>.
+
+#ifndef MUMBLE_PROCESS_RESOLVER_H_
+#define MUMBLE_PROCESS_RESOLVER_H_
+
+#include <stdint.h>
+#include <QtCore/QVector>
+
+/// 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<const char*> m_processNames;
+ /// The vector for the process PIDs
+ QVector<uint64_t> 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<const char*>& getProcessNames() const;
+ /// Gets a reference to the stored process PIDs (corresponding to the names returned by ProcessResolver::getProcessNames())
+ const QVector<uint64_t>& 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 000000000..74d8196ae
--- /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 <https://www.mumble.info/LICENSE>.
+
+#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 <atomic>
+#include <functional>
+#include <future>
+#include <unordered_map>
+
+#include <QObject>
+
+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 000000000..ac8adf6f1
--- /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 <https://www.mumble.info/LICENSE>.
+
+#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 <QVariant>
+#include <QtCore/QHash>
+#include <QtCore/QMutex>
+#include <QtCore/QMutexLocker>
+#include <QtCore/QReadLocker>
+#include <QtCore/QString>
+#include <QtCore/QStringList>
+
+#include <cstring>
+#include <string>
+
+#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 &current : 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("<b>%1:</b> %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 9d383ffe7..b2d61025d 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 <QtCore/QObject>
@@ -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 d898ca0cc..cf1ca3820 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 b9dbb0c4e..0410db421 100644
--- a/src/mumble/AudioInput.h
+++ b/src/mumble/AudioInput.h
@@ -254,6 +254,14 @@ protected:
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 15a01600e..784a29245 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<float *>(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 299736b9d..3d5c5b6da 100644
--- a/src/mumble/AudioOutput.h
+++ b/src/mumble/AudioOutput.h
@@ -127,6 +127,25 @@ public:
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 d904be2b1..cf28c1dd5 100644
--- a/src/mumble/CMakeLists.txt
+++ b/src/mumble/CMakeLists.txt
@@ -35,6 +35,9 @@ option(qtspeech "Use Qt's text-to-speech system (part of the Qt Speech module) i
option(jackaudio "Build support for JackAudio." ON)
option(portaudio "Build support for PortAudio" ON)
+option(plugin-debug "Build Mumble with debug output for plugin developers." OFF)
+option(plugin-callback-debug "Build Mumble with debug output for plugin callbacks inside of Mumble." OFF)
+
if(WIN32)
option(asio "Build support for ASIO audio input." OFF)
option(wasapi "Build support for WASAPI." ON)
@@ -81,6 +84,8 @@ set(MUMBLE_SOURCES
"ACLEditor.cpp"
"ACLEditor.h"
"ACLEditor.ui"
+ "API_v_1_0_x.cpp"
+ "API.h"
"ApplicationPalette.h"
"AudioConfigDialog.cpp"
"AudioConfigDialog.h"
@@ -142,6 +147,8 @@ set(MUMBLE_SOURCES
"LCD.cpp"
"LCD.h"
"LCD.ui"
+ "LegacyPlugin.cpp"
+ "LegacyPlugin.h"
"ListenerLocalVolumeDialog.cpp"
"Log.cpp"
"Log.h"
@@ -162,9 +169,21 @@ set(MUMBLE_SOURCES
"NetworkConfig.ui"
"OpusCodec.cpp"
"OpusCodec.h"
- "Plugins.cpp"
- "Plugins.h"
- "Plugins.ui"
+ "PluginConfig.cpp"
+ "PluginConfig.h"
+ "PluginConfig.ui"
+ "Plugin.cpp"
+ "Plugin.h"
+ "PluginInstaller.cpp"
+ "PluginInstaller.h"
+ "PluginInstaller.ui"
+ "PluginManager.cpp"
+ "PluginManager.h"
+ "PluginUpdater.cpp"
+ "PluginUpdater.h"
+ "PluginUpdater.ui"
+ "PositionalData.cpp"
+ "PositionalData.h"
"PTTButtonWidget.cpp"
"PTTButtonWidget.h"
"PTTButtonWidget.ui"
@@ -347,8 +366,72 @@ target_include_directories(mumble
"widgets"
${SHARED_SOURCE_DIR}
"${3RDPARTY_DIR}/smallft"
+ "${PLUGINS_DIR}"
)
+find_pkg(Poco COMPONENTS Zip)
+
+if(TARGET Poco::Zip)
+ target_link_libraries(mumble
+ PRIVATE
+ Poco::Zip
+ )
+else()
+ message(STATUS "Regular Poco search failed - looking for Poco include dir manually...")
+
+ if(MINGW)
+ # These are the paths for our MXE environment
+ if(32_BIT)
+ set(POCO_INCLUDE_DIR_HINT "/usr/lib/mxe/usr/i686-w64-mingw32.static/include/")
+ else()
+ set(POCO_INCLUDE_DIR_HINT "/usr/lib/mxe/usr/x86_64-w64-mingw32.static/include/")
+ endif()
+ else()
+ set(POCO_INCLUDE_DIR_HINT "/usr/include")
+ endif()
+
+ find_path(POCO_INCLUDE_DIR "Poco/Poco.h" HINTS ${POCO_INCLUDE_DIR_HINT})
+
+ if(POCO_INCLUDE_DIR)
+ message(STATUS "Found Poco include dir at \"${POCO_INCLUDE_DIR}\"")
+ else()
+ message(FATAL_ERROR "Unable to locate Poco include directory")
+ endif()
+
+ find_library(POCO_LIB_FOUNDATION NAMES PocoFoundation PocoFoundationmd REQUIRED)
+ find_library(POCO_LIB_UTIL NAMES PocoUtil PocoUtilmd REQUIRED)
+ find_library(POCO_LIB_XML NAMES PocoXML PocoXMLmd REQUIRED)
+ find_library(POCO_LIB_ZIP NAMES PocoZip PocoZipmd REQUIRED)
+
+ if(POCO_LIB_ZIP)
+ message(STATUS "Found Poco Zip library at \"${POCO_LIB_ZIP}\"")
+ else()
+ message(FATAL_ERROR "Unable to find Poco Zip library")
+ endif()
+
+
+ # Now use the found include dir and libraries by linking it to the target
+ target_include_directories(mumble
+ PRIVATE
+ ${POCO_INCLUDE_DIR}
+ )
+
+ target_link_libraries(mumble
+ PRIVATE
+ ${POCO_LIB_ZIP}
+ ${POCO_LIB_XML}
+ ${POCO_LIB_UTIL}
+ ${POCO_LIB_FOUNDATION}
+ )
+
+ if(static)
+ target_compile_definitions(mumble
+ PUBLIC
+ POCO_STATIC
+ )
+ endif()
+endif()
+
find_pkg("SndFile;LibSndFile;sndfile" REQUIRED)
# Look for various targets as they are named differently on different platforms
@@ -1020,3 +1103,21 @@ if(${CMAKE_VERSION} VERSION_GREATER_EQUAL "3.16.0")
)
endif()
endif()
+
+if(plugin-debug)
+ target_compile_definitions(mumble PRIVATE "MUMBLE_PLUGIN_DEBUG")
+endif()
+
+if(plugin-callback-debug)
+ target_compile_definitions(mumble PRIVATE "MUMBLE_PLUGIN_CALLBACK_DEBUG")
+endif()
+
+if(UNIX)
+ if(${CMAKE_SYSTEM_NAME} STREQUAL "FreeBSD")
+ # On FreeBSD we need the util library for src/ProcessResolver.cpp to work
+ target_link_libraries(mumble PRIVATE util)
+ elseif(${CMAKE_SYSTEM_NAME} MATCHES ".*BSD")
+ # On any other BSD we need the kvm library for src/ProcessResolver.cpp to work
+ target_link_libraries(mumble PRIVATE kvm)
+ endif()
+endif()
diff --git a/src/mumble/ClientUser.cpp b/src/mumble/ClientUser.cpp
index 3e7c7152c..8a66cf496 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 acbbec7e1..b8c001ad1 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 5aee1abf5..d8748bb51 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 @@ public:
*/
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 000000000..420f3cf73
--- /dev/null
+++ b/src/mumble/LegacyPlugin.cpp
@@ -0,0 +1,267 @@
+// Copyright 2021 The Mumble Developers. All rights reserved.
+// Use of this source code is governed by a BSD-style license
+// that can be found in the LICENSE file at the root of the
+// Mumble source tree or at <https://www.mumble.info/LICENSE>.
+
+#include "LegacyPlugin.h"
+#include "MumblePlugin_v_1_0_x.h"
+
+#include <cstdlib>
+#include <wchar.h>
+#include <map>
+#include <string.h>
+#include <codecvt>
+#include <locale>
+
+#include <QRegularExpression>
+
+
+/// 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<mumblePluginFunc>(m_lib.resolve("getMumblePlugin"));
+ mumblePlugin2Func plugin2Func = reinterpret_cast<mumblePlugin2Func>(m_lib.resolve("getMumblePlugin2"));
+ mumblePluginQtFunc pluginQtFunc = reinterpret_cast<mumblePluginQtFunc>(m_lib.resolve("getMumblePluginQt"));
+
+ if (pluginFunc) {
+ m_mumPlug = pluginFunc();
+ }
+ if (plugin2Func) {
+ m_mumPlug2 = plugin2Func();
+ }
+ if (pluginQtFunc) {
+ m_mumPlugQt = pluginQtFunc();
+ }
+
+ // A legacy plugin is valid as long as there is a function to get the MumblePlugin struct from it
+ // and the plugin has been compiled by the same compiler as this client (determined by the plugin's
+ // "magic") and it isn't retracted
+ bool suitableMagic = m_mumPlug && m_mumPlug->magic == MUMBLE_PLUGIN_MAGIC;
+ bool retracted = m_mumPlug && m_mumPlug->shortname == L"Retracted";
+ m_pluginIsValid = pluginFunc && suitableMagic && !retracted;
+
+#ifdef MUMBLE_PLUGIN_DEBUG
+ if (!m_pluginIsValid) {
+ if (!pluginFunc) {
+ qDebug("Plugin \"%s\" is missing the getMumblePlugin() function", qPrintable(m_pluginPath));
+ } else if (!suitableMagic) {
+ qDebug("Plugin \"%s\" was compiled with a different compiler (magic differs)", qPrintable(m_pluginPath));
+ } else {
+ qDebug("Plugin \"%s\" is retracted", qPrintable(m_pluginPath));
+ }
+ }
+#endif
+ }
+}
+
+mumble_error_t LegacyPlugin::init() {
+ {
+ QWriteLocker lock(&m_pluginLock);
+
+ m_pluginIsLoaded = true;
+ }
+
+ // No-op as legacy plugins never have anything to initialize
+ // The only init function they care about is the one that inits positional audio
+ return STATUS_OK;
+}
+
+QString LegacyPlugin::getName() const {
+ PluginReadLocker lock(&m_pluginLock);
+
+ if (!m_name.isEmpty()) {
+ return m_name;
+ } else {
+ return QString::fromLatin1("<Unknown Legacy Plugin>");
+ }
+}
+
+QString LegacyPlugin::getDescription() const {
+ PluginReadLocker lock(&m_pluginLock);
+
+ if (!m_description.isEmpty()) {
+ return m_description;
+ } else {
+ return QString::fromLatin1("<No description provided by the legacy plugin>");
+ }
+}
+
+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<std::wstring, unsigned long long int> pidMap;
+
+ for (size_t i=0; i<programCount; i++) {
+ std::string currentName = programNames[i];
+ std::wstring currentNameWstr = std::wstring_convert<std::codecvt_utf8<wchar_t>>().from_bytes(currentName);
+
+ pidMap.insert(std::pair<std::wstring, unsigned long long int>(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<float*>(avatarPos), static_cast<float*>(avatarDir), static_cast<float*>(avatarAxis),
+ static_cast<float*>(cameraPos), static_cast<float*>(cameraDir), static_cast<float*>(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 000000000..250b624a0
--- /dev/null
+++ b/src/mumble/LegacyPlugin.h
@@ -0,0 +1,82 @@
+// Copyright 2021 The Mumble Developers. All rights reserved.
+// Use of this source code is governed by a BSD-style license
+// that can be found in the LICENSE file at the root of the
+// Mumble source tree or at <https://www.mumble.info/LICENSE>.
+
+#ifndef MUMBLE_MUMBLE_LEGACY_PLUGIN_H_
+#define MUMBLE_MUMBLE_LEGACY_PLUGIN_H_
+
+#include "Plugin.h"
+
+#include <QtCore/QString>
+
+#include <string>
+#include <memory>
+
+#define MUMBLE_ALLOW_DEPRECATED_LEGACY_PLUGIN_API
+#include "mumble_legacy_plugin.h"
+
+class LegacyPlugin;
+
+/// Typedef for a LegacyPlugin pointer
+typedef std::shared_ptr<LegacyPlugin> legacy_plugin_ptr_t;
+/// Typedef for a const LegacyPlugin pointer
+typedef std::shared_ptr<const LegacyPlugin> const_legacy_plugin_ptr_t;
+
+
+/// This class is meant for compatibility for old Mumble "plugins" that stem from before the plugin framework has been
+/// introduced. Thus the "plugins" represented by this class are for positional data gathering only.
+class LegacyPlugin : public Plugin {
+ friend class Plugin; // needed in order for Plugin::createNew to access LegacyPlugin::doInitialize()
+ private:
+ Q_OBJECT
+ Q_DISABLE_COPY(LegacyPlugin)
+
+ protected:
+ /// The name of the "plugin"
+ QString m_name;
+ /// The description of the "plugin"
+ QString m_description;
+ /// The Version of the "plugin"
+ mumble_version_t m_version;
+ /// A pointer to the PluginStruct in its initial version. After initialization this
+ /// field is effectively const and therefore it is not needed to protect read-access by a lock.
+ MumblePlugin *m_mumPlug;
+ /// A pointer to the PluginStruct in its second, enhanced version. After initialization this
+ /// field is effectively const and therefore it is not needed to protect read-access by a lock.
+ MumblePlugin2 *m_mumPlug2;
+ /// A pointer to the PluginStruct that encorporates Qt functionality. After initialization this
+ /// field is effectively const and therefore it is not needed to protect read-access by a lock.
+ MumblePluginQt *m_mumPlugQt;
+
+ virtual void resolveFunctionPointers() override;
+ virtual bool doInitialize() override;
+
+ LegacyPlugin(QString path, bool isBuiltIn = false, QObject *p = 0);
+
+ virtual bool showAboutDialog(QWidget *parent) const override;
+ virtual bool showConfigDialog(QWidget *parent) const override;
+ virtual uint8_t initPositionalData(const char *const*programNames, const uint64_t *programPIDs, size_t programCount) override;
+ virtual bool fetchPositionalData(Position3D& avatarPos, Vector3D& avatarDir, Vector3D& avatarAxis, Position3D& cameraPos, Vector3D& cameraDir,
+ Vector3D& cameraAxis, QString& context, QString& identity) const override;
+ virtual void shutdownPositionalData() override;
+ public:
+ virtual ~LegacyPlugin() override;
+
+ virtual mumble_error_t init() override;
+
+ // functions for direct plugin-interaction
+ virtual QString getName() const override;
+
+ virtual QString getDescription() const override;
+ virtual uint32_t getFeatures() const override;
+ virtual mumble_version_t getAPIVersion() const override;
+
+ virtual mumble_version_t getVersion() const override;
+
+ // functions for checking which underlying plugin functions are implemented
+ virtual bool providesAboutDialog() const override;
+ virtual bool providesConfigDialog() const override;
+};
+
+#endif
diff --git a/src/mumble/Log.cpp b/src/mumble/Log.cpp
index d4c011197..5a2b14edc 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<Log::MsgType>();
+
#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 6958f2be9..d584f2a49 100644
--- a/src/mumble/Log.h
+++ b/src/mumble/Log.h
@@ -90,8 +90,10 @@ public:
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 @@ public:
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 @@ public:
LogDocumentResourceAddedEvent();
};
+Q_DECLARE_METATYPE(Log::MsgType);
+
#endif
diff --git a/src/mumble/MainWindow.cpp b/src/mumble/MainWindow.cpp
index e7ae820d1..f2718f0a3 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 f600ede14..20c8a3000 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 981273c5f..e4ea84336 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 <QPointer>
#include <float.h>
#include <cmath>
-#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<float>(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<float>(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 fa76efbc8..dbeee0d31 100644
--- a/src/mumble/ManualPlugin.h
+++ b/src/mumble/ManualPlugin.h
@@ -12,8 +12,7 @@
#include <QtWidgets/QGraphicsScene>
#include "ui_ManualPlugin.h"
-
-#include "../../plugins/mumble_plugin.h"
+#include "LegacyPlugin.h"
#include <atomic>
#include <chrono>
@@ -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 @@ protected:
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 62887c34f..729085f5e 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 <QTextDocumentFragment>
@@ -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 82cd5c9f4..6b75bd644 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 f9c291288..ef783a987 100644
--- a/src/mumble/NetworkConfig.ui
+++ b/src/mumble/NetworkConfig.ui
@@ -7,7 +7,7 @@
<x>0</x>
<y>0</y>
<width>576</width>
- <height>572</height>
+ <height>584</height>
</rect>
</property>
<property name="windowTitle">
@@ -315,7 +315,7 @@ Prevents the client from sending potentially identifying information about the o
</widget>
</item>
<item>
- <widget class="QCheckBox" name="qcbPluginUpdate">
+ <widget class="QCheckBox" name="qcbPluginUpdateCheck">
<property name="toolTip">
<string>Check for new releases of plugins automatically.</string>
</property>
@@ -323,7 +323,14 @@ Prevents the client from sending potentially identifying information about the o
<string>This will check for new releases of plugins every time you start the program, and download them automatically.</string>
</property>
<property name="text">
- <string>Download plugin and overlay updates on startup</string>
+ <string>Check for plugin updates on startup</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="qcbPluginAutoUpdate">
+ <property name="text">
+ <string>Automatically download and install plugin updates</string>
</property>
</widget>
</item>
diff --git a/src/mumble/Plugin.cpp b/src/mumble/Plugin.cpp
new file mode 100644
index 000000000..8317c584f
--- /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 <https://www.mumble.info/LICENSE>.
+
+#include "Plugin.h"
+#include "Version.h"
+#include "API.h"
+
+#include <QWriteLocker>
+#include <QMutexLocker>
+
+#include <cstring>
+
+
+// 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<const void *>(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<decltype(MumblePluginFunctions::init)>(m_lib.resolve("mumble_init"));
+ m_pluginFnc.shutdown = reinterpret_cast<decltype(MumblePluginFunctions::shutdown)>(m_lib.resolve("mumble_shutdown"));
+ m_pluginFnc.getName = reinterpret_cast<decltype(MumblePluginFunctions::getName)>(m_lib.resolve("mumble_getName"));
+ m_pluginFnc.getAPIVersion = reinterpret_cast<decltype(MumblePluginFunctions::getAPIVersion)>(m_lib.resolve("mumble_getAPIVersion"));
+ m_pluginFnc.registerAPIFunctions = reinterpret_cast<decltype(MumblePluginFunctions::registerAPIFunctions)>(m_lib.resolve("mumble_registerAPIFunctions"));
+ m_pluginFnc.releaseResource = reinterpret_cast<decltype(MumblePluginFunctions::releaseResource)>(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<decltype(MumblePluginFunctions::setMumbleInfo)>(m_lib.resolve("mumble_setMumbleInfo"));
+ m_pluginFnc.getVersion = reinterpret_cast<decltype(MumblePluginFunctions::getVersion)>(m_lib.resolve("mumble_getVersion"));
+ m_pluginFnc.getAuthor = reinterpret_cast<decltype(MumblePluginFunctions::getAuthor)>(m_lib.resolve("mumble_getAuthor"));
+ m_pluginFnc.getDescription = reinterpret_cast<decltype(MumblePluginFunctions::getDescription)>(m_lib.resolve("mumble_getDescription"));
+ m_pluginFnc.getFeatures = reinterpret_cast<decltype(MumblePluginFunctions::getFeatures)>(m_lib.resolve("mumble_getFeatures"));
+ m_pluginFnc.deactivateFeatures = reinterpret_cast<decltype(MumblePluginFunctions::deactivateFeatures)>(m_lib.resolve("mumble_deactivateFeatures"));
+ m_pluginFnc.initPositionalData = reinterpret_cast<decltype(MumblePluginFunctions::initPositionalData)>(m_lib.resolve("mumble_initPositionalData"));
+ m_pluginFnc.fetchPositionalData = reinterpret_cast<decltype(MumblePluginFunctions::fetchPositionalData)>(m_lib.resolve("mumble_fetchPositionalData"));
+ m_pluginFnc.shutdownPositionalData = reinterpret_cast<decltype(MumblePluginFunctions::shutdownPositionalData)>(m_lib.resolve("mumble_shutdownPositionalData"));
+ m_pluginFnc.onServerConnected = reinterpret_cast<decltype(MumblePluginFunctions::onServerConnected)>(m_lib.resolve("mumble_onServerConnected"));
+ m_pluginFnc.onServerDisconnected = reinterpret_cast<decltype(MumblePluginFunctions::onServerDisconnected)>(m_lib.resolve("mumble_onServerDisconnected"));
+ m_pluginFnc.onChannelEntered = reinterpret_cast<decltype(MumblePluginFunctions::onChannelEntered)>(m_lib.resolve("mumble_onChannelEntered"));
+ m_pluginFnc.onChannelExited = reinterpret_cast<decltype(MumblePluginFunctions::onChannelExited)>(m_lib.resolve("mumble_onChannelExited"));
+ m_pluginFnc.onUserTalkingStateChanged = reinterpret_cast<decltype(MumblePluginFunctions::onUserTalkingStateChanged)>(m_lib.resolve("mumble_onUserTalkingStateChanged"));
+ m_pluginFnc.onReceiveData = reinterpret_cast<decltype(MumblePluginFunctions::onReceiveData)>(m_lib.resolve("mumble_onReceiveData"));
+ m_pluginFnc.onAudioInput = reinterpret_cast<decltype(MumblePluginFunctions::onAudioInput)>(m_lib.resolve("mumble_onAudioInput"));
+ m_pluginFnc.onAudioSourceFetched = reinterpret_cast<decltype(MumblePluginFunctions::onAudioSourceFetched)>(m_lib.resolve("mumble_onAudioSourceFetched"));
+ m_pluginFnc.onAudioOutputAboutToPlay = reinterpret_cast<decltype(MumblePluginFunctions::onAudioOutputAboutToPlay)>(m_lib.resolve("mumble_onAudioOutputAboutToPlay"));
+ m_pluginFnc.onServerSynchronized = reinterpret_cast<decltype(MumblePluginFunctions::onServerSynchronized)>(m_lib.resolve("mumble_onServerSynchronized"));
+ m_pluginFnc.onUserAdded = reinterpret_cast<decltype(MumblePluginFunctions::onUserAdded)>(m_lib.resolve("mumble_onUserAdded"));
+ m_pluginFnc.onUserRemoved = reinterpret_cast<decltype(MumblePluginFunctions::onUserRemoved)>(m_lib.resolve("mumble_onUserRemoved"));
+ m_pluginFnc.onChannelAdded = reinterpret_cast<decltype(MumblePluginFunctions::onChannelAdded)>(m_lib.resolve("mumble_onChannelAdded"));
+ m_pluginFnc.onChannelRemoved = reinterpret_cast<decltype(MumblePluginFunctions::onChannelRemoved)>(m_lib.resolve("mumble_onChannelRemoved"));
+ m_pluginFnc.onChannelRenamed = reinterpret_cast<decltype(MumblePluginFunctions::onChannelRenamed)>(m_lib.resolve("mumble_onChannelRenamed"));
+ m_pluginFnc.onKeyEvent = reinterpret_cast<decltype(MumblePluginFunctions::onKeyEvent)>(m_lib.resolve("mumble_onKeyEvent"));
+ m_pluginFnc.hasUpdate = reinterpret_cast<decltype(MumblePluginFunctions::hasUpdate)>(m_lib.resolve("mumble_hasUpdate"));
+ m_pluginFnc.getUpdateDownloadURL = reinterpret_cast<decltype(MumblePluginFunctions::getUpdateDownloadURL)>(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<float*>(avatarPos), static_cast<float*>(avatarDir),
+ static_cast<float*>(avatarAxis), static_cast<float*>(cameraPos), static_cast<float*>(cameraDir), static_cast<float*>(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 000000000..049e622f2
--- /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 <https://www.mumble.info/LICENSE>.
+
+#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 <QObject>
+#include <QReadWriteLock>
+#include <QString>
+#include <QLibrary>
+#include <QMutex>
+#include <QUrl>
+
+#include <stdexcept>
+#include <memory>
+
+/// 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> plugin_ptr_t;
+/// Typedef for a const plugin pointer
+typedef std::shared_ptr<const Plugin> 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<typename T, typename ... Ts>
+ static T* createNew(Ts&&...args) {
+ static_assert(std::is_base_of<Plugin, T>::value, "The Plugin::create() can only be used to instantiate objects of base-type Plugin");
+ static_assert(!std::is_pointer<T>::value, "Plugin::create() can't be used to instantiate pointers. It will return a pointer automatically");
+
+ T *instancePtr = new T(std::forward<Ts>(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 000000000..e70877067
--- /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 <https://www.mumble.info/LICENSE>.
+
+#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 <QtWidgets/QMessageBox>
+#include <QtCore/QUrl>
+#include <QtCore/QDir>
+#include <QtCore/QStandardPaths>
+#include <QtWidgets/QFileDialog>
+#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<QTreeWidgetItem *> 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<const_plugin_ptr_t > 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 000000000..cab9ce7b3
--- /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 <https://www.mumble.info/LICENSE>.
+
+#ifndef MUMBLE_MUMBLE_PLUGINS_H_
+#define MUMBLE_MUMBLE_PLUGINS_H_
+
+#include "ConfigDialog.h"
+#include "ui_PluginConfig.h"
+#include "Plugin.h"
+
+#include <QtCore/QObject>
+#include <QtCore/QMutex>
+#include <QtCore/QReadWriteLock>
+
+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
index b634d3f0b..fff242cac 100644
--- a/src/mumble/Plugins.ui
+++ b/src/mumble/PluginConfig.ui
@@ -6,14 +6,14 @@
<rect>
<x>0</x>
<y>0</y>
- <width>321</width>
- <height>235</height>
+ <width>570</width>
+ <height>289</height>
</rect>
</property>
<property name="windowTitle">
<string>Plugins</string>
</property>
- <layout class="QVBoxLayout">
+ <layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QGroupBox" name="qgbOptions">
<property name="title">
@@ -53,9 +53,6 @@
<attribute name="headerStretchLastSection">
<bool>false</bool>
</attribute>
- <attribute name="headerStretchLastSection">
- <bool>false</bool>
- </attribute>
<column>
<property name="text">
<string>Name</string>
@@ -63,7 +60,17 @@
</column>
<column>
<property name="text">
- <string>Enabled</string>
+ <string>Enable</string>
+ </property>
+ </column>
+ <column>
+ <property name="text">
+ <string>PA</string>
+ </property>
+ </column>
+ <column>
+ <property name="text">
+ <string>KeyEvents</string>
</property>
</column>
</widget>
@@ -84,6 +91,16 @@
</widget>
</item>
<item>
+ <widget class="QPushButton" name="qpbInstallPlugin">
+ <property name="toolTip">
+ <string>Install a plugin from a local file</string>
+ </property>
+ <property name="text">
+ <string>Install plugin...</string>
+ </property>
+ </widget>
+ </item>
+ <item>
<spacer>
<property name="orientation">
<enum>Qt::Horizontal</enum>
@@ -122,6 +139,16 @@
</property>
</widget>
</item>
+ <item>
+ <widget class="QPushButton" name="qpbUnload">
+ <property name="toolTip">
+ <string>Unload the currently selected plugin. This will remove it from the plugin list for the current session.</string>
+ </property>
+ <property name="text">
+ <string>Unload</string>
+ </property>
+ </widget>
+ </item>
</layout>
</item>
</layout>
diff --git a/src/mumble/PluginInstaller.cpp b/src/mumble/PluginInstaller.cpp
new file mode 100644
index 000000000..41494d64e
--- /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 <https://www.mumble.info/LICENSE>.
+
+#include "PluginInstaller.h"
+#include "Global.h"
+
+#include <QtCore/QString>
+#include <QtCore/QException>
+#include <QtCore/QObject>
+#include <QtCore/QStringList>
+#include <QtCore/QDir>
+
+#include <QtGui/QIcon>
+
+#include <exception>
+#include <string>
+#include <fstream>
+
+#include <Poco/Zip/ZipArchive.h>
+#include <Poco/Zip/ZipStream.h>
+#include <Poco/StreamCopier.h>
+#include <Poco/Exception.h>
+
+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<Plugin>(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<QString>(pluginVersion)).arg(
+ usedAPIVersion == VERSION_UNKNOWN ? "Unknown" : static_cast<QString>(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 000000000..d8df3c303
--- /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 <https://www.mumble.info/LICENSE>.
+
+#ifndef MUMBLE_MUMBLE_PLUGININSTALLER_H_
+#define MUMBLE_MUMBLE_PLUGININSTALLER_H_
+
+
+#include <QtCore/QFileInfo>
+#include <QtCore/QException>
+
+#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 000000000..39f575dea
--- /dev/null
+++ b/src/mumble/PluginInstaller.ui
@@ -0,0 +1,243 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>PluginInstaller</class>
+ <widget class="QDialog" name="PluginInstaller">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>360</width>
+ <height>332</height>
+ </rect>
+ </property>
+ <property name="windowTitle">
+ <string>PluginInstaller</string>
+ </property>
+ <property name="sizeGripEnabled">
+ <bool>false</bool>
+ </property>
+ <property name="modal">
+ <bool>false</bool>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <item>
+ <widget class="QLabel" name="qlPrompt">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Expanding" vsizetype="Minimum">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="frameShape">
+ <enum>QFrame::NoFrame</enum>
+ </property>
+ <property name="text">
+ <string>You are about to install the plugin listed below. Do you wish to proceed?</string>
+ </property>
+ <property name="wordWrap">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="Line" name="line">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QScrollArea" name="scrollArea">
+ <property name="frameShape">
+ <enum>QFrame::NoFrame</enum>
+ </property>
+ <property name="widgetResizable">
+ <bool>true</bool>
+ </property>
+ <widget class="QWidget" name="qwPluginInfo">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>348</width>
+ <height>208</height>
+ </rect>
+ </property>
+ <layout class="QFormLayout" name="formLayout">
+ <property name="labelAlignment">
+ <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
+ </property>
+ <property name="verticalSpacing">
+ <number>12</number>
+ </property>
+ <item row="0" column="0">
+ <widget class="QLabel" name="qlName_label">
+ <property name="text">
+ <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Name:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="1">
+ <widget class="QLabel" name="qlName">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Expanding" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string notr="true"/>
+ </property>
+ <property name="wordWrap">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="0">
+ <widget class="QLabel" name="qlVersion_label">
+ <property name="text">
+ <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Version:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+ </property>
+ </widget>
+ </item>
+ <item row="1" column="1">
+ <widget class="QLabel" name="qlVersion">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Expanding" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string notr="true"/>
+ </property>
+ <property name="wordWrap">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="0">
+ <widget class="QLabel" name="qlAuthor_label">
+ <property name="text">
+ <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Author(s):&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+ </property>
+ </widget>
+ </item>
+ <item row="2" column="1">
+ <widget class="QLabel" name="qlAuthor">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Expanding" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string notr="true"/>
+ </property>
+ <property name="wordWrap">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item row="3" column="0">
+ <widget class="QLabel" name="qlDescription_label">
+ <property name="text">
+ <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;Description:&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+ </property>
+ </widget>
+ </item>
+ <item row="3" column="1">
+ <widget class="QLabel" name="qlDescription">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Expanding" vsizetype="Expanding">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string notr="true"/>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
+ </property>
+ <property name="wordWrap">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </widget>
+ </item>
+ <item>
+ <widget class="Line" name="line_2">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QFrame" name="frame">
+ <layout class="QHBoxLayout" name="horizontalLayout">
+ <property name="spacing">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>6</number>
+ </property>
+ <property name="bottomMargin">
+ <number>6</number>
+ </property>
+ <item>
+ <widget class="QPushButton" name="qpbNo">
+ <property name="minimumSize">
+ <size>
+ <width>80</width>
+ <height>30</height>
+ </size>
+ </property>
+ <property name="text">
+ <string>&amp;No</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="horizontalSpacer">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>40</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <widget class="QPushButton" name="qpbYes">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Maximum" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="minimumSize">
+ <size>
+ <width>80</width>
+ <height>30</height>
+ </size>
+ </property>
+ <property name="text">
+ <string>&amp;Yes</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>
diff --git a/src/mumble/PluginManager.cpp b/src/mumble/PluginManager.cpp
new file mode 100644
index 000000000..817eb69be
--- /dev/null
+++ b/src/mumble/PluginManager.cpp
@@ -0,0 +1,933 @@
+// Copyright 2021 The Mumble Developers. All rights reserved.
+// Use of this source code is governed by a BSD-style license
+// that can be found in the LICENSE file at the root of the
+// Mumble source tree or at <https://www.mumble.info/LICENSE>.
+
+#include <limits>
+
+#include "PluginManager.h"
+#include "LegacyPlugin.h"
+#include <QReadLocker>
+#include <QWriteLocker>
+#include <QReadLocker>
+#include <QDir>
+#include <QFileInfoList>
+#include <QFileInfo>
+#include <QVector>
+#include <QByteArray>
+#include <QChar>
+#include <QMutexLocker>
+#include <QHashIterator>
+#include <QKeyEvent>
+#include <QTimer>
+
+#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 <memory>
+
+#ifdef Q_OS_WIN
+ #include <tlhelp32.h>
+ #include <string>
+#endif
+
+#ifdef Q_OS_LINUX
+ #include <QtCore/QStringList>
+#endif
+
+PluginManager::PluginManager(QSet<QString> *additionalSearchPaths, QObject *p)
+ : QObject(p),
+ m_pluginCollectionLock(QReadWriteLock::NonRecursive),
+ m_pluginHashMap(),
+ m_positionalData(),
+ m_positionalDataCheckTimer(),
+ m_sentDataMutex(),
+ m_sentData(),
+ m_activePosDataPluginLock(QReadWriteLock::NonRecursive),
+ m_activePositionalDataPlugin(),
+ m_updater() {
+
+ // Setup search-paths
+ if (additionalSearchPaths) {
+ for (const auto &currentPath : *additionalSearchPaths) {
+ m_pluginSearchPaths.insert(currentPath);
+ }
+ }
+
+#ifdef Q_OS_MAC
+ // Path to plugins inside AppBundle
+ m_pluginSearchPaths.insert(QString::fromLatin1("%1/../Plugins").arg(qApp->applicationDirPath()));
+#endif
+
+#ifdef MUMBLE_PLUGIN_PATH
+ // Path to where plugins are/will be installed on the system
+ m_pluginSearchPaths.insert(QString::fromLatin1(MUMTEXT(MUMBLE_PLUGIN_PATH)));
+#endif
+
+ // Path to "plugins" dir right next to the executable's location. This is the case for when Mumble
+ // is run after compilation without having installed it anywhere special
+ m_pluginSearchPaths.insert(QString::fromLatin1("%1/plugins").arg(MumbleApplication::instance()->applicationVersionRootPath()));
+
+ // Path to where the plugin installer will write plugins
+ m_pluginSearchPaths.insert(PluginInstaller::getInstallDir());
+
+#ifdef Q_OS_WIN
+ // According to MS KB Q131065, we need this to OpenProcess()
+
+ m_hToken = nullptr;
+
+ if (!OpenThreadToken(GetCurrentThread(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, FALSE, &m_hToken)) {
+ if (GetLastError() == ERROR_NO_TOKEN) {
+ ImpersonateSelf(SecurityImpersonation);
+ OpenThreadToken(GetCurrentThread(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, FALSE, &m_hToken);
+ }
+ }
+
+ TOKEN_PRIVILEGES tp;
+ LUID luid;
+ m_cbPrevious=sizeof(TOKEN_PRIVILEGES);
+
+ LookupPrivilegeValue(NULL, SE_DEBUG_NAME, &luid);
+
+ tp.PrivilegeCount = 1;
+ tp.Privileges[0].Luid = luid;
+ tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
+
+ AdjustTokenPrivileges(m_hToken, FALSE, &tp, sizeof(TOKEN_PRIVILEGES), &m_tpPrevious, &m_cbPrevious);
+#endif
+
+ // Synchronize the positional data in a regular interval
+ // By making this the parent of the created timer, we don't have to delete it explicitly
+ QTimer *serverSyncTimer = new QTimer(this);
+ QObject::connect(serverSyncTimer, &QTimer::timeout, this, &PluginManager::on_syncPositionalData);
+ serverSyncTimer->start(POSITIONAL_SERVER_SYNC_INTERVAL);
+
+ // Install this manager as a global eventFilter in order to get notified about all keypresses
+ if (QCoreApplication::instance()) {
+ QCoreApplication::instance()->installEventFilter(this);
+ }
+
+ // Set up the timer for regularly checking for available positional data plugins
+ m_positionalDataCheckTimer.setInterval(POSITIONAL_DATA_CHECK_INTERVAL);
+ m_positionalDataCheckTimer.start();
+ QObject::connect(&m_positionalDataCheckTimer, &QTimer::timeout, this, &PluginManager::checkForAvailablePositionalDataPlugin);
+
+ QObject::connect(&m_updater, &PluginUpdater::updatesAvailable, this, &PluginManager::on_updatesAvailable);
+ QObject::connect(this, &PluginManager::keyEvent, this, &PluginManager::on_keyEvent);
+}
+
+PluginManager::~PluginManager() {
+ clearPlugins();
+
+#ifdef Q_OS_WIN
+ AdjustTokenPrivileges(m_hToken, FALSE, &m_tpPrevious, m_cbPrevious, NULL, NULL);
+ CloseHandle(m_hToken);
+#endif
+}
+
+/// Emits a log about a plugin with the given name having lost link (positional audio)
+///
+/// @param pluginName The name of the plugin that lost link
+void reportLostLink(const QString& pluginName) {
+ Global::get().l->log(Log::Information, PluginManager::tr("%1 lost link").arg(pluginName.toHtmlEscaped()));
+}
+
+bool PluginManager::eventFilter(QObject *target, QEvent *event) {
+ if (event->type() == QEvent::KeyPress || event->type() == QEvent::KeyRelease) {
+ static QVector<QKeyEvent*> processedEvents;
+
+ QKeyEvent *kEvent = static_cast<QKeyEvent *>(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("<builtin>"), "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 &currentPath : m_pluginSearchPaths) {
+ QFileInfoList currentList = QDir(currentPath).entryInfoList();
+
+ for (int k=0; k<currentList.size(); k++) {
+ QFileInfo currentInfo = currentList[k];
+
+ if (!QLibrary::isLibrary(currentInfo.absoluteFilePath())) {
+ // consider only files that actually could be libraries
+ continue;
+ }
+
+ try {
+ plugin_ptr_t p(Plugin::createNew<Plugin>(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<LegacyPlugin>(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<ManualPlugin> mp(Plugin::createNew<ManualPlugin>());
+
+ m_pluginHashMap.insert(mp->getID(), mp);
+#ifdef MUMBLE_PLUGIN_DEBUG
+ LOG_FOUND_BUILTIN(mp);
+#endif
+ } catch(const PluginError& e) {
+ // At the time this function is running the MainWindow is not necessarily created yet, so we can't use
+ // the normal Log::log function
+ Log::logOrDefer(Log::Warning, tr("Failed at loading manual plugin: %1").arg(QString::fromUtf8(e.what())));
+ }
+#endif
+ }
+
+ QReadLocker readLock(&m_pluginCollectionLock);
+
+ // load plugins based on settings
+ // iterate over all plugins that have saved settings
+ auto it = Global::get().s.qhPluginSettings.constBegin();
+ while (it != Global::get().s.qhPluginSettings.constEnd()) {
+ // for this we need a way to get a plugin based on the filepath
+ const QString pluginKey = it.key();
+ const PluginSetting setting = it.value();
+
+ // iterate over all loaded plugins to see if the current setting is applicable
+ auto pluginIt = m_pluginHashMap.begin();
+ while (pluginIt != m_pluginHashMap.end()) {
+ plugin_ptr_t plugin = pluginIt.value();
+ QString pluginHash = QLatin1String(QCryptographicHash::hash(plugin->getFilePath().toUtf8(), QCryptographicHash::Sha1).toHex());
+ if (pluginKey == pluginHash) {
+ if (setting.enabled) {
+ loadPlugin(plugin->getID());
+
+ const uint32_t features = plugin->getFeatures();
+
+ if (!setting.positionalDataEnabled && (features & FEATURE_POSITIONAL)) {
+ // try to deactivate the feature if the setting says so
+ plugin->deactivateFeatures(FEATURE_POSITIONAL);
+ }
+ }
+
+ // positional data is a special feature that has to be enabled/disabled in the Plugin wrapper class
+ // additionally to telling the plugin library that the feature shall be deactivated
+ plugin->enablePositionalData(setting.positionalDataEnabled);
+
+ plugin->allowKeyboardMonitoring(setting.allowKeyboardMonitoring);
+
+ break;
+ }
+
+ pluginIt++;
+ }
+
+ it++;
+ }
+}
+
+const_plugin_ptr_t PluginManager::getPlugin(plugin_id_t pluginID) const {
+ QReadLocker lock(&m_pluginCollectionLock);
+
+ return m_pluginHashMap.value(pluginID);
+}
+
+void PluginManager::checkForPluginUpdates() {
+ m_updater.checkForUpdates();
+}
+
+bool PluginManager::fetchPositionalData() {
+ if (Global::get().bPosTest) {
+ // This is for testing-purposes only so the "fetched" position doesn't have any real meaning
+ m_positionalData.reset();
+
+ m_positionalData.m_playerDir.z = 1.0f;
+ m_positionalData.m_playerAxis.y = 1.0f;
+ m_positionalData.m_cameraDir.z = 1.0f;
+ m_positionalData.m_cameraAxis.y = 1.0f;
+
+ return true;
+ }
+
+ QReadLocker activePluginLock(&m_activePosDataPluginLock);
+
+ if (!m_activePositionalDataPlugin) {
+ // It appears as if there is currently no plugin capable of delivering positional audio
+ // Set positional data to zero-values
+ m_positionalData.reset();
+
+ return false;
+ }
+
+ QWriteLocker posDataLock(&m_positionalData.m_lock);
+
+ bool retStatus = m_activePositionalDataPlugin->fetchPositionalData(m_positionalData.m_playerPos, m_positionalData.m_playerDir,
+ m_positionalData.m_playerAxis, m_positionalData.m_cameraPos, m_positionalData.m_cameraDir, m_positionalData.m_cameraAxis,
+ m_positionalData.m_context, m_positionalData.m_identity);
+
+ // Add the plugin's name to the context as well to prevent name-clashes between plugins
+ if (!m_positionalData.m_context.isEmpty()) {
+ m_positionalData.m_context = m_activePositionalDataPlugin->getName() + QChar::Null + m_positionalData.m_context;
+ }
+
+ if (!retStatus) {
+ // Shut the currently active plugin down and set a new one (if available)
+ m_activePositionalDataPlugin->shutdownPositionalData();
+
+ reportLostLink(m_activePositionalDataPlugin->getName());
+
+ // unlock the read-lock in order to allow selectActivePositionaldataPlugin to gain a write-lock
+ activePluginLock.unlock();
+
+ selectActivePositionalDataPlugin();
+ } else {
+ // If the return-status doesn't indicate an error, we can assume that positional data is available
+ // The remaining problematic case is, if the player is exactly at position (0,0,0) as this is used as an indicator for the
+ // absence of positional data in the mix() function in AudioOutput.cpp
+ // Thus we have to make sure that this position is never set if positional data is actually available.
+ // We solve this problem by shifting the player a minimal amount on the z-axis
+ if (m_positionalData.m_playerPos == Position3D(0.0f, 0.0f, 0.0f)) {
+ m_positionalData.m_playerPos = {0.0f, 0.0f, std::numeric_limits<float>::min()};
+ }
+ if (m_positionalData.m_cameraPos == Position3D(0.0f, 0.0f, 0.0f)) {
+ m_positionalData.m_cameraPos = {0.0f, 0.0f, std::numeric_limits<float>::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<const_plugin_ptr_t > PluginManager::getPlugins(bool sorted) const {
+ QReadLocker lock(&m_pluginCollectionLock);
+
+ QVector<const_plugin_ptr_t> pluginList;
+
+ auto it = m_pluginHashMap.constBegin();
+ if (sorted) {
+ QList<plugin_id_t> 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<void(Plugin&)> 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<ClientUser*>(QObject::sender());
+#ifdef MUMBLE_PLUGIN_CALLBACK_DEBUG
+ if (user) {
+ qDebug() << "PluginManager: User" << user->qsName << "changed talking state to" << getTalkingStateStr(user->tsState);
+ } else {
+ qCritical() << "PluginManager: Unable to identify ClientUser";
+ }
+#endif
+
+ if (user) {
+ // Convert Mumble's talking state to the TalkingState used in the API
+ mumble_talking_state_t ts = INVALID;
+
+ switch(user->tsState) {
+ case Settings::TalkState::Passive:
+ ts = PASSIVE;
+ break;
+ case Settings::TalkState::Talking:
+ ts = TALKING;
+ break;
+ case Settings::TalkState::Whispering:
+ ts = WHISPERING;
+ break;
+ case Settings::TalkState::Shouting:
+ ts = SHOUTING;
+ break;
+ case Settings::TalkState::MutedTalking:
+ ts = TALKING_MUTED;
+ break;
+ }
+
+ if (ts == INVALID) {
+ qWarning("PluginManager.cpp: Invalid talking state encountered");
+ // An error occured
+ return;
+ }
+
+ const mumble_connection_t connectionID = Global::get().sh->getConnectionID();
+
+ foreachPlugin([user, ts, connectionID](Plugin& plugin) {
+ if (plugin.isLoaded()) {
+ plugin.onUserTalkingStateChanged(connectionID, user->uiSession, ts);
+ }
+ });
+ }
+}
+
+void PluginManager::on_audioInput(short *inputPCM, unsigned int sampleCount, unsigned int channelCount, unsigned int sampleRate, bool isSpeech) const {
+#ifdef MUMBLE_PLUGIN_CALLBACK_DEBUG
+ qDebug() << "PluginManager: AudioInput with" << channelCount << "channels and" << sampleCount << "samples per channel. IsSpeech:" << isSpeech;
+#endif
+
+ foreachPlugin([inputPCM, sampleCount, channelCount, sampleRate, isSpeech](Plugin& plugin) {
+ if (plugin.isLoaded()) {
+ plugin.onAudioInput(inputPCM, sampleCount, channelCount, sampleRate, isSpeech);
+ }
+ });
+}
+
+void PluginManager::on_audioSourceFetched(float* outputPCM, unsigned int sampleCount, unsigned int channelCount, unsigned int sampleRate, bool isSpeech, const ClientUser* user) const {
+#ifdef MUMBLE_PLUGIN_CALLBACK_DEBUG
+ qDebug() << "PluginManager: AudioSource with" << channelCount << "channels and" << sampleCount << "samples per channel fetched. IsSpeech:" << isSpeech;
+ if (user != nullptr) {
+ qDebug() << "Sender-ID:" << user->uiSession;
+ }
+#endif
+
+ foreachPlugin([outputPCM, sampleCount, channelCount, sampleRate, isSpeech, user](Plugin& plugin) {
+ if (plugin.isLoaded()) {
+ plugin.onAudioSourceFetched(outputPCM, sampleCount, channelCount, sampleRate, isSpeech, user ? user->uiSession : -1);
+ }
+ });
+}
+
+void PluginManager::on_audioOutputAboutToPlay(float *outputPCM, unsigned int sampleCount, unsigned int channelCount, unsigned int sampleRate, bool *modifiedAudio) const {
+#ifdef MUMBLE_PLUGIN_CALLBACK_DEBUG
+ qDebug() << "PluginManager: AudioOutput with" << channelCount << "channels and" << sampleCount << "samples per channel";
+#endif
+ foreachPlugin([outputPCM, sampleCount, channelCount, sampleRate, modifiedAudio](Plugin& plugin) {
+ if (plugin.isLoaded()) {
+ if(plugin.onAudioOutputAboutToPlay(outputPCM, sampleCount, sampleRate, channelCount)) {
+ *modifiedAudio = true;
+ }
+ }
+ });
+}
+
+void PluginManager::on_receiveData(const ClientUser *sender, const uint8_t *data, size_t dataLength, const char *dataID) const {
+#ifdef MUMBLE_PLUGIN_CALLBACK_DEBUG
+ qDebug() << "PluginManager: Data with ID" << dataID << "and length" << dataLength << "received. Sender-ID:" << sender->uiSession;
+#endif
+
+ const mumble_connection_t connectionID = Global::get().sh->getConnectionID();
+
+ foreachPlugin([sender, data, dataLength, dataID, connectionID](Plugin& plugin) {
+ if (plugin.isLoaded()) {
+ plugin.onReceiveData(connectionID, sender->uiSession, data, dataLength, dataID);
+ }
+ });
+}
+
+void PluginManager::on_serverSynchronized() const {
+#ifdef MUMBLE_PLUGIN_CALLBACK_DEBUG
+ qDebug() << "PluginManager: Server synchronized";
+#endif
+
+ const mumble_connection_t connectionID = Global::get().sh->getConnectionID();
+
+ foreachPlugin([connectionID](Plugin& plugin) {
+ if (plugin.isLoaded()) {
+ plugin.onServerSynchronized(connectionID);
+ }
+ });
+}
+
+void PluginManager::on_userAdded(mumble_userid_t userID) const {
+#ifdef MUMBLE_PLUGIN_CALLBACK_DEBUG
+ qDebug() << "PluginManager: Added user with ID" << userID;
+#endif
+
+ const mumble_connection_t connectionID = Global::get().sh->getConnectionID();
+
+ foreachPlugin([userID, connectionID](Plugin& plugin) {
+ if (plugin.isLoaded()) {
+ plugin.onUserAdded(connectionID, userID);
+ };
+ });
+}
+
+void PluginManager::on_userRemoved(mumble_userid_t userID) const {
+#ifdef MUMBLE_PLUGIN_CALLBACK_DEBUG
+ qDebug() << "PluginManager: Removed user with ID" << userID;
+#endif
+
+ const mumble_connection_t connectionID = Global::get().sh->getConnectionID();
+
+ foreachPlugin([userID, connectionID](Plugin& plugin) {
+ if (plugin.isLoaded()) {
+ plugin.onUserRemoved(connectionID, userID);
+ };
+ });
+}
+
+void PluginManager::on_channelAdded(mumble_channelid_t channelID) const {
+#ifdef MUMBLE_PLUGIN_CALLBACK_DEBUG
+ qDebug() << "PluginManager: Added channel with ID" << channelID;
+#endif
+
+ const mumble_connection_t connectionID = Global::get().sh->getConnectionID();
+
+ foreachPlugin([channelID, connectionID](Plugin& plugin) {
+ if (plugin.isLoaded()) {
+ plugin.onChannelAdded(connectionID, channelID);
+ };
+ });
+}
+
+void PluginManager::on_channelRemoved(mumble_channelid_t channelID) const {
+#ifdef MUMBLE_PLUGIN_CALLBACK_DEBUG
+ qDebug() << "PluginManager: Removed channel with ID" << channelID;
+#endif
+
+ const mumble_connection_t connectionID = Global::get().sh->getConnectionID();
+
+ foreachPlugin([channelID, connectionID](Plugin& plugin) {
+ if (plugin.isLoaded()) {
+ plugin.onChannelRemoved(connectionID, channelID);
+ };
+ });
+}
+
+void PluginManager::on_channelRenamed(int channelID) const {
+#ifdef MUMBLE_PLUGIN_CALLBACK_DEBUG
+ qDebug() << "PluginManager: Renamed channel with ID" << channelID;
+#endif
+
+ const mumble_connection_t connectionID = Global::get().sh->getConnectionID();
+
+ foreachPlugin([channelID, connectionID](Plugin& plugin) {
+ if (plugin.isLoaded()) {
+ plugin.onChannelRenamed(connectionID, channelID);
+ };
+ });
+}
+
+void PluginManager::on_keyEvent(unsigned int key, Qt::KeyboardModifiers modifiers, bool isPress) const {
+#ifdef MUMBLE_PLUGIN_CALLBACK_DEBUG
+ qDebug() << "PluginManager: Key event detected: keyCode =" << key << "modifiers:"
+ << modifiers << "isPress =" << isPress;
+#else
+ Q_UNUSED(modifiers);
+#endif
+
+ // Convert from Qt encoding to our own encoding
+ mumble_keycode_t keyCode = API::qtKeyCodeToAPIKeyCode(key);
+
+ foreachPlugin([keyCode, isPress](Plugin &plugin) {
+ if (plugin.isLoaded()) {
+ plugin.onKeyEvent(keyCode, isPress);
+ }
+ });
+}
+
+void PluginManager::on_syncPositionalData() {
+ // fetch positional data
+ if (fetchPositionalData()) {
+ // Sync the gathered data (context + identity) with the server
+ if (!Global::get().uiSession) {
+ // For some reason the local session ID is not set -> clear all data sent to the server in order to gurantee
+ // a re-send once the session is restored and there is data available
+ QMutexLocker mLock(&m_sentDataMutex);
+
+ m_sentData.context.clear();
+ m_sentData.identity.clear();
+ } else {
+ // Check if the identity and/or the context has changed and if it did, send that new info to the server
+ QMutexLocker mLock(&m_sentDataMutex);
+ QReadLocker rLock(&m_positionalData.m_lock);
+
+ if (m_sentData.context != m_positionalData.m_context || m_sentData.identity != m_positionalData.m_identity ) {
+ MumbleProto::UserState mpus;
+ mpus.set_session(Global::get().uiSession);
+
+ if (m_sentData.context != m_positionalData.m_context) {
+ m_sentData.context = m_positionalData.m_context;
+ mpus.set_plugin_context(m_sentData.context.toUtf8().constData(), m_sentData.context.size());
+ }
+ if (m_sentData.identity != m_positionalData.m_identity) {
+ m_sentData.identity = m_positionalData.m_identity;
+ mpus.set_plugin_identity(m_sentData.identity.toUtf8().constData());
+ }
+
+ if (Global::get().sh) {
+ // send the message if the serverHandler is available
+ Global::get().sh->sendMessage(mpus);
+ }
+ }
+ }
+ }
+}
+
+void PluginManager::on_updatesAvailable() {
+ if (Global::get().s.bPluginAutoUpdate) {
+ m_updater.update();
+ } else {
+ m_updater.promptAndUpdate();
+ }
+}
+
+void PluginManager::checkForAvailablePositionalDataPlugin() {
+ bool performSearch = false;
+ {
+ QReadLocker activePluginLock(&m_activePosDataPluginLock);
+
+ performSearch = !m_activePositionalDataPlugin;
+ }
+
+ if (performSearch) {
+ selectActivePositionalDataPlugin();
+ }
+}
diff --git a/src/mumble/PluginManager.h b/src/mumble/PluginManager.h
new file mode 100644
index 000000000..02f4db5fa
--- /dev/null
+++ b/src/mumble/PluginManager.h
@@ -0,0 +1,279 @@
+// Copyright 2021 The Mumble Developers. All rights reserved.
+// Use of this source code is governed by a BSD-style license
+// that can be found in the LICENSE file at the root of the
+// Mumble source tree or at <https://www.mumble.info/LICENSE>.
+
+#ifndef MUMBLE_MUMBLE_PLUGINMANAGER_H_
+#define MUMBLE_MUMBLE_PLUGINMANAGER_H_
+
+#include <QObject>
+#include <QReadWriteLock>
+#include <QString>
+#include <QTimer>
+#include <QHash>
+#include <QMutex>
+#ifdef Q_OS_WIN
+ #ifndef NOMINMAX
+ #define NOMINMAX
+ #endif
+ #include <windows.h>
+#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 <functional>
+
+/// 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<plugin_id_t, plugin_ptr_t> m_pluginHashMap;
+ /// A set of directories to search plugins in
+ QSet<QString> m_pluginSearchPaths;
+#ifdef Q_OS_WIN
+ // This stuff is apparently needed on Windows in order to deal with DLLs
+ HANDLE m_hToken;
+ TOKEN_PRIVILEGES m_tpPrevious;
+ DWORD m_cbPrevious;
+#endif
+ /// The PositionalData object holding the current positional data (as retrieved by the respective plugin)
+ PositionalData m_positionalData;
+
+ /// A timer that causes the manager to regularly check for available plugins that can currently
+ /// deliver positional data.
+ QTimer m_positionalDataCheckTimer;
+
+ /// The mutex for sentData. This has to be aquired before accessing sentData
+ mutable QMutex m_sentDataMutex;
+ /// The bits of the positional data that have already been sent to the server. It is used to determine whether
+ /// the new data has to be sent to the server (in case it has changed). You have ti aquire sentDataMutex before
+ /// accessing this field.
+ PluginManager_SentData m_sentData;
+
+ /// The lock for activePositionalDataPlugin. It has to be aquired before accessing the respective field.
+ mutable QReadWriteLock m_activePosDataPluginLock;
+ /// The plugin that is currently used to retrieve positional data. You have to aquire activePosDataPluginLock before
+ /// accessing this field.
+ plugin_ptr_t m_activePositionalDataPlugin;
+ /// The PluginUpdater used to handle plugin updates.
+ PluginUpdater m_updater;
+
+ // We override the QObject::eventFilter function in order to be able to install the pluginManager as an event filter
+ // to the main application in order to get notified about keystrokes.
+ bool eventFilter(QObject *target, QEvent *event) Q_DECL_OVERRIDE;
+
+ /// Unloads all plugins that are currently loaded.
+ void unloadPlugins() const;
+ /// Clears the current list of plugins
+ void clearPlugins();
+ /// Iterates over the plugins and tries to select a plugin that currently claims to be able to deliver positional data. If
+ /// it found a plugin, activePositionalDataPlugin is set accordingly. If not, it is set to nullptr.
+ ///
+ /// @returns Whether this function succeeded in finding such a plugin
+ bool selectActivePositionalDataPlugin();
+
+ /// A internal helper function that iterates over all plugins and calls the given function providing the current plugin as
+ /// a parameter.
+ void foreachPlugin(std::function<void(Plugin&)>) const;
+ public:
+ // How often positional data (identity & context) should be synched with the server if there is any (in ms)
+ static constexpr int POSITIONAL_SERVER_SYNC_INTERVAL = 500;
+ // How often the manager should check for available positional data plugins
+ static constexpr int POSITIONAL_DATA_CHECK_INTERVAL = 1000;
+
+ /// Constructor
+ ///
+ /// @param additionalSearchPaths A pointer to a set of additional search paths or nullptr if no additional
+ /// paths are required.
+ /// @param p The parent QObject
+ PluginManager(QSet<QString> *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<const_plugin_ptr_t> getPlugins(bool sorted = false) const;
+ /// Loads the plugin with the given ID. Loading means initializing the plugin.
+ ///
+ /// @param pluginID The ID of the plugin to load
+ /// @returns Whether the plugin could be successfully loaded
+ bool loadPlugin(plugin_id_t pluginID) const;
+ /// Unloads the plugin with the given ID. Unloading means shutting the plugign down.
+ ///
+ /// @param pluginID The ID of the plugin to unload
+ void unloadPlugin(plugin_id_t pluginID) const;
+ /// Unloads the given plugin. Unloading means shutting the plugign down.
+ ///
+ /// @param plugin The plugin to unload
+ void unloadPlugin(Plugin &plugin) const;
+ /// Clears the plugin from the list of known plugins
+ ///
+ /// @param pluginID The ID of the plugin to forget about
+ /// @returns Whether the plugin has been cleared successfully
+ bool clearPlugin(plugin_id_t pluginID);
+ /// Deactivates the given features for the plugin with the given ID
+ ///
+ /// @param pluginID The ID of the plugin to access
+ /// @param features The feature set that should be deactivated. The features are or'ed together.
+ /// @returns The feature set that could not be deactivated
+ uint32_t deactivateFeaturesFor(plugin_id_t pluginID, uint32_t features) const;
+ /// Allows or forbids the given plugin to monitor keyboard events.
+ ///
+ /// @param pluginID The ID of the plugin to access
+ /// @param allow Whether to allow the monitoring or not
+ void allowKeyboardMonitoringFor(plugin_id_t pluginID, bool allow) const;
+ /// Checks whether a plugin with the given ID exists.
+ ///
+ /// @param pluginID The ID to check
+ /// @returns Whether such a plugin exists
+ bool pluginExists(plugin_id_t pluginID) const;
+
+ public slots:
+ /// Rescans the plugin directory and load all plugins from there after having cleared the current plugin list
+ void rescanPlugins();
+ /// Slot that gets called whenever data from another plugin has been received. This function will then delegate
+ /// this to the respective plugin callback
+ ///
+ /// @param sender A pointer to the ClientUser whose client has sent the data
+ /// @param data The byte-array representing the sent data
+ /// @param dataLength The length of the data array
+ /// @param dataID The ID of the data
+ void on_receiveData(const ClientUser *sender, const uint8_t *data, size_t dataLength, const char *dataID) const;
+ /// Slot that gets called when the local client connects to a server. It will delegate it to the respective plugin callback.
+ void on_serverConnected() const;
+ /// Slot that gets called when the local client disconnects to a server. It will delegate it to the respective plugin callback.
+ void on_serverDisconnected() const;
+ /// Slot that gets called when a client enters a channel. It will delegate it to the respective plugin callback.
+ ///
+ /// @param newChannel A pointer to the new channel
+ /// @param prevChannel A pointer to the previous channel or nullptr if no such channel exists
+ /// @param user A pointer to the user that entered the channel
+ void on_channelEntered(const Channel *newChannel, const Channel *prevChannel, const User *user) const;
+ /// Slot that gets called when a client leaves a channel. It will delegate it to the respective plugin callback.
+ ///
+ /// @param channel A pointer to the channel that has been left
+ /// @param user A pointer to the user that entered the channel
+ void on_channelExited(const Channel *channel, const User *user) const;
+ /// Slot that gets called when the local client changes its talking state. It will delegate it to the respective plugin callback.
+ void on_userTalkingStateChanged() const;
+ /// Slot that gets called when the local client receives audio input. It will delegate it to the respective plugin callback.
+ ///
+ /// @param inputPCM The array containing the input PCM (pulse-code-modulation). Its length is sampleCount * channelCount
+ /// @param sampleCount The amount of samples in the PCM array
+ /// @param channelCount The amount of channels in the PCM array
+ /// @param sampleRate The used sample rate in Hz
+ /// @param isSpeech Whether Mumble considers this input as speech
+ void on_audioInput(short *inputPCM, unsigned int sampleCount, unsigned int channelCount, unsigned int sampleRate, bool isSpeech) const;
+ /// Slot that gets called when the local client has fetched an audio source. It will delegate it to the respective plugin callback.
+ ///
+ /// @param outputPCM The array containing the output-PCM (pulse-code-modulation). Its length is sampleCount * channelCount
+ /// @param sampleCount The amount of samples in the PCM array
+ /// @param channelCount The amount of channels in the PCM array
+ /// @param sampleRate The used sample rate in Hz
+ /// @param isSpeech Whether Mumble considers this input as speech
+ /// @param user A pointer to the ClientUser the audio source corresposnds to
+ void on_audioSourceFetched(float *outputPCM, unsigned int sampleCount, unsigned int channelCount, unsigned int sampleRate, bool isSpeech,
+ const ClientUser *user) const;
+ /// Slot that gets called when the local client is about to play some audio. It will delegate it to the respective plugin callback.
+ ///
+ /// @param outputPCM The array containing the output-PCM (pulse-code-modulation). Its length is sampleCount * channelCount
+ /// @param sampleCount The amount of samples in the PCM array
+ /// @param channelCount The amount of channels in the PCM array
+ /// @param sampleRate The used sample rate in Hz
+ void on_audioOutputAboutToPlay(float *outputPCM, unsigned int sampleCount, unsigned int channelCount, unsigned int sampleRate,
+ bool *modifiedAudio) const;
+ /// Slot that gets called after the local client has finished synchronizing with the server. It will delegate it to the respective
+ /// plugin callback.
+ void on_serverSynchronized() const;
+ /// Slot that gets called when a new user is added to the user model. It will delegate it to the respective plugin callbacks.
+ ///
+ /// @param userID The ID of the added user
+ void on_userAdded(unsigned int userID) const;
+ /// Slot that gets called when a user is removed from the user model. It will delegate it to the respective plugin callbacks.
+ ///
+ /// @param userID The ID of the removed user
+ void on_userRemoved(unsigned int userID) const;
+ /// Slot that gets called when a new channel is added to the user model. It will delegate it to the respective plugin callbacks.
+ ///
+ /// @param channelID The ID of the added channel
+ void on_channelAdded(int channelID) const;
+ /// Slot that gets called when a channel is removed from the user model. It will delegate it to the respective plugin callbacks.
+ ///
+ /// @param channelID The ID of the removed channel
+ void on_channelRemoved(int channelID) const;
+ /// Slot that gets called when a channel is renamed. It will delegate it to the respective plugin callbacks.
+ ///
+ /// @param channelID The ID of the renamed channel
+ void on_channelRenamed(int channelID) const;
+ /// Slot that gets called when a key has been pressed or released while Mumble has keyboard focus.
+ ///
+ /// @param key The code of the affected key (as encoded by Qt::Key)
+ /// @param modifiers The modifiers that were active in the moment of the event
+ /// @param isPress True if the key has been pressed, false if it has been released
+ void on_keyEvent(unsigned int key, Qt::KeyboardModifiers modifiers, bool isPress) const;
+
+ /// Slot that gets called whenever the positional data should be synchronized with the server. Before it does that, it tries to
+ /// fetch new data.
+ void on_syncPositionalData();
+ /// Slot called if there are plugin updates available
+ void on_updatesAvailable();
+
+ protected slots:
+ /// If there is no active positional data plugin, this function will initiate searching for a
+ /// new one.
+ void checkForAvailablePositionalDataPlugin();
+
+ signals:
+ /// A signal emitted if the PluginManager (acting as an event filter) detected
+ /// a QKeyEvent.
+ ///
+ /// @param key The code of the affected key (as encoded by Qt::Key)
+ /// @param modifiers The modifiers that were active in the moment of the event
+ /// @param isPress True if the key has been pressed, false if it has been released
+ void keyEvent(unsigned int key, Qt::KeyboardModifiers modifiers, bool isPress);
+};
+
+#endif
diff --git a/src/mumble/PluginUpdater.cpp b/src/mumble/PluginUpdater.cpp
new file mode 100644
index 000000000..e7f4f2594
--- /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 <https://www.mumble.info/LICENSE>.
+
+#include "PluginUpdater.h"
+#include "PluginManager.h"
+#include "Log.h"
+#include "PluginInstaller.h"
+#include "Global.h"
+
+#include <QtWidgets/QCheckBox>
+#include <QtWidgets/QLabel>
+#include <QtCore/QHashIterator>
+#include <QtCore/QSignalBlocker>
+#include <QtCore/QByteArray>
+#include <QtCore/QDir>
+#include <QtCore/QFile>
+#include <QtConcurrent>
+#include <QNetworkRequest>
+
+#include <algorithm>
+
+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<const_plugin_ptr_t> 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 &currentPair = m_pluginUpdateWidgets[i];
+
+ static_cast<QFormLayout*>(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 &currentPair = 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 &currentPair = m_pluginUpdateWidgets[i];
+
+ currentPair.pluginCheckBox->setCheckState(static_cast<Qt::CheckState>(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 &currentPair = 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<plugin_id_t>(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<QNetworkReply::NetworkError>().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 000000000..2d9041d3d
--- /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 <https://www.mumble.info/LICENSE>.
+
+#ifndef MUMBLE_MUMBLE_PLUGINUPDATER_H_
+#define MUMBLE_MUMBLE_PLUGINUPDATER_H_
+
+#include <QtCore/QVector>
+#include <QtCore/QUrl>
+#include <QtCore/QMutex>
+#include <QNetworkAccessManager>
+#include <QNetworkReply>
+
+#include <atomic>
+
+#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<bool> 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<UpdateEntry> 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<UpdateWidgetPair> 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 000000000..8f2118d65
--- /dev/null
+++ b/src/mumble/PluginUpdater.ui
@@ -0,0 +1,224 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>PluginUpdater</class>
+ <widget class="QDialog" name="PluginUpdater">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>616</width>
+ <height>460</height>
+ </rect>
+ </property>
+ <property name="windowTitle">
+ <string>PluginUpdater</string>
+ </property>
+ <property name="sizeGripEnabled">
+ <bool>false</bool>
+ </property>
+ <property name="modal">
+ <bool>false</bool>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <item>
+ <widget class="QLabel" name="qlTitleText">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Expanding" vsizetype="Maximum">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string>The following plugins can be updated.</string>
+ </property>
+ <property name="wordWrap">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="verticalSpacer">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeType">
+ <enum>QSizePolicy::Fixed</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>20</width>
+ <height>7</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <widget class="Line" name="line_2">
+ <property name="styleSheet">
+ <string notr="true">background-color: rgb(94, 94, 94);</string>
+ </property>
+ <property name="lineWidth">
+ <number>1</number>
+ </property>
+ <property name="midLineWidth">
+ <number>0</number>
+ </property>
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="qcbSelectAll">
+ <property name="text">
+ <string>Select all</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QScrollArea" name="scrollArea">
+ <property name="autoFillBackground">
+ <bool>false</bool>
+ </property>
+ <property name="styleSheet">
+ <string notr="true"/>
+ </property>
+ <property name="frameShadow">
+ <enum>QFrame::Sunken</enum>
+ </property>
+ <property name="widgetResizable">
+ <bool>true</bool>
+ </property>
+ <widget class="QWidget" name="qwContent">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>600</width>
+ <height>284</height>
+ </rect>
+ </property>
+ <property name="styleSheet">
+ <string notr="true"/>
+ </property>
+ <layout class="QFormLayout" name="formLayout">
+ <property name="labelAlignment">
+ <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set>
+ </property>
+ <property name="horizontalSpacing">
+ <number>15</number>
+ </property>
+ <item row="0" column="0">
+ <widget class="QLabel" name="qlPlugin">
+ <property name="styleSheet">
+ <string notr="true">font-weight: bold;</string>
+ </property>
+ <property name="text">
+ <string>Plugin</string>
+ </property>
+ </widget>
+ </item>
+ <item row="0" column="1">
+ <widget class="QLabel" name="qlURL">
+ <property name="styleSheet">
+ <string notr="true">font-weight: bold;</string>
+ </property>
+ <property name="text">
+ <string>Download-URL</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </widget>
+ </item>
+ <item>
+ <widget class="Line" name="line">
+ <property name="styleSheet">
+ <string notr="true">background-color: rgb(94, 94, 94);</string>
+ </property>
+ <property name="lineWidth">
+ <number>1</number>
+ </property>
+ <property name="midLineWidth">
+ <number>0</number>
+ </property>
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="verticalSpacer_2">
+ <property name="orientation">
+ <enum>Qt::Vertical</enum>
+ </property>
+ <property name="sizeType">
+ <enum>QSizePolicy::Fixed</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>20</width>
+ <height>7</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <widget class="QLabel" name="qlUpdateSelected">
+ <property name="text">
+ <string>Do you want to update the selected plugins?</string>
+ </property>
+ <property name="wordWrap">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QDialogButtonBox" name="buttonBox">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="standardButtons">
+ <set>QDialogButtonBox::No|QDialogButtonBox::Yes</set>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <resources/>
+ <connections>
+ <connection>
+ <sender>buttonBox</sender>
+ <signal>accepted()</signal>
+ <receiver>PluginUpdater</receiver>
+ <slot>accept()</slot>
+ <hints>
+ <hint type="sourcelabel">
+ <x>248</x>
+ <y>254</y>
+ </hint>
+ <hint type="destinationlabel">
+ <x>157</x>
+ <y>274</y>
+ </hint>
+ </hints>
+ </connection>
+ <connection>
+ <sender>buttonBox</sender>
+ <signal>rejected()</signal>
+ <receiver>PluginUpdater</receiver>
+ <slot>reject()</slot>
+ <hints>
+ <hint type="sourcelabel">
+ <x>316</x>
+ <y>260</y>
+ </hint>
+ <hint type="destinationlabel">
+ <x>286</x>
+ <y>274</y>
+ </hint>
+ </hints>
+ </connection>
+ </connections>
+</ui>
diff --git a/src/mumble/Plugins.cpp b/src/mumble/Plugins.cpp
deleted file mode 100644
index 4510f757c..000000000
--- 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 <https://www.mumble.info/LICENSE>.
-
-#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 <QtCore/QLibrary>
-#include <QtCore/QUrlQuery>
-
-#ifdef Q_OS_WIN
-# include <QtCore/QTemporaryFile>
-#endif
-
-#include <QtWidgets/QMessageBox>
-#include <QtXml/QDomDocument>
-
-#ifdef Q_OS_WIN
-# include <softpub.h>
-# include <tlhelp32.h>
-#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 fd951bb73..000000000
--- 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 <https://www.mumble.info/LICENSE>.
-
-#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 <QtCore/QMutex>
-#include <QtCore/QObject>
-#include <QtCore/QReadWriteLock>
-#include <QtCore/QUrl>
-
-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 000000000..8a510e1df
--- /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 <https://www.mumble.info/LICENSE>.
+
+#include "PositionalData.h"
+
+#include <cmath>
+#include <cstdlib>
+#include <stdexcept>
+
+#include <QtCore/QReadLocker>
+
+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 000000000..a0f6f4013
--- /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 <https://www.mumble.info/LICENSE>.
+
+#ifndef MUMBLE_MUMBLE_POSITIONAL_AUDIO_CONTEXT_H_
+#define MUMBLE_MUMBLE_POSITIONAL_AUDIO_CONTEXT_H_
+
+#include <QtCore/QString>
+#include <QtCore/QReadWriteLock>
+
+/// 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 1e7291aac..429adc3f3 100644
--- a/src/mumble/ServerHandler.cpp
+++ b/src/mumble/ServerHandler.cpp
@@ -57,6 +57,10 @@
# include <sys/socket.h>
#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 5fc29c504..d20833aff 100644
--- a/src/mumble/ServerHandler.h
+++ b/src/mumble/ServerHandler.h
@@ -62,6 +62,9 @@ private:
Database *database;
+ static QMutex nextConnectionIDMutex;
+ static int nextConnectionID;
+
protected:
QString qsHostName;
QString qsUserName;
@@ -70,6 +73,7 @@ protected:
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 @@ public:
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 @@ public:
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 31d91bc68..aac13c4a9 100644
--- a/src/mumble/Settings.cpp
+++ b/src/mumble/Settings.cpp
@@ -15,6 +15,8 @@
#include <QtCore/QProcessEnvironment>
#include <QtCore/QStandardPaths>
+#include <QtCore/QFileInfo>
+#include <QtCore/QRegularExpression>
#include <QtGui/QImageReader>
#include <QtWidgets/QSystemTrayIcon>
#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 a074dd6ad..ab46e5391 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 35fca925d..ceb7c1dd4 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 c9824f25a..330500509 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 dad3cce16..a1ef34826 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 <QtCore/QProcess>
#include <QtGui/QDesktopServices>
@@ -58,7 +62,6 @@
# include <shellapi.h>
#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] [<url>]\n"
+ MainWindow::tr("Usage: mumble [options] [<url> | <plugin_list>]\n"
"\n"
"<url> specifies a URL to connect to after startup instead of showing\n"
"the connection window, and has the following form:\n"
"mumble://[<username>[:<password>]@]<host>[:<port>][/<channel>[/"
"<subchannel>...]][?version=<x.y.z>]\n"
"\n"
+ "<plugin_list> 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 cfde87e2a..6fb23e141 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<uint32_t> uniqueReceivers;
+ uniqueReceivers.reserve(receiverSessions.size());
+
+ for(int i = 0; static_cast<size_t>(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 6ebaeb18d..5e1456167 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 bac90df5b..91b48f535 100644
--- a/src/murmur/Meta.h
+++ b/src/murmur/Meta.h
@@ -104,6 +104,9 @@ public:
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 4c8dd0939..c0c979814 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 abebd0ffe..ac854cea6 100644
--- a/src/murmur/Server.h
+++ b/src/murmur/Server.h
@@ -140,6 +140,9 @@ public:
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 f2dac8abb..367889bee 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 fd40071ca..c34ebeaf7 100644
--- a/src/murmur/ServerUser.h
+++ b/src/murmur/ServerUser.h
@@ -147,6 +147,7 @@ public:
QMap< QString, QString > qmWhisperRedirect;
LeakyBucket leakyBucket;
+ LeakyBucket m_pluginMessageBucket;
int iLastPermissionCheck;
QMap< int, unsigned int > qmPermissionSent;