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:
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;