diff options
Diffstat (limited to 'src')
103 files changed, 15607 insertions, 1974 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 ¤t : m_entries) { + const Entry &entry = current.second; + + // Delete leaked resource + entry.m_deleter(current.first); + + // Print an error about the leaked resource + printf("[ERROR]: Plugin with ID %d leaked memory from a call to API function \"%s\"\n", entry.m_pluginID, entry.m_sourceFunctionName); + } +} +// Some common delete-functions +void defaultDeleter(const void *ptr) { + // We use const-cast in order to circumvent the shortcoming of the free() signature only taking + // in void * and not const void *. Delete on the other hand is allowed on const pointers which is + // why this is an okay thing to do. + // See also https://stackoverflow.com/questions/2819535/unable-to-free-const-pointers-in-c + free(const_cast< void * >(ptr)); +} + + +///////////////////////////////////////////////////////////////////////////////////////// +/////////////////////////////////// API IMPLEMENTATION ////////////////////////////////// +///////////////////////////////////////////////////////////////////////////////////////// + +// This macro registers type, type * and type ** to Qt's metatype system +// and also their const variants (except const value as that doesn't make sense) +#define REGISTER_METATYPE(type) \ + qRegisterMetaType< type >(#type); \ + qRegisterMetaType< type * >(#type " *"); \ + qRegisterMetaType< type ** >(#type " **"); \ + qRegisterMetaType< const type * >("const " #type " *"); \ + qRegisterMetaType< const type ** >("const " #type " **"); \ + +MumbleAPI::MumbleAPI() { + // Move this object to the main thread + moveToThread(qApp->thread()); + + // Register all API types to Qt's metatype system + REGISTER_METATYPE(bool); + REGISTER_METATYPE(char); + REGISTER_METATYPE(double); + REGISTER_METATYPE(int); + REGISTER_METATYPE(int64_t); + REGISTER_METATYPE(mumble_channelid_t); + REGISTER_METATYPE(mumble_connection_t); + REGISTER_METATYPE(mumble_plugin_id_t); + REGISTER_METATYPE(mumble_settings_key_t); + REGISTER_METATYPE(mumble_transmission_mode_t); + REGISTER_METATYPE(mumble_userid_t); + REGISTER_METATYPE(mumble_userid_t); + REGISTER_METATYPE(size_t); + REGISTER_METATYPE(uint8_t); + + // Define additional types that can't be defined using macro REGISTER_METATYPE + qRegisterMetaType< api_promise_t * >("api_promise_t *"); + qRegisterMetaType< API::api_promise_t * >("API::api_promise_t *"); + qRegisterMetaType< const void * >("const void *"); + qRegisterMetaType< const void ** >("const void **"); + qRegisterMetaType< void * >("void *"); + qRegisterMetaType< void ** >("void **"); +} + +#undef REFGISTER_METATYPE + +MumbleAPI &MumbleAPI::get() { + static MumbleAPI api; + + return api; +} + +void MumbleAPI::freeMemory_v_1_0_x(mumble_plugin_id_t callerID, const void *ptr, api_promise_t *promise) { + if (QThread::currentThread() != thread()) { + // Invoke in main thread + QMetaObject::invokeMethod(this, "freeMemory_v_1_0_x", Qt::QueuedConnection, Q_ARG(mumble_plugin_id_t, callerID), + Q_ARG(const void *, ptr), Q_ARG(api_promise_t *, promise)); + + return; + } + + // Don't verify plugin ID here to avoid memory leaks + UNUSED(callerID); + + auto it = m_curator.m_entries.find(ptr); + if (it != m_curator.m_entries.cend()) { + MumbleAPICurator::Entry &entry = (*it).second; + + // call the deleter to delete the resource + entry.m_deleter(ptr); + + // Remove pointer from curator + m_curator.m_entries.erase(it); + + EXIT_WITH(STATUS_OK); + } else { + EXIT_WITH(EC_POINTER_NOT_FOUND); + } +} + +void MumbleAPI::getActiveServerConnection_v_1_0_x(mumble_plugin_id_t callerID, mumble_connection_t *connection, + api_promise_t *promise) { + if (QThread::currentThread() != thread()) { + // Invoke in main thread + QMetaObject::invokeMethod(this, "getActiveServerConnection_v_1_0_x", Qt::QueuedConnection, + Q_ARG(mumble_plugin_id_t, callerID), Q_ARG(mumble_connection_t *, connection), + Q_ARG(api_promise_t *, promise)); + + return; + } + + VERIFY_PLUGIN_ID(callerID); + + if (Global::get().sh) { + *connection = Global::get().sh->getConnectionID(); + + EXIT_WITH(STATUS_OK); + } else { + EXIT_WITH(EC_NO_ACTIVE_CONNECTION); + } +} + +void MumbleAPI::isConnectionSynchronized_v_1_0_x(mumble_plugin_id_t callerID, mumble_connection_t connection, + bool *synchronized, api_promise_t *promise) { + if (QThread::currentThread() != thread()) { + // Invoke in main thread + QMetaObject::invokeMethod(this, "isConnectionSynchronized_v_1_0_x", Qt::QueuedConnection, + Q_ARG(mumble_plugin_id_t, callerID), Q_ARG(mumble_connection_t, connection), + Q_ARG(bool *, synchronized), Q_ARG(api_promise_t *, promise)); + + return; + } + + VERIFY_PLUGIN_ID(callerID); + VERIFY_CONNECTION(connection); + + // Right now there can only be one connection and if Global::get().uiSession is zero, then the synchronization has not finished + // yet (or there is no connection to begin with) + *synchronized = Global::get().uiSession != 0; + + EXIT_WITH(STATUS_OK); +} + +void MumbleAPI::getLocalUserID_v_1_0_x(mumble_plugin_id_t callerID, mumble_connection_t connection, + mumble_userid_t *userID, api_promise_t *promise) { + if (QThread::currentThread() != thread()) { + // Invoke in main thread + QMetaObject::invokeMethod(this, "getLocalUserID_v_1_0_x", Qt::QueuedConnection, + Q_ARG(mumble_plugin_id_t, callerID), Q_ARG(mumble_connection_t, connection), + Q_ARG(mumble_userid_t *, userID), Q_ARG(api_promise_t *, promise)); + + return; + } + + VERIFY_PLUGIN_ID(callerID); + + VERIFY_CONNECTION(connection); + ENSURE_CONNECTION_SYNCHRONIZED(connection); + + *userID = Global::get().uiSession; + + EXIT_WITH(STATUS_OK); +} + +void MumbleAPI::getUserName_v_1_0_x(mumble_plugin_id_t callerID, mumble_connection_t connection, mumble_userid_t userID, + const char **name, api_promise_t *promise) { + if (QThread::currentThread() != thread()) { + // Invoke in main thread + QMetaObject::invokeMethod(this, "getUserName_v_1_0_x", Qt::QueuedConnection, + Q_ARG(mumble_plugin_id_t, callerID), Q_ARG(mumble_connection_t, connection), + Q_ARG(mumble_userid_t, userID), Q_ARG(const char **, name), + Q_ARG(api_promise_t *, promise)); + + return; + } + + VERIFY_PLUGIN_ID(callerID); + + VERIFY_CONNECTION(connection); + ENSURE_CONNECTION_SYNCHRONIZED(connection); + + const ClientUser *user = ClientUser::get(userID); + + if (user) { + // +1 for NULL terminator + size_t size = user->qsName.toUtf8().size() + 1; + + char *nameArray = reinterpret_cast< char * >(malloc(size * sizeof(char))); + + std::strcpy(nameArray, user->qsName.toUtf8().data()); + + // save the allocated pointer and how to delete it + m_curator.m_entries.insert({ nameArray, { defaultDeleter, callerID, "getUserName" } }); + + *name = nameArray; + + EXIT_WITH(STATUS_OK); + } else { + EXIT_WITH(EC_USER_NOT_FOUND); + } +} + +void MumbleAPI::getChannelName_v_1_0_x(mumble_plugin_id_t callerID, mumble_connection_t connection, + mumble_channelid_t channelID, const char **name, api_promise_t *promise) { + if (QThread::currentThread() != thread()) { + // Invoke in main thread + QMetaObject::invokeMethod(this, "getChannelName_v_1_0_x", Qt::QueuedConnection, + Q_ARG(mumble_plugin_id_t, callerID), Q_ARG(mumble_connection_t, connection), + Q_ARG(mumble_channelid_t, channelID), Q_ARG(const char **, name), + Q_ARG(api_promise_t *, promise)); + + return; + } + + VERIFY_PLUGIN_ID(callerID); + + VERIFY_CONNECTION(connection); + ENSURE_CONNECTION_SYNCHRONIZED(connection); + + const Channel *channel = Channel::get(channelID); + + if (channel) { + // +1 for NULL terminator + size_t size = channel->qsName.toUtf8().size() + 1; + + char *nameArray = reinterpret_cast< char * >(malloc(size * sizeof(char))); + + std::strcpy(nameArray, channel->qsName.toUtf8().data()); + + // save the allocated pointer and how to delete it + m_curator.m_entries.insert({ nameArray, { defaultDeleter, callerID, "getChannelName" } }); + + *name = nameArray; + + EXIT_WITH(STATUS_OK); + } else { + EXIT_WITH(EC_CHANNEL_NOT_FOUND); + } +} + +void MumbleAPI::getAllUsers_v_1_0_x(mumble_plugin_id_t callerID, mumble_connection_t connection, + mumble_userid_t **users, size_t *userCount, api_promise_t *promise) { + if (QThread::currentThread() != thread()) { + // Invoke in main thread + QMetaObject::invokeMethod(this, "getAllUsers_v_1_0_x", Qt::QueuedConnection, + Q_ARG(mumble_plugin_id_t, callerID), Q_ARG(mumble_connection_t, connection), + Q_ARG(mumble_userid_t **, users), Q_ARG(size_t *, userCount), + Q_ARG(api_promise_t *, promise)); + + return; + } + + VERIFY_PLUGIN_ID(callerID); + + VERIFY_CONNECTION(connection); + ENSURE_CONNECTION_SYNCHRONIZED(connection); + + QReadLocker userLock(&ClientUser::c_qrwlUsers); + + size_t amount = ClientUser::c_qmUsers.size(); + + auto it = ClientUser::c_qmUsers.constBegin(); + + mumble_userid_t *userIDs = reinterpret_cast< mumble_userid_t * >(malloc(sizeof(mumble_userid_t) * amount)); + + unsigned int index = 0; + while (it != ClientUser::c_qmUsers.constEnd()) { + userIDs[index] = it.key(); + + it++; + index++; + } + + m_curator.m_entries.insert({ userIDs, { defaultDeleter, callerID, "getAllUsers" } }); + + *users = userIDs; + *userCount = amount; + + EXIT_WITH(STATUS_OK); +} + +void MumbleAPI::getAllChannels_v_1_0_x(mumble_plugin_id_t callerID, mumble_connection_t connection, + mumble_channelid_t **channels, size_t *channelCount, api_promise_t *promise) { + if (QThread::currentThread() != thread()) { + // Invoke in main thread + QMetaObject::invokeMethod(this, "getAllChannels_v_1_0_x", Qt::QueuedConnection, + Q_ARG(mumble_plugin_id_t, callerID), Q_ARG(mumble_connection_t, connection), + Q_ARG(mumble_channelid_t **, channels), Q_ARG(size_t *, channelCount), + Q_ARG(api_promise_t *, promise)); + + return; + } + + VERIFY_PLUGIN_ID(callerID); + + VERIFY_CONNECTION(connection); + ENSURE_CONNECTION_SYNCHRONIZED(connection); + + QReadLocker channelLock(&Channel::c_qrwlChannels); + + size_t amount = Channel::c_qhChannels.size(); + + auto it = Channel::c_qhChannels.constBegin(); + + mumble_channelid_t *channelIDs = + reinterpret_cast< mumble_channelid_t * >(malloc(sizeof(mumble_channelid_t) * amount)); + + unsigned int index = 0; + while (it != Channel::c_qhChannels.constEnd()) { + channelIDs[index] = it.key(); + + it++; + index++; + } + + m_curator.m_entries.insert({ channelIDs, { defaultDeleter, callerID, "getAllChannels" } }); + + *channels = channelIDs; + *channelCount = amount; + + EXIT_WITH(STATUS_OK); +} + +void MumbleAPI::getChannelOfUser_v_1_0_x(mumble_plugin_id_t callerID, mumble_connection_t connection, + mumble_userid_t userID, mumble_channelid_t *channelID, + api_promise_t *promise) { + if (QThread::currentThread() != thread()) { + // Invoke in main thread + QMetaObject::invokeMethod(this, "getChannelOfUser_v_1_0_x", Qt::QueuedConnection, + Q_ARG(mumble_plugin_id_t, callerID), Q_ARG(mumble_connection_t, connection), + Q_ARG(mumble_userid_t, userID), Q_ARG(mumble_channelid_t *, channelID), + Q_ARG(api_promise_t *, promise)); + + return; + } + + VERIFY_PLUGIN_ID(callerID); + + VERIFY_CONNECTION(connection); + ENSURE_CONNECTION_SYNCHRONIZED(connection); + + const ClientUser *user = ClientUser::get(userID); + + if (!user) { + EXIT_WITH(EC_USER_NOT_FOUND); + } + + if (user->cChannel) { + *channelID = user->cChannel->iId; + + EXIT_WITH(STATUS_OK); + } else { + EXIT_WITH(EC_GENERIC_ERROR); + } +} + +void MumbleAPI::getUsersInChannel_v_1_0_x(mumble_plugin_id_t callerID, mumble_connection_t connection, + mumble_channelid_t channelID, mumble_userid_t **users, size_t *userCount, + api_promise_t *promise) { + if (QThread::currentThread() != thread()) { + // Invoke in main thread + QMetaObject::invokeMethod(this, "getUsersInChannel_v_1_0_x", Qt::QueuedConnection, + Q_ARG(mumble_plugin_id_t, callerID), Q_ARG(mumble_connection_t, connection), + Q_ARG(mumble_channelid_t, channelID), Q_ARG(mumble_userid_t **, users), + Q_ARG(size_t *, userCount), Q_ARG(api_promise_t *, promise)); + + return; + } + + VERIFY_PLUGIN_ID(callerID); + + VERIFY_CONNECTION(connection); + ENSURE_CONNECTION_SYNCHRONIZED(connection); + + const Channel *channel = Channel::get(channelID); + + if (!channel) { + EXIT_WITH(EC_CHANNEL_NOT_FOUND); + } + + size_t amount = channel->qlUsers.size(); + + mumble_userid_t *userIDs = reinterpret_cast< mumble_userid_t * >(malloc(sizeof(mumble_userid_t) * amount)); + + int index = 0; + foreach (const User *currentUser, channel->qlUsers) { + userIDs[index] = currentUser->uiSession; + + index++; + } + + m_curator.m_entries.insert({ userIDs, { defaultDeleter, callerID, "getUsersInChannel" } }); + + *users = userIDs; + *userCount = amount; + + EXIT_WITH(STATUS_OK); +} + +void MumbleAPI::getLocalUserTransmissionMode_v_1_0_x(mumble_plugin_id_t callerID, + mumble_transmission_mode_t *transmissionMode, + api_promise_t *promise) { + if (QThread::currentThread() != thread()) { + // Invoke in main thread + QMetaObject::invokeMethod( + this, "getLocalUserTransmissionMode_v_1_0_x", Qt::QueuedConnection, Q_ARG(mumble_plugin_id_t, callerID), + Q_ARG(mumble_transmission_mode_t *, transmissionMode), Q_ARG(api_promise_t *, promise)); + + return; + } + + VERIFY_PLUGIN_ID(callerID); + + switch (Global::get().s.atTransmit) { + case Settings::AudioTransmit::Continuous: + *transmissionMode = TM_CONTINOUS; + EXIT_WITH(STATUS_OK); + case Settings::AudioTransmit::VAD: + *transmissionMode = TM_VOICE_ACTIVATION; + EXIT_WITH(STATUS_OK); + case Settings::AudioTransmit::PushToTalk: + *transmissionMode = TM_PUSH_TO_TALK; + EXIT_WITH(STATUS_OK); + } + + // Unable to resolve transmission mode + EXIT_WITH(EC_GENERIC_ERROR); +} + +void MumbleAPI::isUserLocallyMuted_v_1_0_x(mumble_plugin_id_t callerID, mumble_connection_t connection, + mumble_userid_t userID, bool *muted, api_promise_t *promise) { + if (QThread::currentThread() != thread()) { + // Invoke in main thread + QMetaObject::invokeMethod(this, "isUserLocallyMuted_v_1_0_x", Qt::QueuedConnection, + Q_ARG(mumble_plugin_id_t, callerID), Q_ARG(mumble_connection_t, connection), + Q_ARG(mumble_userid_t, userID), Q_ARG(bool *, muted), + Q_ARG(api_promise_t *, promise)); + + return; + } + + VERIFY_PLUGIN_ID(callerID); + + VERIFY_CONNECTION(connection); + ENSURE_CONNECTION_SYNCHRONIZED(connection); + + const ClientUser *user = ClientUser::get(userID); + + if (!user) { + EXIT_WITH(EC_USER_NOT_FOUND); + } + + *muted = user->bLocalMute; + + EXIT_WITH(STATUS_OK); +} + +void MumbleAPI::isLocalUserMuted_v_1_0_x(mumble_plugin_id_t callerID, bool *muted, api_promise_t *promise) { + if (QThread::currentThread() != thread()) { + // Invoke in main thread + QMetaObject::invokeMethod(this, "isLocalUserMuted_v_1_0_x", Qt::QueuedConnection, + Q_ARG(mumble_plugin_id_t, callerID), Q_ARG(bool *, muted), + Q_ARG(api_promise_t *, promise)); + + return; + } + + VERIFY_PLUGIN_ID(callerID); + + *muted = Global::get().s.bMute; + + EXIT_WITH(STATUS_OK); +} + +void MumbleAPI::isLocalUserDeafened_v_1_0_x(mumble_plugin_id_t callerID, bool *deafened, api_promise_t *promise) { + if (QThread::currentThread() != thread()) { + // Invoke in main thread + QMetaObject::invokeMethod(this, "isLocalUserDeafened_v_1_0_x", Qt::QueuedConnection, + Q_ARG(mumble_plugin_id_t, callerID), Q_ARG(bool *, deafened), + Q_ARG(api_promise_t *, promise)); + + return; + } + + VERIFY_PLUGIN_ID(callerID); + + *deafened = Global::get().s.bDeaf; + + EXIT_WITH(STATUS_OK); +} + +void MumbleAPI::getUserHash_v_1_0_x(mumble_plugin_id_t callerID, mumble_connection_t connection, mumble_userid_t userID, + const char **hash, api_promise_t *promise) { + if (QThread::currentThread() != thread()) { + // Invoke in main thread + QMetaObject::invokeMethod(this, "getUserHash_v_1_0_x", Qt::QueuedConnection, + Q_ARG(mumble_plugin_id_t, callerID), Q_ARG(mumble_connection_t, connection), + Q_ARG(mumble_userid_t, userID), Q_ARG(const char **, hash), + Q_ARG(api_promise_t *, promise)); + + return; + } + + VERIFY_PLUGIN_ID(callerID); + + VERIFY_CONNECTION(connection); + ENSURE_CONNECTION_SYNCHRONIZED(connection); + + const ClientUser *user = ClientUser::get(userID); + + if (!user) { + EXIT_WITH(EC_USER_NOT_FOUND); + } + + // The user's hash is already in hexadecimal representation, so we don't have to worry about null-bytes in it + // +1 for NULL terminator + size_t size = user->qsHash.toUtf8().size() + 1; + + char *hashArray = reinterpret_cast< char * >(malloc(size * sizeof(char))); + + std::strcpy(hashArray, user->qsHash.toUtf8().data()); + + m_curator.m_entries.insert({ hashArray, { defaultDeleter, callerID, "getUserHash" } }); + + *hash = hashArray; + + EXIT_WITH(STATUS_OK); +} + +void MumbleAPI::getServerHash_v_1_0_x(mumble_plugin_id_t callerID, mumble_connection_t connection, const char **hash, + api_promise_t *promise) { + if (QThread::currentThread() != thread()) { + // Invoke in main thread + QMetaObject::invokeMethod(this, "getServerHash_v_1_0_x", Qt::QueuedConnection, + Q_ARG(mumble_plugin_id_t, callerID), Q_ARG(mumble_connection_t, connection), + Q_ARG(const char **, hash), Q_ARG(api_promise_t *, promise)); + + return; + } + + VERIFY_PLUGIN_ID(callerID); + + VERIFY_CONNECTION(connection); + ENSURE_CONNECTION_SYNCHRONIZED(connection); + + // Use hexadecimal representation in order for the String to be properly printable and for it to be C-encodable + QByteArray hashHex = Global::get().sh->qbaDigest.toHex(); + QString strHash = QString::fromLatin1(hashHex); + + // +1 for NULL terminator + size_t size = strHash.toUtf8().size() + 1; + + char *hashArray = reinterpret_cast< char * >(malloc(size * sizeof(char))); + + std::strcpy(hashArray, strHash.toUtf8().data()); + + m_curator.m_entries.insert({ hashArray, { defaultDeleter, callerID, "getServerHash" } }); + + *hash = hashArray; + + EXIT_WITH(STATUS_OK); +} + +void MumbleAPI::requestLocalUserTransmissionMode_v_1_0_x(mumble_plugin_id_t callerID, + mumble_transmission_mode_t transmissionMode, + api_promise_t *promise) { + if (QThread::currentThread() != thread()) { + // Invoke in main thread + QMetaObject::invokeMethod(this, "requestLocalUserTransmissionMode_v_1_0_x", Qt::QueuedConnection, + Q_ARG(mumble_plugin_id_t, callerID), + Q_ARG(mumble_transmission_mode_t, transmissionMode), Q_ARG(api_promise_t *, promise)); + + return; + } + + VERIFY_PLUGIN_ID(callerID); + + switch (transmissionMode) { + case TM_CONTINOUS: + Global::get().s.atTransmit = Settings::AudioTransmit::Continuous; + EXIT_WITH(STATUS_OK); + case TM_VOICE_ACTIVATION: + Global::get().s.atTransmit = Settings::AudioTransmit::VAD; + EXIT_WITH(STATUS_OK); + case TM_PUSH_TO_TALK: + Global::get().s.atTransmit = Settings::AudioTransmit::PushToTalk; + EXIT_WITH(STATUS_OK); + } + + EXIT_WITH(EC_UNKNOWN_TRANSMISSION_MODE); +} + +void MumbleAPI::getUserComment_v_1_0_x(mumble_plugin_id_t callerID, mumble_connection_t connection, + mumble_userid_t userID, const char **comment, api_promise_t *promise) { + if (QThread::currentThread() != thread()) { + // Invoke in main thread + QMetaObject::invokeMethod(this, "getUserComment_v_1_0_x", Qt::QueuedConnection, + Q_ARG(mumble_plugin_id_t, callerID), Q_ARG(mumble_connection_t, connection), + Q_ARG(mumble_userid_t, userID), Q_ARG(const char **, comment), + Q_ARG(api_promise_t *, promise)); + + return; + } + + VERIFY_PLUGIN_ID(callerID); + + VERIFY_CONNECTION(connection); + ENSURE_CONNECTION_SYNCHRONIZED(connection); + + ClientUser *user = ClientUser::get(userID); + + if (!user) { + EXIT_WITH(EC_USER_NOT_FOUND); + } + + if (user->qsComment.isEmpty() && !user->qbaCommentHash.isEmpty()) { + user->qsComment = QString::fromUtf8(Global::get().db->blob(user->qbaCommentHash)); + + if (user->qsComment.isEmpty()) { + // The user's comment hasn't been synchronized to this client yet + EXIT_WITH(EC_UNSYNCHRONIZED_BLOB); + } + } + + // +1 for NULL terminator + size_t size = user->qsComment.toUtf8().size() + 1; + + char *nameArray = reinterpret_cast< char * >(malloc(size * sizeof(char))); + + std::strcpy(nameArray, user->qsComment.toUtf8().data()); + + m_curator.m_entries.insert({ nameArray, { defaultDeleter, callerID, "getUserComment" } }); + + *comment = nameArray; + + EXIT_WITH(STATUS_OK); +} + +void MumbleAPI::getChannelDescription_v_1_0_x(mumble_plugin_id_t callerID, mumble_connection_t connection, + mumble_channelid_t channelID, const char **description, + api_promise_t *promise) { + if (QThread::currentThread() != thread()) { + // Invoke in main thread + QMetaObject::invokeMethod(this, "getChannelDescription_v_1_0_x", Qt::QueuedConnection, + Q_ARG(mumble_plugin_id_t, callerID), Q_ARG(mumble_connection_t, connection), + Q_ARG(mumble_channelid_t, channelID), Q_ARG(const char **, description), + Q_ARG(api_promise_t *, promise)); + + return; + } + + VERIFY_PLUGIN_ID(callerID); + + VERIFY_CONNECTION(connection); + ENSURE_CONNECTION_SYNCHRONIZED(connection); + + Channel *channel = Channel::get(channelID); + + if (!channel) { + EXIT_WITH(EC_CHANNEL_NOT_FOUND); + } + + if (channel->qsDesc.isEmpty() && !channel->qbaDescHash.isEmpty()) { + channel->qsDesc = QString::fromUtf8(Global::get().db->blob(channel->qbaDescHash)); + + if (channel->qsDesc.isEmpty()) { + // The channel's description hasn't been synchronized to this client yet + EXIT_WITH(EC_UNSYNCHRONIZED_BLOB); + } + } + + // +1 for NULL terminator + size_t size = channel->qsDesc.toUtf8().size() + 1; + + char *nameArray = reinterpret_cast< char * >(malloc(size * sizeof(char))); + + std::strcpy(nameArray, channel->qsDesc.toUtf8().data()); + + m_curator.m_entries.insert({ nameArray, { defaultDeleter, callerID, "getChannelDescription" } }); + + *description = nameArray; + + EXIT_WITH(STATUS_OK); +} + +void MumbleAPI::requestUserMove_v_1_0_x(mumble_plugin_id_t callerID, mumble_connection_t connection, + mumble_userid_t userID, mumble_channelid_t channelID, const char *password, + api_promise_t *promise) { + if (QThread::currentThread() != thread()) { + // Invoke in main thread + QMetaObject::invokeMethod(this, "requestUserMove_v_1_0_x", Qt::QueuedConnection, + Q_ARG(mumble_plugin_id_t, callerID), Q_ARG(mumble_connection_t, connection), + Q_ARG(mumble_userid_t, userID), Q_ARG(mumble_channelid_t, channelID), + Q_ARG(const char *, password), Q_ARG(api_promise_t *, promise)); + + return; + } + + VERIFY_PLUGIN_ID(callerID); + + VERIFY_CONNECTION(connection); + ENSURE_CONNECTION_SYNCHRONIZED(connection); + + const ClientUser *user = ClientUser::get(userID); + + if (!user) { + EXIT_WITH(EC_USER_NOT_FOUND); + } + + const Channel *channel = Channel::get(channelID); + + if (!channel) { + EXIT_WITH(EC_CHANNEL_NOT_FOUND); + } + + if (channel != user->cChannel) { + // send move-request to the server only if the user is not in the channel already + QStringList passwordList; + if (password) { + passwordList << QString::fromUtf8(password); + } + + Global::get().sh->joinChannel(user->uiSession, channel->iId, passwordList); + } + + EXIT_WITH(STATUS_OK); +} + +void MumbleAPI::requestMicrophoneActivationOverwrite_v_1_0_x(mumble_plugin_id_t callerID, bool activate, + api_promise_t *promise) { + if (QThread::currentThread() != thread()) { + // Invoke in main thread + QMetaObject::invokeMethod(this, "requestMicrophoneActivationOverwrite_v_1_0_x", Qt::QueuedConnection, + Q_ARG(mumble_plugin_id_t, callerID), Q_ARG(bool, activate), + Q_ARG(api_promise_t *, promise)); + + return; + } + + VERIFY_PLUGIN_ID(callerID); + + PluginData::get().overwriteMicrophoneActivation.store(activate); + + EXIT_WITH(STATUS_OK); +} + +void MumbleAPI::requestLocalMute_v_1_0_x(mumble_plugin_id_t callerID, mumble_connection_t connection, + mumble_userid_t userID, bool muted, api_promise_t *promise) { + if (QThread::currentThread() != thread()) { + // Invoke in main thread + QMetaObject::invokeMethod(this, "requestLocalMute_v_1_0_x", Qt::QueuedConnection, + Q_ARG(mumble_plugin_id_t, callerID), Q_ARG(mumble_connection_t, connection), + Q_ARG(mumble_userid_t, userID), Q_ARG(bool, muted), Q_ARG(api_promise_t *, promise)); + + return; + } + + VERIFY_PLUGIN_ID(callerID); + + VERIFY_CONNECTION(connection); + ENSURE_CONNECTION_SYNCHRONIZED(connection); + + if (userID == Global::get().uiSession) { + // Can't locally mute the local user + EXIT_WITH(EC_INVALID_MUTE_TARGET); + } + + ClientUser *user = ClientUser::get(userID); + + if (!user) { + EXIT_WITH(EC_USER_NOT_FOUND); + } + + user->setLocalMute(muted); + + EXIT_WITH(STATUS_OK); +} + +void MumbleAPI::requestLocalUserMute_v_1_0_x(mumble_plugin_id_t callerID, bool muted, api_promise_t *promise) { + if (QThread::currentThread() != thread()) { + // Invoke in main thread + QMetaObject::invokeMethod(this, "requestLocalUserMute_v_1_0_x", Qt::QueuedConnection, + Q_ARG(mumble_plugin_id_t, callerID), Q_ARG(bool, muted), + Q_ARG(api_promise_t *, promise)); + + return; + } + + VERIFY_PLUGIN_ID(callerID); + + if (!Global::get().mw) { + EXIT_WITH(EC_INTERNAL_ERROR); + } + + Global::get().mw->setAudioMute(muted); + + EXIT_WITH(STATUS_OK); +} + +void MumbleAPI::requestLocalUserDeaf_v_1_0_x(mumble_plugin_id_t callerID, bool deafened, api_promise_t *promise) { + if (QThread::currentThread() != thread()) { + // Invoke in main thread + QMetaObject::invokeMethod(this, "requestLocalUserDeaf_v_1_0_x", Qt::QueuedConnection, + Q_ARG(mumble_plugin_id_t, callerID), Q_ARG(bool, deafened), + Q_ARG(api_promise_t *, promise)); + + return; + } + + VERIFY_PLUGIN_ID(callerID); + + if (!Global::get().mw) { + EXIT_WITH(EC_INTERNAL_ERROR); + } + + Global::get().mw->setAudioDeaf(deafened); + + EXIT_WITH(STATUS_OK); +} + +void MumbleAPI::requestSetLocalUserComment_v_1_0_x(mumble_plugin_id_t callerID, mumble_connection_t connection, + const char *comment, api_promise_t *promise) { + if (QThread::currentThread() != thread()) { + // Invoke in main thread + QMetaObject::invokeMethod(this, "requestSetLocalUserComment_v_1_0_x", Qt::QueuedConnection, + Q_ARG(mumble_plugin_id_t, callerID), Q_ARG(mumble_connection_t, connection), + Q_ARG(const char *, comment), Q_ARG(api_promise_t *, promise)); + + return; + } + + VERIFY_PLUGIN_ID(callerID); + + VERIFY_CONNECTION(connection); + ENSURE_CONNECTION_SYNCHRONIZED(connection); + + ClientUser *localUser = ClientUser::get(Global::get().uiSession); + + if (!localUser) { + EXIT_WITH(EC_USER_NOT_FOUND); + } + + if (!Global::get().mw || !Global::get().mw->pmModel) { + EXIT_WITH(EC_INTERNAL_ERROR); + } + + Global::get().mw->pmModel->setComment(localUser, QString::fromUtf8(comment)); + + EXIT_WITH(STATUS_OK); +} + +void MumbleAPI::findUserByName_v_1_0_x(mumble_plugin_id_t callerID, mumble_connection_t connection, + const char *userName, mumble_userid_t *userID, api_promise_t *promise) { + if (QThread::currentThread() != thread()) { + // Invoke in main thread + QMetaObject::invokeMethod(this, "findUserByName_v_1_0_x", Qt::QueuedConnection, + Q_ARG(mumble_plugin_id_t, callerID), Q_ARG(mumble_connection_t, connection), + Q_ARG(const char *, userName), Q_ARG(mumble_userid_t *, userID), + Q_ARG(api_promise_t *, promise)); + + return; + } + + VERIFY_PLUGIN_ID(callerID); + + VERIFY_CONNECTION(connection); + ENSURE_CONNECTION_SYNCHRONIZED(connection); + + const QString qsUserName = QString::fromUtf8(userName); + + QReadLocker userLock(&ClientUser::c_qrwlUsers); + + auto it = ClientUser::c_qmUsers.constBegin(); + while (it != ClientUser::c_qmUsers.constEnd()) { + if (it.value()->qsName == qsUserName) { + *userID = it.key(); + + EXIT_WITH(STATUS_OK); + } + + it++; + } + + EXIT_WITH(EC_USER_NOT_FOUND); +} + +void MumbleAPI::findChannelByName_v_1_0_x(mumble_plugin_id_t callerID, mumble_connection_t connection, + const char *channelName, mumble_channelid_t *channelID, + api_promise_t *promise) { + if (QThread::currentThread() != thread()) { + // Invoke in main thread + QMetaObject::invokeMethod(this, "findChannelByName_v_1_0_x", Qt::QueuedConnection, + Q_ARG(mumble_plugin_id_t, callerID), Q_ARG(mumble_connection_t, connection), + Q_ARG(const char *, channelName), Q_ARG(mumble_channelid_t *, channelID), + Q_ARG(api_promise_t *, promise)); + + return; + } + + VERIFY_PLUGIN_ID(callerID); + + VERIFY_CONNECTION(connection); + ENSURE_CONNECTION_SYNCHRONIZED(connection); + + const QString qsChannelName = QString::fromUtf8(channelName); + + QReadLocker channelLock(&Channel::c_qrwlChannels); + + auto it = Channel::c_qhChannels.constBegin(); + while (it != Channel::c_qhChannels.constEnd()) { + if (it.value()->qsName == qsChannelName) { + *channelID = it.key(); + + EXIT_WITH(STATUS_OK); + } + + it++; + } + + EXIT_WITH(EC_CHANNEL_NOT_FOUND); +} + +QVariant getMumbleSettingHelper(mumble_settings_key_t key) { + QVariant value; + + // All values are explicitly cast to the target type of their associated API. For instance there is not API to + // get float values but there is one for doubles. Therefore floats have to be cast to doubles in order for the + // type checking to work out. + switch (key) { + case MSK_AUDIO_INPUT_VOICE_HOLD: + value = static_cast< int >(Global::get().s.iVoiceHold); + break; + case MSK_AUDIO_INPUT_VAD_SILENCE_THRESHOLD: + value = static_cast< double >(Global::get().s.fVADmin); + break; + case MSK_AUDIO_INPUT_VAD_SPEECH_THRESHOLD: + value = static_cast< double >(Global::get().s.fVADmax); + break; + case MSK_AUDIO_OUTPUT_PA_MINIMUM_DISTANCE: + value = static_cast< double >(Global::get().s.fAudioMinDistance); + break; + case MSK_AUDIO_OUTPUT_PA_MAXIMUM_DISTANCE: + value = static_cast< double >(Global::get().s.fAudioMaxDistance); + break; + case MSK_AUDIO_OUTPUT_PA_BLOOM: + value = static_cast< double >(Global::get().s.fAudioBloom); + break; + case MSK_AUDIO_OUTPUT_PA_MINIMUM_VOLUME: + value = static_cast< double >(Global::get().s.fAudioMaxDistVolume); + break; + case MSK_INVALID: + // There is no setting associated with this key + break; + } + + return value; +} + +// IS_TYPE actually only checks if the QVariant can be converted to the needed type since that's all that we really care +// about at the end of the day. +#define IS_TYPE(var, varType) static_cast< QMetaType::Type >(var.type()) == varType +#define IS_NOT_TYPE(var, varType) static_cast< QMetaType::Type >(var.type()) != varType + +void MumbleAPI::getMumbleSetting_bool_v_1_0_x(mumble_plugin_id_t callerID, mumble_settings_key_t key, bool *outValue, + api_promise_t *promise) { + if (QThread::currentThread() != thread()) { + // Invoke in main thread + QMetaObject::invokeMethod(this, "getMumbleSetting_bool_v_1_0_x", Qt::QueuedConnection, + Q_ARG(mumble_plugin_id_t, callerID), Q_ARG(mumble_settings_key_t, key), + Q_ARG(bool *, outValue), Q_ARG(api_promise_t *, promise)); + + return; + } + + VERIFY_PLUGIN_ID(callerID); + + QVariant value = getMumbleSettingHelper(key); + + if (!value.isValid()) { + // We also return that for MSK_INVALID + EXIT_WITH(EC_UNKNOWN_SETTINGS_KEY); + } + + if (IS_NOT_TYPE(value, QMetaType::Bool)) { + EXIT_WITH(EC_WRONG_SETTINGS_TYPE); + } + + *outValue = value.toBool(); + + EXIT_WITH(STATUS_OK); +} + +void MumbleAPI::getMumbleSetting_int_v_1_0_x(mumble_plugin_id_t callerID, mumble_settings_key_t key, int64_t *outValue, + api_promise_t *promise) { + if (QThread::currentThread() != thread()) { + // Invoke in main thread + QMetaObject::invokeMethod(this, "getMumbleSetting_int_v_1_0_x", Qt::QueuedConnection, + Q_ARG(mumble_plugin_id_t, callerID), Q_ARG(mumble_settings_key_t, key), + Q_ARG(int64_t *, outValue), Q_ARG(api_promise_t *, promise)); + + return; + } + + VERIFY_PLUGIN_ID(callerID); + + QVariant value = getMumbleSettingHelper(key); + + if (!value.isValid()) { + // We also return that for MSK_INVALID + EXIT_WITH(EC_UNKNOWN_SETTINGS_KEY); + } + + if (IS_NOT_TYPE(value, QMetaType::Int)) { + EXIT_WITH(EC_WRONG_SETTINGS_TYPE); + } + + *outValue = value.toInt(); + + EXIT_WITH(STATUS_OK); +} + +void MumbleAPI::getMumbleSetting_double_v_1_0_x(mumble_plugin_id_t callerID, mumble_settings_key_t key, + double *outValue, api_promise_t *promise) { + if (QThread::currentThread() != thread()) { + // Invoke in main thread + QMetaObject::invokeMethod(this, "getMumbleSetting_double_v_1_0_x", Qt::QueuedConnection, + Q_ARG(mumble_plugin_id_t, callerID), Q_ARG(mumble_settings_key_t, key), + Q_ARG(double *, outValue), Q_ARG(api_promise_t *, promise)); + + return; + } + + VERIFY_PLUGIN_ID(callerID); + + QVariant value = getMumbleSettingHelper(key); + + if (!value.isValid()) { + // We also return that for MSK_INVALID + EXIT_WITH(EC_UNKNOWN_SETTINGS_KEY); + } + + if (IS_NOT_TYPE(value, QMetaType::Double)) { + EXIT_WITH(EC_WRONG_SETTINGS_TYPE); + } + + *outValue = value.toDouble(); + + EXIT_WITH(STATUS_OK); +} + +void MumbleAPI::getMumbleSetting_string_v_1_0_x(mumble_plugin_id_t callerID, mumble_settings_key_t key, + const char **outValue, api_promise_t *promise) { + if (QThread::currentThread() != thread()) { + // Invoke in main thread + QMetaObject::invokeMethod(this, "getMumbleSetting_string_v_1_0_x", Qt::QueuedConnection, + Q_ARG(mumble_plugin_id_t, callerID), Q_ARG(mumble_settings_key_t, key), + Q_ARG(const char **, outValue), Q_ARG(api_promise_t *, promise)); + + return; + } + + VERIFY_PLUGIN_ID(callerID); + + QVariant value = getMumbleSettingHelper(key); + + if (!value.isValid()) { + // We also return that for MSK_INVALID + EXIT_WITH(EC_UNKNOWN_SETTINGS_KEY); + } + + if (IS_NOT_TYPE(value, QMetaType::QString)) { + EXIT_WITH(EC_WRONG_SETTINGS_TYPE); + } + + const QString stringValue = value.toString(); + + // +1 for NULL terminator + size_t size = stringValue.toUtf8().size() + 1; + + char *valueArray = reinterpret_cast< char * >(malloc(size * sizeof(char))); + + std::strcpy(valueArray, stringValue.toUtf8().data()); + + m_curator.m_entries.insert({ valueArray, { defaultDeleter, callerID, "getMumbleSetting_string" } }); + + *outValue = valueArray; + + EXIT_WITH(STATUS_OK); +} + +mumble_error_t setMumbleSettingHelper(mumble_settings_key_t key, QVariant value) { + switch (key) { + case MSK_AUDIO_INPUT_VOICE_HOLD: + if (IS_TYPE(value, QMetaType::Int)) { + Global::get().s.iVoiceHold = value.toInt(); + + return STATUS_OK; + } else { + return EC_WRONG_SETTINGS_TYPE; + } + case MSK_AUDIO_INPUT_VAD_SILENCE_THRESHOLD: + if (IS_TYPE(value, QMetaType::Double)) { + Global::get().s.fVADmin = static_cast< float >(value.toDouble()); + + return STATUS_OK; + } else { + return EC_WRONG_SETTINGS_TYPE; + } + case MSK_AUDIO_INPUT_VAD_SPEECH_THRESHOLD: + if (IS_TYPE(value, QMetaType::Double)) { + Global::get().s.fVADmax = static_cast< float >(value.toDouble()); + + return STATUS_OK; + } else { + return EC_WRONG_SETTINGS_TYPE; + } + case MSK_AUDIO_OUTPUT_PA_MINIMUM_DISTANCE: + if (IS_TYPE(value, QMetaType::Double)) { + Global::get().s.fAudioMinDistance = static_cast< float >(value.toDouble()); + + return STATUS_OK; + } else { + return EC_WRONG_SETTINGS_TYPE; + } + case MSK_AUDIO_OUTPUT_PA_MAXIMUM_DISTANCE: + if (IS_TYPE(value, QMetaType::Double)) { + Global::get().s.fAudioMaxDistance = static_cast< float >(value.toDouble()); + + return STATUS_OK; + } else { + return EC_WRONG_SETTINGS_TYPE; + } + case MSK_AUDIO_OUTPUT_PA_BLOOM: + if (IS_TYPE(value, QMetaType::Double)) { + Global::get().s.fAudioBloom = static_cast< float >(value.toDouble()); + + return STATUS_OK; + } else { + return EC_WRONG_SETTINGS_TYPE; + } + case MSK_AUDIO_OUTPUT_PA_MINIMUM_VOLUME: + if (IS_TYPE(value, QMetaType::Double)) { + Global::get().s.fAudioMaxDistVolume = static_cast< float >(value.toDouble()); + + return STATUS_OK; + } else { + return EC_WRONG_SETTINGS_TYPE; + } + case MSK_INVALID: + // Do nothing + break; + } + + return EC_UNKNOWN_SETTINGS_KEY; +} + +void MumbleAPI::setMumbleSetting_bool_v_1_0_x(mumble_plugin_id_t callerID, mumble_settings_key_t key, bool value, + api_promise_t *promise) { + if (QThread::currentThread() != thread()) { + // Invoke in main thread + QMetaObject::invokeMethod(this, "setMumbleSetting_bool_v_1_0_x", Qt::QueuedConnection, + Q_ARG(mumble_plugin_id_t, callerID), Q_ARG(mumble_settings_key_t, key), + Q_ARG(bool, value), Q_ARG(api_promise_t *, promise)); + + return; + } + + VERIFY_PLUGIN_ID(callerID); + + mumble_error_t exitCode = setMumbleSettingHelper(key, value); + EXIT_WITH(exitCode); +} + +void MumbleAPI::setMumbleSetting_int_v_1_0_x(mumble_plugin_id_t callerID, mumble_settings_key_t key, int64_t value, + api_promise_t *promise) { + if (QThread::currentThread() != thread()) { + // Invoke in main thread + QMetaObject::invokeMethod(this, "setMumbleSetting_int_v_1_0_x", Qt::QueuedConnection, + Q_ARG(mumble_plugin_id_t, callerID), Q_ARG(mumble_settings_key_t, key), + Q_ARG(int64_t, value), Q_ARG(api_promise_t *, promise)); + + return; + } + + VERIFY_PLUGIN_ID(callerID); + + mumble_error_t exitCode = setMumbleSettingHelper(key, QVariant::fromValue(value)); + EXIT_WITH(exitCode); +} + +void MumbleAPI::setMumbleSetting_double_v_1_0_x(mumble_plugin_id_t callerID, mumble_settings_key_t key, double value, + api_promise_t *promise) { + if (QThread::currentThread() != thread()) { + // Invoke in main thread + QMetaObject::invokeMethod(this, "setMumbleSetting_double_v_1_0_x", Qt::QueuedConnection, + Q_ARG(mumble_plugin_id_t, callerID), Q_ARG(mumble_settings_key_t, key), + Q_ARG(double, value), Q_ARG(api_promise_t *, promise)); + + return; + } + + VERIFY_PLUGIN_ID(callerID); + + mumble_error_t exitCode = setMumbleSettingHelper(key, value); + EXIT_WITH(exitCode); +} + +void MumbleAPI::setMumbleSetting_string_v_1_0_x(mumble_plugin_id_t callerID, mumble_settings_key_t key, + const char *value, api_promise_t *promise) { + if (QThread::currentThread() != thread()) { + // Invoke in main thread + QMetaObject::invokeMethod(this, "setMumbleSetting_string_v_1_0_x", Qt::QueuedConnection, + Q_ARG(mumble_plugin_id_t, callerID), Q_ARG(mumble_settings_key_t, key), + Q_ARG(const char *, value), Q_ARG(api_promise_t *, promise)); + + return; + } + + VERIFY_PLUGIN_ID(callerID); + + mumble_error_t exitCode = setMumbleSettingHelper(key, QString::fromUtf8(value)); + EXIT_WITH(exitCode); +} +#undef IS_TYPE +#undef IS_NOT_TYPE + +void MumbleAPI::sendData_v_1_0_x(mumble_plugin_id_t callerID, mumble_connection_t connection, + const mumble_userid_t *users, size_t userCount, const uint8_t *data, size_t dataLength, + const char *dataID, api_promise_t *promise) { + if (QThread::currentThread() != thread()) { + // Invoke in main thread + QMetaObject::invokeMethod(this, "sendData_v_1_0_x", Qt::QueuedConnection, Q_ARG(mumble_plugin_id_t, callerID), + Q_ARG(mumble_connection_t, connection), Q_ARG(const mumble_userid_t *, users), + Q_ARG(size_t, userCount), Q_ARG(const uint8_t *, data), Q_ARG(size_t, dataLength), + Q_ARG(const char *, dataID), Q_ARG(api_promise_t *, promise)); + + return; + } + + VERIFY_PLUGIN_ID(callerID); + + VERIFY_CONNECTION(connection); + ENSURE_CONNECTION_SYNCHRONIZED(connection); + + if (dataLength > Mumble::Plugins::PluginMessage::MAX_DATA_LENGTH) { + EXIT_WITH(EC_DATA_TOO_BIG); + } + if (std::strlen(dataID) > Mumble::Plugins::PluginMessage::MAX_DATA_ID_LENGTH) { + EXIT_WITH(EC_DATA_ID_TOO_LONG); + } + + MumbleProto::PluginDataTransmission mpdt; + mpdt.set_sendersession(Global::get().uiSession); + + for (size_t i = 0; i < userCount; i++) { + const ClientUser *user = ClientUser::get(users[i]); + + if (user) { + mpdt.add_receiversessions(users[i]); + } else { + EXIT_WITH(EC_USER_NOT_FOUND); + } + } + + mpdt.set_data(data, dataLength); + mpdt.set_dataid(dataID); + + if (Global::get().sh) { + Global::get().sh->sendMessage(mpdt); + + EXIT_WITH(STATUS_OK); + } else { + EXIT_WITH(EC_CONNECTION_NOT_FOUND); + } +} + +void MumbleAPI::log_v_1_0_x(mumble_plugin_id_t callerID, const char *message, api_promise_t *promise) { + if (QThread::currentThread() != thread()) { + // Invoke in main thread + QMetaObject::invokeMethod(this, "log_v_1_0_x", Qt::QueuedConnection, Q_ARG(mumble_plugin_id_t, callerID), + Q_ARG(const char *, message), Q_ARG(api_promise_t *, promise)); + + return; + } + + // We verify the plugin manually as we need a handle to it later + const_plugin_ptr_t plugin = Global::get().pluginManager->getPlugin(callerID); + if (!plugin) { + EXIT_WITH(EC_INVALID_PLUGIN_ID); + } + + QString msg = QString::fromLatin1("<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><html><head/><body><p><span style=" font-weight:600;">Name:</span></p></body></html></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><html><head/><body><p><span style=" font-weight:600;">Version:</span></p></body></html></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><html><head/><body><p><span style=" font-weight:600;">Author(s):</span></p></body></html></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><html><head/><body><p><span style=" font-weight:600;">Description:</span></p></body></html></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>&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>&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 ¤tPath : *additionalSearchPaths) { + m_pluginSearchPaths.insert(currentPath); + } + } + +#ifdef Q_OS_MAC + // Path to plugins inside AppBundle + m_pluginSearchPaths.insert(QString::fromLatin1("%1/../Plugins").arg(qApp->applicationDirPath())); +#endif + +#ifdef MUMBLE_PLUGIN_PATH + // Path to where plugins are/will be installed on the system + m_pluginSearchPaths.insert(QString::fromLatin1(MUMTEXT(MUMBLE_PLUGIN_PATH))); +#endif + + // Path to "plugins" dir right next to the executable's location. This is the case for when Mumble + // is run after compilation without having installed it anywhere special + m_pluginSearchPaths.insert(QString::fromLatin1("%1/plugins").arg(MumbleApplication::instance()->applicationVersionRootPath())); + + // Path to where the plugin installer will write plugins + m_pluginSearchPaths.insert(PluginInstaller::getInstallDir()); + +#ifdef Q_OS_WIN + // According to MS KB Q131065, we need this to OpenProcess() + + m_hToken = nullptr; + + if (!OpenThreadToken(GetCurrentThread(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, FALSE, &m_hToken)) { + if (GetLastError() == ERROR_NO_TOKEN) { + ImpersonateSelf(SecurityImpersonation); + OpenThreadToken(GetCurrentThread(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, FALSE, &m_hToken); + } + } + + TOKEN_PRIVILEGES tp; + LUID luid; + m_cbPrevious=sizeof(TOKEN_PRIVILEGES); + + LookupPrivilegeValue(NULL, SE_DEBUG_NAME, &luid); + + tp.PrivilegeCount = 1; + tp.Privileges[0].Luid = luid; + tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED; + + AdjustTokenPrivileges(m_hToken, FALSE, &tp, sizeof(TOKEN_PRIVILEGES), &m_tpPrevious, &m_cbPrevious); +#endif + + // Synchronize the positional data in a regular interval + // By making this the parent of the created timer, we don't have to delete it explicitly + QTimer *serverSyncTimer = new QTimer(this); + QObject::connect(serverSyncTimer, &QTimer::timeout, this, &PluginManager::on_syncPositionalData); + serverSyncTimer->start(POSITIONAL_SERVER_SYNC_INTERVAL); + + // Install this manager as a global eventFilter in order to get notified about all keypresses + if (QCoreApplication::instance()) { + QCoreApplication::instance()->installEventFilter(this); + } + + // Set up the timer for regularly checking for available positional data plugins + m_positionalDataCheckTimer.setInterval(POSITIONAL_DATA_CHECK_INTERVAL); + m_positionalDataCheckTimer.start(); + QObject::connect(&m_positionalDataCheckTimer, &QTimer::timeout, this, &PluginManager::checkForAvailablePositionalDataPlugin); + + QObject::connect(&m_updater, &PluginUpdater::updatesAvailable, this, &PluginManager::on_updatesAvailable); + QObject::connect(this, &PluginManager::keyEvent, this, &PluginManager::on_keyEvent); +} + +PluginManager::~PluginManager() { + clearPlugins(); + +#ifdef Q_OS_WIN + AdjustTokenPrivileges(m_hToken, FALSE, &m_tpPrevious, m_cbPrevious, NULL, NULL); + CloseHandle(m_hToken); +#endif +} + +/// Emits a log about a plugin with the given name having lost link (positional audio) +/// +/// @param pluginName The name of the plugin that lost link +void reportLostLink(const QString& pluginName) { + Global::get().l->log(Log::Information, PluginManager::tr("%1 lost link").arg(pluginName.toHtmlEscaped())); +} + +bool PluginManager::eventFilter(QObject *target, QEvent *event) { + if (event->type() == QEvent::KeyPress || event->type() == QEvent::KeyRelease) { + static QVector<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 ¤tPath : 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 ¤tPair = 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 ¤tPair = m_pluginUpdateWidgets[i]; + + qwContent->layout()->removeWidget(currentPair.pluginCheckBox); + qwContent->layout()->removeWidget(currentPair.urlLabel); + + delete currentPair.pluginCheckBox; + delete currentPair.urlLabel; + } +} + +void PluginUpdater::on_selectAll(int checkState) { + // failsafe for partially selected state (shouldn't happen though) + if (checkState == Qt::PartiallyChecked) { + checkState = Qt::Unchecked; + } + + // Select or deselect all plugins + for (int i = 0; i < m_pluginUpdateWidgets.size(); i++) { + UpdateWidgetPair ¤tPair = m_pluginUpdateWidgets[i]; + + currentPair.pluginCheckBox->setCheckState(static_cast<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 ¤tPair = m_pluginUpdateWidgets[i]; + + if (!currentPair.pluginCheckBox->isChecked()) { + // One unchecked checkBox is enough to know that the selectAll + // CheckBox can't be checked, so we can abort at this point + return; + } + } + + qcbSelectAll->setCheckState(Qt::Checked); +} + +void PluginUpdater::on_finished(int result) { + if (result == QDialog::Accepted) { + if (qcbSelectAll->isChecked()) { + // all plugins shall be updated, so we don't have to check them individually + return; + } + + QMutexLocker l(&m_dataMutex); + + // The user wants to update the selected plugins only + // remove the plugins that shouldn't be updated from m_pluginsToUpdate + auto it = m_pluginsToUpdate.begin(); + while (it != m_pluginsToUpdate.end()) { + plugin_id_t id = it->pluginID; + + // find the corresponding checkbox + bool updateCurrent = false; + for (int k = 0; k < m_pluginUpdateWidgets.size(); k++) { + QCheckBox *checkBox = m_pluginUpdateWidgets[k].pluginCheckBox; + QVariant idVariant = checkBox->property("pluginID"); + + if (idVariant.isValid() && static_cast<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/mumble/mumble_ar.ts b/src/mumble/mumble_ar.ts index 71cf51bd8..4fc00bb4e 100644 --- a/src/mumble/mumble_ar.ts +++ b/src/mumble/mumble_ar.ts @@ -3782,6 +3782,10 @@ This field describes the size of an LCD device. The size is given either in pixe <source>User stopped listening to channel</source> <translation>المستخدم توقف عن الاستماع الى القناة</translation> </message> + <message> + <source>Plugin message</source> + <translation type="unfinished"></translation> + </message> </context> <context> <name>LogConfig</name> @@ -6125,12 +6129,13 @@ Valid actions are: <translation type="unfinished"></translation> </message> <message> - <source>Usage: mumble [options] [<url>] + <source>Usage: mumble [options] [<url> | <plugin_list>] <url> specifies a URL to connect to after startup instead of showing the connection window, and has the following form: mumble://[<username>[:<password>]@]<host>[:<port>][/<channel>[/<subchannel>...]][?version=<x.y.z>] +<plugin_list> is a list of plugin files that shall be installed The version query parameter has to be set in order to invoke the correct client version. It currently defaults to 1.2.0. @@ -6450,10 +6455,6 @@ Valid options are: <translation type="unfinished"></translation> </message> <message> - <source>Download plugin and overlay updates on startup</source> - <translation type="unfinished"></translation> - </message> - <message> <source>Privacy</source> <translation type="unfinished"></translation> </message> @@ -6478,6 +6479,14 @@ Prevents the client from sending potentially identifying information about the o <source>Hide public server list</source> <translation type="unfinished"></translation> </message> + <message> + <source>Check for plugin updates on startup</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Automatically download and install plugin updates</source> + <translation type="unfinished"></translation> + </message> </context> <context> <name>Overlay</name> @@ -7028,30 +7037,188 @@ To upgrade these files to their latest versions, click the button below.</source <translation type="unfinished">اسم</translation> </message> <message> - <source>Enabled</source> + <source>Enable</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>PA</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>KeyEvents</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Install a plugin from a local file</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Install plugin...</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unload the currently selected plugin. This will remove it from the plugin list for the current session.</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unload</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>The plugin was installed successfully</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to deactivate all requested features for plugin "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to load plugin "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Whether the positional audio feature of this plugin should be enabled</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>This plugin does not provide support for positional audio</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Whether this plugin should be enabled</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>PluginInstaller</name> + <message> + <source>PluginInstaller</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>You are about to install the plugin listed below. Do you wish to proceed?</source> + <translation type="unfinished"></translation> + </message> + <message> + <source><html><head/><body><p><span style=" font-weight:600;">Name:</span></p></body></html></source> + <translation type="unfinished"></translation> + </message> + <message> + <source><html><head/><body><p><span style=" font-weight:600;">Version:</span></p></body></html></source> + <translation type="unfinished"></translation> + </message> + <message> + <source><html><head/><body><p><span style=" font-weight:600;">Author(s):</span></p></body></html></source> + <translation type="unfinished"></translation> + </message> + <message> + <source><html><head/><body><p><span style=" font-weight:600;">Description:</span></p></body></html></source> + <translation type="unfinished"></translation> + </message> + <message> + <source>&No</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>&Yes</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>The file "%1" is not a valid plugin file!</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Found more than one plugin library for the current OS in "%1" ("%2" and "%3")!</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to find a plugin for the current OS in "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to load plugin "%1" - check the plugin interface!</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to delete old plugin at "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to copy plugin library from "%1" to "%2"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to move plugin library to "%1"</source> <translation type="unfinished"></translation> </message> </context> <context> - <name>Plugins</name> + <name>PluginManager</name> + <message> + <source>%1 lost link</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>%1 linked</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Plugin "%1" encountered a permanent error in positional data gathering</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Non-plugin found in plugin directory: "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Failed at loading manual plugin: %1</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>PluginUpdater</name> + <message> + <source>PluginUpdater</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>The following plugins can be updated.</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Select all</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Plugin</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Download-URL</source> + <translation type="unfinished"></translation> + </message> <message> - <source>Skipping plugin update in debug mode.</source> + <source>Do you want to update the selected plugins?</source> <translation type="unfinished"></translation> </message> <message> - <source>Downloaded new or updated plugin to %1.</source> + <source>Unable to download plugin update for "%1" from "%2" (%3)</source> <translation type="unfinished"></translation> </message> <message> - <source>Failed to install new plugin to %1.</source> + <source>Update for plugin "%1" failed due to too many redirects</source> <translation type="unfinished"></translation> </message> <message> - <source>%1 lost link.</source> + <source>Unable to download plugin update for "%1" from "%2" (HTTP status code %3)</source> <translation type="unfinished"></translation> </message> <message> - <source>%1 linked.</source> + <source>Successfully updated plugin "%1"</source> <translation type="unfinished"></translation> </message> </context> diff --git a/src/mumble/mumble_bg.ts b/src/mumble/mumble_bg.ts index 4f3c1690e..8ff93bd2e 100644 --- a/src/mumble/mumble_bg.ts +++ b/src/mumble/mumble_bg.ts @@ -3779,6 +3779,10 @@ This field describes the size of an LCD device. The size is given either in pixe <source>User stopped listening to channel</source> <translation type="unfinished"></translation> </message> + <message> + <source>Plugin message</source> + <translation type="unfinished"></translation> + </message> </context> <context> <name>LogConfig</name> @@ -6122,12 +6126,13 @@ Valid actions are: <translation type="unfinished"></translation> </message> <message> - <source>Usage: mumble [options] [<url>] + <source>Usage: mumble [options] [<url> | <plugin_list>] <url> specifies a URL to connect to after startup instead of showing the connection window, and has the following form: mumble://[<username>[:<password>]@]<host>[:<port>][/<channel>[/<subchannel>...]][?version=<x.y.z>] +<plugin_list> is a list of plugin files that shall be installed The version query parameter has to be set in order to invoke the correct client version. It currently defaults to 1.2.0. @@ -6447,10 +6452,6 @@ Valid options are: <translation>Свързване към последния сървър при пускане на програмата</translation> </message> <message> - <source>Download plugin and overlay updates on startup</source> - <translation>Сваляне на обновления за слоя и приставките при пускане на програмата</translation> - </message> - <message> <source>Privacy</source> <translation>Поверителност</translation> </message> @@ -6475,6 +6476,14 @@ Prevents the client from sending potentially identifying information about the o <source>Hide public server list</source> <translation type="unfinished"></translation> </message> + <message> + <source>Check for plugin updates on startup</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Automatically download and install plugin updates</source> + <translation type="unfinished"></translation> + </message> </context> <context> <name>Overlay</name> @@ -7025,30 +7034,188 @@ To upgrade these files to their latest versions, click the button below.</source <translation>Име</translation> </message> <message> - <source>Enabled</source> - <translation>Включено</translation> + <source>Enable</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>PA</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>KeyEvents</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Install a plugin from a local file</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Install plugin...</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unload the currently selected plugin. This will remove it from the plugin list for the current session.</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unload</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>The plugin was installed successfully</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to deactivate all requested features for plugin "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to load plugin "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Whether the positional audio feature of this plugin should be enabled</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>This plugin does not provide support for positional audio</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Whether this plugin should be enabled</source> + <translation type="unfinished"></translation> </message> </context> <context> - <name>Plugins</name> + <name>PluginInstaller</name> + <message> + <source>PluginInstaller</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>You are about to install the plugin listed below. Do you wish to proceed?</source> + <translation type="unfinished"></translation> + </message> + <message> + <source><html><head/><body><p><span style=" font-weight:600;">Name:</span></p></body></html></source> + <translation type="unfinished"></translation> + </message> + <message> + <source><html><head/><body><p><span style=" font-weight:600;">Version:</span></p></body></html></source> + <translation type="unfinished"></translation> + </message> + <message> + <source><html><head/><body><p><span style=" font-weight:600;">Author(s):</span></p></body></html></source> + <translation type="unfinished"></translation> + </message> + <message> + <source><html><head/><body><p><span style=" font-weight:600;">Description:</span></p></body></html></source> + <translation type="unfinished"></translation> + </message> + <message> + <source>&No</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>&Yes</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>The file "%1" is not a valid plugin file!</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Found more than one plugin library for the current OS in "%1" ("%2" and "%3")!</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to find a plugin for the current OS in "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to load plugin "%1" - check the plugin interface!</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to delete old plugin at "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to copy plugin library from "%1" to "%2"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to move plugin library to "%1"</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>PluginManager</name> + <message> + <source>%1 lost link</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>%1 linked</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Plugin "%1" encountered a permanent error in positional data gathering</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Non-plugin found in plugin directory: "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Failed at loading manual plugin: %1</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>PluginUpdater</name> + <message> + <source>PluginUpdater</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>The following plugins can be updated.</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Select all</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Plugin</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Download-URL</source> + <translation type="unfinished"></translation> + </message> <message> - <source>Skipping plugin update in debug mode.</source> + <source>Do you want to update the selected plugins?</source> <translation type="unfinished"></translation> </message> <message> - <source>Downloaded new or updated plugin to %1.</source> + <source>Unable to download plugin update for "%1" from "%2" (%3)</source> <translation type="unfinished"></translation> </message> <message> - <source>Failed to install new plugin to %1.</source> + <source>Update for plugin "%1" failed due to too many redirects</source> <translation type="unfinished"></translation> </message> <message> - <source>%1 lost link.</source> + <source>Unable to download plugin update for "%1" from "%2" (HTTP status code %3)</source> <translation type="unfinished"></translation> </message> <message> - <source>%1 linked.</source> + <source>Successfully updated plugin "%1"</source> <translation type="unfinished"></translation> </message> </context> diff --git a/src/mumble/mumble_br.ts b/src/mumble/mumble_br.ts index 5fed8a97e..56c3e4cd4 100644 --- a/src/mumble/mumble_br.ts +++ b/src/mumble/mumble_br.ts @@ -3778,6 +3778,10 @@ This field describes the size of an LCD device. The size is given either in pixe <source>User stopped listening to channel</source> <translation type="unfinished"></translation> </message> + <message> + <source>Plugin message</source> + <translation type="unfinished"></translation> + </message> </context> <context> <name>LogConfig</name> @@ -6121,12 +6125,13 @@ Valid actions are: <translation type="unfinished"></translation> </message> <message> - <source>Usage: mumble [options] [<url>] + <source>Usage: mumble [options] [<url> | <plugin_list>] <url> specifies a URL to connect to after startup instead of showing the connection window, and has the following form: mumble://[<username>[:<password>]@]<host>[:<port>][/<channel>[/<subchannel>...]][?version=<x.y.z>] +<plugin_list> is a list of plugin files that shall be installed The version query parameter has to be set in order to invoke the correct client version. It currently defaults to 1.2.0. @@ -6446,10 +6451,6 @@ Valid options are: <translation type="unfinished"></translation> </message> <message> - <source>Download plugin and overlay updates on startup</source> - <translation type="unfinished"></translation> - </message> - <message> <source>Privacy</source> <translation type="unfinished"></translation> </message> @@ -6474,6 +6475,14 @@ Prevents the client from sending potentially identifying information about the o <source>Hide public server list</source> <translation type="unfinished"></translation> </message> + <message> + <source>Check for plugin updates on startup</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Automatically download and install plugin updates</source> + <translation type="unfinished"></translation> + </message> </context> <context> <name>Overlay</name> @@ -7024,30 +7033,188 @@ To upgrade these files to their latest versions, click the button below.</source <translation type="unfinished">Anv</translation> </message> <message> - <source>Enabled</source> + <source>Enable</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>PA</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>KeyEvents</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Install a plugin from a local file</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Install plugin...</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unload the currently selected plugin. This will remove it from the plugin list for the current session.</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unload</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>The plugin was installed successfully</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to deactivate all requested features for plugin "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to load plugin "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Whether the positional audio feature of this plugin should be enabled</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>This plugin does not provide support for positional audio</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Whether this plugin should be enabled</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>PluginInstaller</name> + <message> + <source>PluginInstaller</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>You are about to install the plugin listed below. Do you wish to proceed?</source> + <translation type="unfinished"></translation> + </message> + <message> + <source><html><head/><body><p><span style=" font-weight:600;">Name:</span></p></body></html></source> + <translation type="unfinished"></translation> + </message> + <message> + <source><html><head/><body><p><span style=" font-weight:600;">Version:</span></p></body></html></source> + <translation type="unfinished"></translation> + </message> + <message> + <source><html><head/><body><p><span style=" font-weight:600;">Author(s):</span></p></body></html></source> + <translation type="unfinished"></translation> + </message> + <message> + <source><html><head/><body><p><span style=" font-weight:600;">Description:</span></p></body></html></source> + <translation type="unfinished"></translation> + </message> + <message> + <source>&No</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>&Yes</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>The file "%1" is not a valid plugin file!</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Found more than one plugin library for the current OS in "%1" ("%2" and "%3")!</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to find a plugin for the current OS in "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to load plugin "%1" - check the plugin interface!</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to delete old plugin at "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to copy plugin library from "%1" to "%2"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to move plugin library to "%1"</source> <translation type="unfinished"></translation> </message> </context> <context> - <name>Plugins</name> + <name>PluginManager</name> + <message> + <source>%1 lost link</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>%1 linked</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Plugin "%1" encountered a permanent error in positional data gathering</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Non-plugin found in plugin directory: "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Failed at loading manual plugin: %1</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>PluginUpdater</name> + <message> + <source>PluginUpdater</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>The following plugins can be updated.</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Select all</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Plugin</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Download-URL</source> + <translation type="unfinished"></translation> + </message> <message> - <source>Skipping plugin update in debug mode.</source> + <source>Do you want to update the selected plugins?</source> <translation type="unfinished"></translation> </message> <message> - <source>Downloaded new or updated plugin to %1.</source> + <source>Unable to download plugin update for "%1" from "%2" (%3)</source> <translation type="unfinished"></translation> </message> <message> - <source>Failed to install new plugin to %1.</source> + <source>Update for plugin "%1" failed due to too many redirects</source> <translation type="unfinished"></translation> </message> <message> - <source>%1 lost link.</source> + <source>Unable to download plugin update for "%1" from "%2" (HTTP status code %3)</source> <translation type="unfinished"></translation> </message> <message> - <source>%1 linked.</source> + <source>Successfully updated plugin "%1"</source> <translation type="unfinished"></translation> </message> </context> diff --git a/src/mumble/mumble_ca.ts b/src/mumble/mumble_ca.ts index d545cbe6a..df853080e 100644 --- a/src/mumble/mumble_ca.ts +++ b/src/mumble/mumble_ca.ts @@ -3784,6 +3784,10 @@ This field describes the size of an LCD device. The size is given either in pixe <source>User stopped listening to channel</source> <translation type="unfinished"></translation> </message> + <message> + <source>Plugin message</source> + <translation type="unfinished"></translation> + </message> </context> <context> <name>LogConfig</name> @@ -6127,12 +6131,13 @@ Valid actions are: <translation type="unfinished"></translation> </message> <message> - <source>Usage: mumble [options] [<url>] + <source>Usage: mumble [options] [<url> | <plugin_list>] <url> specifies a URL to connect to after startup instead of showing the connection window, and has the following form: mumble://[<username>[:<password>]@]<host>[:<port>][/<channel>[/<subchannel>...]][?version=<x.y.z>] +<plugin_list> is a list of plugin files that shall be installed The version query parameter has to be set in order to invoke the correct client version. It currently defaults to 1.2.0. @@ -6452,10 +6457,6 @@ Valid options are: <translation type="unfinished"></translation> </message> <message> - <source>Download plugin and overlay updates on startup</source> - <translation type="unfinished"></translation> - </message> - <message> <source>Privacy</source> <translation type="unfinished"></translation> </message> @@ -6480,6 +6481,14 @@ Prevents the client from sending potentially identifying information about the o <source>Hide public server list</source> <translation type="unfinished"></translation> </message> + <message> + <source>Check for plugin updates on startup</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Automatically download and install plugin updates</source> + <translation type="unfinished"></translation> + </message> </context> <context> <name>Overlay</name> @@ -7030,30 +7039,188 @@ To upgrade these files to their latest versions, click the button below.</source <translation type="unfinished">Nom</translation> </message> <message> - <source>Enabled</source> + <source>Enable</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>PA</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>KeyEvents</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Install a plugin from a local file</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Install plugin...</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unload the currently selected plugin. This will remove it from the plugin list for the current session.</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unload</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>The plugin was installed successfully</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to deactivate all requested features for plugin "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to load plugin "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Whether the positional audio feature of this plugin should be enabled</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>This plugin does not provide support for positional audio</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Whether this plugin should be enabled</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>PluginInstaller</name> + <message> + <source>PluginInstaller</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>You are about to install the plugin listed below. Do you wish to proceed?</source> + <translation type="unfinished"></translation> + </message> + <message> + <source><html><head/><body><p><span style=" font-weight:600;">Name:</span></p></body></html></source> + <translation type="unfinished"></translation> + </message> + <message> + <source><html><head/><body><p><span style=" font-weight:600;">Version:</span></p></body></html></source> + <translation type="unfinished"></translation> + </message> + <message> + <source><html><head/><body><p><span style=" font-weight:600;">Author(s):</span></p></body></html></source> + <translation type="unfinished"></translation> + </message> + <message> + <source><html><head/><body><p><span style=" font-weight:600;">Description:</span></p></body></html></source> + <translation type="unfinished"></translation> + </message> + <message> + <source>&No</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>&Yes</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>The file "%1" is not a valid plugin file!</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Found more than one plugin library for the current OS in "%1" ("%2" and "%3")!</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to find a plugin for the current OS in "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to load plugin "%1" - check the plugin interface!</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to delete old plugin at "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to copy plugin library from "%1" to "%2"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to move plugin library to "%1"</source> <translation type="unfinished"></translation> </message> </context> <context> - <name>Plugins</name> + <name>PluginManager</name> + <message> + <source>%1 lost link</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>%1 linked</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Plugin "%1" encountered a permanent error in positional data gathering</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Non-plugin found in plugin directory: "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Failed at loading manual plugin: %1</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>PluginUpdater</name> + <message> + <source>PluginUpdater</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>The following plugins can be updated.</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Select all</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Plugin</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Download-URL</source> + <translation type="unfinished"></translation> + </message> <message> - <source>Skipping plugin update in debug mode.</source> + <source>Do you want to update the selected plugins?</source> <translation type="unfinished"></translation> </message> <message> - <source>Downloaded new or updated plugin to %1.</source> + <source>Unable to download plugin update for "%1" from "%2" (%3)</source> <translation type="unfinished"></translation> </message> <message> - <source>Failed to install new plugin to %1.</source> + <source>Update for plugin "%1" failed due to too many redirects</source> <translation type="unfinished"></translation> </message> <message> - <source>%1 lost link.</source> + <source>Unable to download plugin update for "%1" from "%2" (HTTP status code %3)</source> <translation type="unfinished"></translation> </message> <message> - <source>%1 linked.</source> + <source>Successfully updated plugin "%1"</source> <translation type="unfinished"></translation> </message> </context> diff --git a/src/mumble/mumble_cs.ts b/src/mumble/mumble_cs.ts index df4b57b60..d564ee872 100644 --- a/src/mumble/mumble_cs.ts +++ b/src/mumble/mumble_cs.ts @@ -3833,6 +3833,10 @@ Toto pole popisuje velikost LCD zařízení. Velikost je udávána buď v pixele <source>User stopped listening to channel</source> <translation type="unfinished"></translation> </message> + <message> + <source>Plugin message</source> + <translation type="unfinished"></translation> + </message> </context> <context> <name>LogConfig</name> @@ -6181,12 +6185,13 @@ Valid actions are: <translation type="unfinished"></translation> </message> <message> - <source>Usage: mumble [options] [<url>] + <source>Usage: mumble [options] [<url> | <plugin_list>] <url> specifies a URL to connect to after startup instead of showing the connection window, and has the following form: mumble://[<username>[:<password>]@]<host>[:<port>][/<channel>[/<subchannel>...]][?version=<x.y.z>] +<plugin_list> is a list of plugin files that shall be installed The version query parameter has to be set in order to invoke the correct client version. It currently defaults to 1.2.0. @@ -6506,10 +6511,6 @@ Valid options are: <translation>Při startu se znovu připojit na poslední server</translation> </message> <message> - <source>Download plugin and overlay updates on startup</source> - <translation>Při startu stáhnout aktualizace zásuvných modulů a překryvů</translation> - </message> - <message> <source>Privacy</source> <translation type="unfinished"></translation> </message> @@ -6534,6 +6535,14 @@ Prevents the client from sending potentially identifying information about the o <source>Hide public server list</source> <translation type="unfinished"></translation> </message> + <message> + <source>Check for plugin updates on startup</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Automatically download and install plugin updates</source> + <translation type="unfinished"></translation> + </message> </context> <context> <name>Overlay</name> @@ -7088,31 +7097,189 @@ Pro aktualizaci těchto souborů na jejich poslední verzi, klikněte na tlačí <translation>Jméno</translation> </message> <message> - <source>Enabled</source> - <translation>Povoleno</translation> + <source>Enable</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>PA</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>KeyEvents</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Install a plugin from a local file</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Install plugin...</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unload the currently selected plugin. This will remove it from the plugin list for the current session.</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unload</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>The plugin was installed successfully</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to deactivate all requested features for plugin "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to load plugin "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Whether the positional audio feature of this plugin should be enabled</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>This plugin does not provide support for positional audio</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Whether this plugin should be enabled</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>PluginInstaller</name> + <message> + <source>PluginInstaller</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>You are about to install the plugin listed below. Do you wish to proceed?</source> + <translation type="unfinished"></translation> + </message> + <message> + <source><html><head/><body><p><span style=" font-weight:600;">Name:</span></p></body></html></source> + <translation type="unfinished"></translation> + </message> + <message> + <source><html><head/><body><p><span style=" font-weight:600;">Version:</span></p></body></html></source> + <translation type="unfinished"></translation> + </message> + <message> + <source><html><head/><body><p><span style=" font-weight:600;">Author(s):</span></p></body></html></source> + <translation type="unfinished"></translation> + </message> + <message> + <source><html><head/><body><p><span style=" font-weight:600;">Description:</span></p></body></html></source> + <translation type="unfinished"></translation> + </message> + <message> + <source>&No</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>&Yes</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>The file "%1" is not a valid plugin file!</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Found more than one plugin library for the current OS in "%1" ("%2" and "%3")!</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to find a plugin for the current OS in "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to load plugin "%1" - check the plugin interface!</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to delete old plugin at "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to copy plugin library from "%1" to "%2"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to move plugin library to "%1"</source> + <translation type="unfinished"></translation> </message> </context> <context> - <name>Plugins</name> + <name>PluginManager</name> <message> - <source>Skipping plugin update in debug mode.</source> - <translation>V režimu ladění přeskakuji aktualizaci zásuvných modulů.</translation> + <source>%1 lost link</source> + <translation type="unfinished"></translation> </message> <message> - <source>Downloaded new or updated plugin to %1.</source> - <translation>Aktualizován nebo stáhnut nový zásuvný modul do %1.</translation> + <source>%1 linked</source> + <translation type="unfinished"></translation> </message> <message> - <source>Failed to install new plugin to %1.</source> - <translation>Nelze instalovat nový zásuvný modul do %1.</translation> + <source>Plugin "%1" encountered a permanent error in positional data gathering</source> + <translation type="unfinished"></translation> </message> <message> - <source>%1 lost link.</source> - <translation>%1 ztraceno propojení.</translation> + <source>Non-plugin found in plugin directory: "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Failed at loading manual plugin: %1</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>PluginUpdater</name> + <message> + <source>PluginUpdater</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>The following plugins can be updated.</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Select all</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Plugin</source> + <translation type="unfinished"></translation> </message> <message> - <source>%1 linked.</source> - <translation>%1 propojen.</translation> + <source>Download-URL</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Do you want to update the selected plugins?</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to download plugin update for "%1" from "%2" (%3)</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Update for plugin "%1" failed due to too many redirects</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to download plugin update for "%1" from "%2" (HTTP status code %3)</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Successfully updated plugin "%1"</source> + <translation type="unfinished"></translation> </message> </context> <context> diff --git a/src/mumble/mumble_cy.ts b/src/mumble/mumble_cy.ts index 19d907b49..d3ce85dca 100644 --- a/src/mumble/mumble_cy.ts +++ b/src/mumble/mumble_cy.ts @@ -3782,6 +3782,10 @@ This field describes the size of an LCD device. The size is given either in pixe <source>User stopped listening to channel</source> <translation type="unfinished"></translation> </message> + <message> + <source>Plugin message</source> + <translation type="unfinished"></translation> + </message> </context> <context> <name>LogConfig</name> @@ -6125,12 +6129,13 @@ Valid actions are: <translation type="unfinished"></translation> </message> <message> - <source>Usage: mumble [options] [<url>] + <source>Usage: mumble [options] [<url> | <plugin_list>] <url> specifies a URL to connect to after startup instead of showing the connection window, and has the following form: mumble://[<username>[:<password>]@]<host>[:<port>][/<channel>[/<subchannel>...]][?version=<x.y.z>] +<plugin_list> is a list of plugin files that shall be installed The version query parameter has to be set in order to invoke the correct client version. It currently defaults to 1.2.0. @@ -6450,10 +6455,6 @@ Valid options are: <translation>Ailgysylltu i'r gweinydd olaf ar gychwyn</translation> </message> <message> - <source>Download plugin and overlay updates on startup</source> - <translation type="unfinished"></translation> - </message> - <message> <source>Privacy</source> <translation type="unfinished"></translation> </message> @@ -6478,6 +6479,14 @@ Prevents the client from sending potentially identifying information about the o <source>Hide public server list</source> <translation type="unfinished"></translation> </message> + <message> + <source>Check for plugin updates on startup</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Automatically download and install plugin updates</source> + <translation type="unfinished"></translation> + </message> </context> <context> <name>Overlay</name> @@ -7028,30 +7037,188 @@ To upgrade these files to their latest versions, click the button below.</source <translation>Enw</translation> </message> <message> - <source>Enabled</source> - <translation>Wedi'i alluogi</translation> + <source>Enable</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>PA</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>KeyEvents</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Install a plugin from a local file</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Install plugin...</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unload the currently selected plugin. This will remove it from the plugin list for the current session.</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unload</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>The plugin was installed successfully</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to deactivate all requested features for plugin "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to load plugin "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Whether the positional audio feature of this plugin should be enabled</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>This plugin does not provide support for positional audio</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Whether this plugin should be enabled</source> + <translation type="unfinished"></translation> </message> </context> <context> - <name>Plugins</name> + <name>PluginInstaller</name> + <message> + <source>PluginInstaller</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>You are about to install the plugin listed below. Do you wish to proceed?</source> + <translation type="unfinished"></translation> + </message> + <message> + <source><html><head/><body><p><span style=" font-weight:600;">Name:</span></p></body></html></source> + <translation type="unfinished"></translation> + </message> + <message> + <source><html><head/><body><p><span style=" font-weight:600;">Version:</span></p></body></html></source> + <translation type="unfinished"></translation> + </message> + <message> + <source><html><head/><body><p><span style=" font-weight:600;">Author(s):</span></p></body></html></source> + <translation type="unfinished"></translation> + </message> + <message> + <source><html><head/><body><p><span style=" font-weight:600;">Description:</span></p></body></html></source> + <translation type="unfinished"></translation> + </message> + <message> + <source>&No</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>&Yes</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>The file "%1" is not a valid plugin file!</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Found more than one plugin library for the current OS in "%1" ("%2" and "%3")!</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to find a plugin for the current OS in "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to load plugin "%1" - check the plugin interface!</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to delete old plugin at "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to copy plugin library from "%1" to "%2"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to move plugin library to "%1"</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>PluginManager</name> + <message> + <source>%1 lost link</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>%1 linked</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Plugin "%1" encountered a permanent error in positional data gathering</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Non-plugin found in plugin directory: "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Failed at loading manual plugin: %1</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>PluginUpdater</name> + <message> + <source>PluginUpdater</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>The following plugins can be updated.</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Select all</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Plugin</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Download-URL</source> + <translation type="unfinished"></translation> + </message> <message> - <source>Skipping plugin update in debug mode.</source> + <source>Do you want to update the selected plugins?</source> <translation type="unfinished"></translation> </message> <message> - <source>Downloaded new or updated plugin to %1.</source> + <source>Unable to download plugin update for "%1" from "%2" (%3)</source> <translation type="unfinished"></translation> </message> <message> - <source>Failed to install new plugin to %1.</source> + <source>Update for plugin "%1" failed due to too many redirects</source> <translation type="unfinished"></translation> </message> <message> - <source>%1 lost link.</source> + <source>Unable to download plugin update for "%1" from "%2" (HTTP status code %3)</source> <translation type="unfinished"></translation> </message> <message> - <source>%1 linked.</source> + <source>Successfully updated plugin "%1"</source> <translation type="unfinished"></translation> </message> </context> diff --git a/src/mumble/mumble_da.ts b/src/mumble/mumble_da.ts index c480f2470..72d08e75f 100644 --- a/src/mumble/mumble_da.ts +++ b/src/mumble/mumble_da.ts @@ -3831,6 +3831,10 @@ Dette felt beskriver størrelsen af en LCD-enhed. Størrelsen er enten opgivet i <source>User stopped listening to channel</source> <translation type="unfinished"></translation> </message> + <message> + <source>Plugin message</source> + <translation type="unfinished"></translation> + </message> </context> <context> <name>LogConfig</name> @@ -6177,12 +6181,13 @@ Valid actions are: <translation type="unfinished"></translation> </message> <message> - <source>Usage: mumble [options] [<url>] + <source>Usage: mumble [options] [<url> | <plugin_list>] <url> specifies a URL to connect to after startup instead of showing the connection window, and has the following form: mumble://[<username>[:<password>]@]<host>[:<port>][/<channel>[/<subchannel>...]][?version=<x.y.z>] +<plugin_list> is a list of plugin files that shall be installed The version query parameter has to be set in order to invoke the correct client version. It currently defaults to 1.2.0. @@ -6502,10 +6507,6 @@ Valid options are: <translation>Forbind igen til sidst anvendte server ved opstart</translation> </message> <message> - <source>Download plugin and overlay updates on startup</source> - <translation>Download plugin- og overlægningsopdateringer ved opstart</translation> - </message> - <message> <source>Privacy</source> <translation type="unfinished"></translation> </message> @@ -6530,6 +6531,14 @@ Prevents the client from sending potentially identifying information about the o <source>Hide public server list</source> <translation type="unfinished"></translation> </message> + <message> + <source>Check for plugin updates on startup</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Automatically download and install plugin updates</source> + <translation type="unfinished"></translation> + </message> </context> <context> <name>Overlay</name> @@ -7084,31 +7093,189 @@ For at opgradere disse filer til deres nyeste version, klik på knappen nedenfor <translation>Navn</translation> </message> <message> - <source>Enabled</source> - <translation>Aktiveret</translation> + <source>Enable</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>PA</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>KeyEvents</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Install a plugin from a local file</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Install plugin...</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unload the currently selected plugin. This will remove it from the plugin list for the current session.</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unload</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>The plugin was installed successfully</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to deactivate all requested features for plugin "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to load plugin "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Whether the positional audio feature of this plugin should be enabled</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>This plugin does not provide support for positional audio</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Whether this plugin should be enabled</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>PluginInstaller</name> + <message> + <source>PluginInstaller</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>You are about to install the plugin listed below. Do you wish to proceed?</source> + <translation type="unfinished"></translation> + </message> + <message> + <source><html><head/><body><p><span style=" font-weight:600;">Name:</span></p></body></html></source> + <translation type="unfinished"></translation> + </message> + <message> + <source><html><head/><body><p><span style=" font-weight:600;">Version:</span></p></body></html></source> + <translation type="unfinished"></translation> + </message> + <message> + <source><html><head/><body><p><span style=" font-weight:600;">Author(s):</span></p></body></html></source> + <translation type="unfinished"></translation> + </message> + <message> + <source><html><head/><body><p><span style=" font-weight:600;">Description:</span></p></body></html></source> + <translation type="unfinished"></translation> + </message> + <message> + <source>&No</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>&Yes</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>The file "%1" is not a valid plugin file!</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Found more than one plugin library for the current OS in "%1" ("%2" and "%3")!</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to find a plugin for the current OS in "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to load plugin "%1" - check the plugin interface!</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to delete old plugin at "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to copy plugin library from "%1" to "%2"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to move plugin library to "%1"</source> + <translation type="unfinished"></translation> </message> </context> <context> - <name>Plugins</name> + <name>PluginManager</name> <message> - <source>Skipping plugin update in debug mode.</source> - <translation>Springer over pluginopdateringer i fejlfindingstilstand.</translation> + <source>%1 lost link</source> + <translation type="unfinished"></translation> </message> <message> - <source>Downloaded new or updated plugin to %1.</source> - <translation>Downloadede nyt eller opdateret plugin til %1.</translation> + <source>%1 linked</source> + <translation type="unfinished"></translation> </message> <message> - <source>Failed to install new plugin to %1.</source> - <translation>Installation af nyt plugin til %1 mislykkedes.</translation> + <source>Plugin "%1" encountered a permanent error in positional data gathering</source> + <translation type="unfinished"></translation> </message> <message> - <source>%1 lost link.</source> - <translation>%1 mistede link.</translation> + <source>Non-plugin found in plugin directory: "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Failed at loading manual plugin: %1</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>PluginUpdater</name> + <message> + <source>PluginUpdater</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>The following plugins can be updated.</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Select all</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Plugin</source> + <translation type="unfinished"></translation> </message> <message> - <source>%1 linked.</source> - <translation>%1 er linket.</translation> + <source>Download-URL</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Do you want to update the selected plugins?</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to download plugin update for "%1" from "%2" (%3)</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Update for plugin "%1" failed due to too many redirects</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to download plugin update for "%1" from "%2" (HTTP status code %3)</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Successfully updated plugin "%1"</source> + <translation type="unfinished"></translation> </message> </context> <context> diff --git a/src/mumble/mumble_de.ts b/src/mumble/mumble_de.ts index 5ac64fbe7..3ceee3768 100644 --- a/src/mumble/mumble_de.ts +++ b/src/mumble/mumble_de.ts @@ -3882,6 +3882,10 @@ This field describes the size of an LCD device. The size is given either in pixe <source>User stopped listening to channel</source> <translation>Benutzer hört dem Kanal nicht mehr zu</translation> </message> + <message> + <source>Plugin message</source> + <translation type="unfinished"></translation> + </message> </context> <context> <name>LogConfig</name> @@ -6239,12 +6243,13 @@ Valid actions are: <translation type="unfinished"></translation> </message> <message> - <source>Usage: mumble [options] [<url>] + <source>Usage: mumble [options] [<url> | <plugin_list>] <url> specifies a URL to connect to after startup instead of showing the connection window, and has the following form: mumble://[<username>[:<password>]@]<host>[:<port>][/<channel>[/<subchannel>...]][?version=<x.y.z>] +<plugin_list> is a list of plugin files that shall be installed The version query parameter has to be set in order to invoke the correct client version. It currently defaults to 1.2.0. @@ -6567,10 +6572,6 @@ Mumble hat ein kleines Entwickler-Team. Deshalb muss die verfügbare Zeit auf di <translation>Beim Start zum zuletzt benutzten Server verbinden</translation> </message> <message> - <source>Download plugin and overlay updates on startup</source> - <translation>Plugin und Overlay Updates beim Starten herunterladen</translation> - </message> - <message> <source>Privacy</source> <translation>Datenschutz</translation> </message> @@ -6596,6 +6597,14 @@ Verhindert, dass potenziell identifizierende Informationen über das Betriebssys <source>Hide public server list</source> <translation>Öffentliche Serverliste verstecken</translation> </message> + <message> + <source>Check for plugin updates on startup</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Automatically download and install plugin updates</source> + <translation type="unfinished"></translation> + </message> </context> <context> <name>Overlay</name> @@ -7151,31 +7160,189 @@ Um diese Dateien zu aktualisieren, klicken Sie unten den Button.</translation> <translation>Name</translation> </message> <message> - <source>Enabled</source> - <translation>Aktiviert</translation> + <source>Enable</source> + <translation type="unfinished">Aktivieren</translation> + </message> + <message> + <source>PA</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>KeyEvents</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Install a plugin from a local file</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Install plugin...</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unload the currently selected plugin. This will remove it from the plugin list for the current session.</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unload</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>The plugin was installed successfully</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to deactivate all requested features for plugin "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to load plugin "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Whether the positional audio feature of this plugin should be enabled</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>This plugin does not provide support for positional audio</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Whether this plugin should be enabled</source> + <translation type="unfinished"></translation> </message> </context> <context> - <name>Plugins</name> + <name>PluginInstaller</name> <message> - <source>Skipping plugin update in debug mode.</source> - <translation>Überspringe Plugin-Aktualisierung im Debug-Modus.</translation> + <source>PluginInstaller</source> + <translation type="unfinished"></translation> </message> <message> - <source>Downloaded new or updated plugin to %1.</source> - <translation>Neues oder aktualisiertes Plugin nach %1 heruntergeladen.</translation> + <source>You are about to install the plugin listed below. Do you wish to proceed?</source> + <translation type="unfinished"></translation> </message> <message> - <source>Failed to install new plugin to %1.</source> - <translation>Installation eines neuen Plugins nach %1 fehlgeschlagen.</translation> + <source><html><head/><body><p><span style=" font-weight:600;">Name:</span></p></body></html></source> + <translation type="unfinished"></translation> </message> <message> - <source>%1 lost link.</source> - <translation>%1 hat Verbindung verloren.</translation> + <source><html><head/><body><p><span style=" font-weight:600;">Version:</span></p></body></html></source> + <translation type="unfinished"></translation> + </message> + <message> + <source><html><head/><body><p><span style=" font-weight:600;">Author(s):</span></p></body></html></source> + <translation type="unfinished"></translation> + </message> + <message> + <source><html><head/><body><p><span style=" font-weight:600;">Description:</span></p></body></html></source> + <translation type="unfinished"></translation> + </message> + <message> + <source>&No</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>&Yes</source> + <translation type="unfinished"></translation> </message> <message> - <source>%1 linked.</source> - <translation>%1 verbunden.</translation> + <source>The file "%1" is not a valid plugin file!</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Found more than one plugin library for the current OS in "%1" ("%2" and "%3")!</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to find a plugin for the current OS in "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to load plugin "%1" - check the plugin interface!</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to delete old plugin at "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to copy plugin library from "%1" to "%2"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to move plugin library to "%1"</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>PluginManager</name> + <message> + <source>%1 lost link</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>%1 linked</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Plugin "%1" encountered a permanent error in positional data gathering</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Non-plugin found in plugin directory: "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Failed at loading manual plugin: %1</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>PluginUpdater</name> + <message> + <source>PluginUpdater</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>The following plugins can be updated.</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Select all</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Plugin</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Download-URL</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Do you want to update the selected plugins?</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to download plugin update for "%1" from "%2" (%3)</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Update for plugin "%1" failed due to too many redirects</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to download plugin update for "%1" from "%2" (HTTP status code %3)</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Successfully updated plugin "%1"</source> + <translation type="unfinished"></translation> </message> </context> <context> diff --git a/src/mumble/mumble_el.ts b/src/mumble/mumble_el.ts index a577a1e36..761c641a3 100644 --- a/src/mumble/mumble_el.ts +++ b/src/mumble/mumble_el.ts @@ -3835,6 +3835,10 @@ This field describes the size of an LCD device. The size is given either in pixe <source>User stopped listening to channel</source> <translation type="unfinished"></translation> </message> + <message> + <source>Plugin message</source> + <translation type="unfinished"></translation> + </message> </context> <context> <name>LogConfig</name> @@ -6186,12 +6190,13 @@ Valid actions are: <translation type="unfinished"></translation> </message> <message> - <source>Usage: mumble [options] [<url>] + <source>Usage: mumble [options] [<url> | <plugin_list>] <url> specifies a URL to connect to after startup instead of showing the connection window, and has the following form: mumble://[<username>[:<password>]@]<host>[:<port>][/<channel>[/<subchannel>...]][?version=<x.y.z>] +<plugin_list> is a list of plugin files that shall be installed The version query parameter has to be set in order to invoke the correct client version. It currently defaults to 1.2.0. @@ -6511,10 +6516,6 @@ Valid options are: <translation>Επανασύνδεση με τον τελευταίο διακομιστή που χρησιμοποιήθηκε κατά την εκκίνηση.</translation> </message> <message> - <source>Download plugin and overlay updates on startup</source> - <translation>Να γίνεται λήψη ενημερώσεων για τα πρόσθετα και το overlay κατά την εκκίνηση</translation> - </message> - <message> <source>Privacy</source> <translation>Ιδιωτικότητα</translation> </message> @@ -6540,6 +6541,14 @@ Prevents the client from sending potentially identifying information about the o <source>Hide public server list</source> <translation type="unfinished"></translation> </message> + <message> + <source>Check for plugin updates on startup</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Automatically download and install plugin updates</source> + <translation type="unfinished"></translation> + </message> </context> <context> <name>Overlay</name> @@ -7094,31 +7103,189 @@ To upgrade these files to their latest versions, click the button below.</source <translation>Όνομα</translation> </message> <message> - <source>Enabled</source> - <translation>Ενεργοποιημένο</translation> + <source>Enable</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>PA</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>KeyEvents</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Install a plugin from a local file</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Install plugin...</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unload the currently selected plugin. This will remove it from the plugin list for the current session.</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unload</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>The plugin was installed successfully</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to deactivate all requested features for plugin "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to load plugin "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Whether the positional audio feature of this plugin should be enabled</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>This plugin does not provide support for positional audio</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Whether this plugin should be enabled</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>PluginInstaller</name> + <message> + <source>PluginInstaller</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>You are about to install the plugin listed below. Do you wish to proceed?</source> + <translation type="unfinished"></translation> + </message> + <message> + <source><html><head/><body><p><span style=" font-weight:600;">Name:</span></p></body></html></source> + <translation type="unfinished"></translation> + </message> + <message> + <source><html><head/><body><p><span style=" font-weight:600;">Version:</span></p></body></html></source> + <translation type="unfinished"></translation> + </message> + <message> + <source><html><head/><body><p><span style=" font-weight:600;">Author(s):</span></p></body></html></source> + <translation type="unfinished"></translation> + </message> + <message> + <source><html><head/><body><p><span style=" font-weight:600;">Description:</span></p></body></html></source> + <translation type="unfinished"></translation> + </message> + <message> + <source>&No</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>&Yes</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>The file "%1" is not a valid plugin file!</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Found more than one plugin library for the current OS in "%1" ("%2" and "%3")!</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to find a plugin for the current OS in "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to load plugin "%1" - check the plugin interface!</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to delete old plugin at "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to copy plugin library from "%1" to "%2"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to move plugin library to "%1"</source> + <translation type="unfinished"></translation> </message> </context> <context> - <name>Plugins</name> + <name>PluginManager</name> <message> - <source>Skipping plugin update in debug mode.</source> - <translation>Παράλειψη της ενημέρωσης των πρόσθετων στη λειτουργία εντοπισμού σφαλμάτων.</translation> + <source>%1 lost link</source> + <translation type="unfinished"></translation> </message> <message> - <source>Downloaded new or updated plugin to %1.</source> - <translation>Έγινε λήψη νέου ή ενημερωμένου πρόσθετου στο %1.</translation> + <source>%1 linked</source> + <translation type="unfinished"></translation> </message> <message> - <source>Failed to install new plugin to %1.</source> - <translation>Απέτυχε η εγκατάσταση του νέου πρόσθετου στο %1.</translation> + <source>Plugin "%1" encountered a permanent error in positional data gathering</source> + <translation type="unfinished"></translation> </message> <message> - <source>%1 lost link.</source> - <translation>%1 χαμένος σύνδεσμος.</translation> + <source>Non-plugin found in plugin directory: "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Failed at loading manual plugin: %1</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>PluginUpdater</name> + <message> + <source>PluginUpdater</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>The following plugins can be updated.</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Select all</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Plugin</source> + <translation type="unfinished"></translation> </message> <message> - <source>%1 linked.</source> - <translation>%1 σύνδεσμος.</translation> + <source>Download-URL</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Do you want to update the selected plugins?</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to download plugin update for "%1" from "%2" (%3)</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Update for plugin "%1" failed due to too many redirects</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to download plugin update for "%1" from "%2" (HTTP status code %3)</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Successfully updated plugin "%1"</source> + <translation type="unfinished"></translation> </message> </context> <context> diff --git a/src/mumble/mumble_en.ts b/src/mumble/mumble_en.ts index 4a0358e0d..49e2aceb2 100644 --- a/src/mumble/mumble_en.ts +++ b/src/mumble/mumble_en.ts @@ -3777,6 +3777,10 @@ This field describes the size of an LCD device. The size is given either in pixe <source>User stopped listening to channel</source> <translation type="unfinished"></translation> </message> + <message> + <source>Plugin message</source> + <translation type="unfinished"></translation> + </message> </context> <context> <name>LogConfig</name> @@ -6120,12 +6124,13 @@ Valid actions are: <translation type="unfinished"></translation> </message> <message> - <source>Usage: mumble [options] [<url>] + <source>Usage: mumble [options] [<url> | <plugin_list>] <url> specifies a URL to connect to after startup instead of showing the connection window, and has the following form: mumble://[<username>[:<password>]@]<host>[:<port>][/<channel>[/<subchannel>...]][?version=<x.y.z>] +<plugin_list> is a list of plugin files that shall be installed The version query parameter has to be set in order to invoke the correct client version. It currently defaults to 1.2.0. @@ -6445,10 +6450,6 @@ Valid options are: <translation type="unfinished"></translation> </message> <message> - <source>Download plugin and overlay updates on startup</source> - <translation type="unfinished"></translation> - </message> - <message> <source>Privacy</source> <translation type="unfinished"></translation> </message> @@ -6473,6 +6474,14 @@ Prevents the client from sending potentially identifying information about the o <source>Hide public server list</source> <translation type="unfinished"></translation> </message> + <message> + <source>Check for plugin updates on startup</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Automatically download and install plugin updates</source> + <translation type="unfinished"></translation> + </message> </context> <context> <name>Overlay</name> @@ -7023,30 +7032,188 @@ To upgrade these files to their latest versions, click the button below.</source <translation type="unfinished"></translation> </message> <message> - <source>Enabled</source> + <source>Enable</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>PA</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>KeyEvents</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Install a plugin from a local file</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Install plugin...</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unload the currently selected plugin. This will remove it from the plugin list for the current session.</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unload</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>The plugin was installed successfully</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to deactivate all requested features for plugin "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to load plugin "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Whether the positional audio feature of this plugin should be enabled</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>This plugin does not provide support for positional audio</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Whether this plugin should be enabled</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>PluginInstaller</name> + <message> + <source>PluginInstaller</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>You are about to install the plugin listed below. Do you wish to proceed?</source> + <translation type="unfinished"></translation> + </message> + <message> + <source><html><head/><body><p><span style=" font-weight:600;">Name:</span></p></body></html></source> + <translation type="unfinished"></translation> + </message> + <message> + <source><html><head/><body><p><span style=" font-weight:600;">Version:</span></p></body></html></source> + <translation type="unfinished"></translation> + </message> + <message> + <source><html><head/><body><p><span style=" font-weight:600;">Author(s):</span></p></body></html></source> + <translation type="unfinished"></translation> + </message> + <message> + <source><html><head/><body><p><span style=" font-weight:600;">Description:</span></p></body></html></source> + <translation type="unfinished"></translation> + </message> + <message> + <source>&No</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>&Yes</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>The file "%1" is not a valid plugin file!</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Found more than one plugin library for the current OS in "%1" ("%2" and "%3")!</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to find a plugin for the current OS in "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to load plugin "%1" - check the plugin interface!</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to delete old plugin at "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to copy plugin library from "%1" to "%2"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to move plugin library to "%1"</source> <translation type="unfinished"></translation> </message> </context> <context> - <name>Plugins</name> + <name>PluginManager</name> + <message> + <source>%1 lost link</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>%1 linked</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Plugin "%1" encountered a permanent error in positional data gathering</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Non-plugin found in plugin directory: "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Failed at loading manual plugin: %1</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>PluginUpdater</name> + <message> + <source>PluginUpdater</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>The following plugins can be updated.</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Select all</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Plugin</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Download-URL</source> + <translation type="unfinished"></translation> + </message> <message> - <source>Skipping plugin update in debug mode.</source> + <source>Do you want to update the selected plugins?</source> <translation type="unfinished"></translation> </message> <message> - <source>Downloaded new or updated plugin to %1.</source> + <source>Unable to download plugin update for "%1" from "%2" (%3)</source> <translation type="unfinished"></translation> </message> <message> - <source>Failed to install new plugin to %1.</source> + <source>Update for plugin "%1" failed due to too many redirects</source> <translation type="unfinished"></translation> </message> <message> - <source>%1 lost link.</source> + <source>Unable to download plugin update for "%1" from "%2" (HTTP status code %3)</source> <translation type="unfinished"></translation> </message> <message> - <source>%1 linked.</source> + <source>Successfully updated plugin "%1"</source> <translation type="unfinished"></translation> </message> </context> diff --git a/src/mumble/mumble_en_GB.ts b/src/mumble/mumble_en_GB.ts index c1cd32e0d..63b1260bb 100644 --- a/src/mumble/mumble_en_GB.ts +++ b/src/mumble/mumble_en_GB.ts @@ -3814,6 +3814,10 @@ This field describes the size of an LCD device. The size is given either in pixe <source>User stopped listening to channel</source> <translation type="unfinished"></translation> </message> + <message> + <source>Plugin message</source> + <translation type="unfinished"></translation> + </message> </context> <context> <name>LogConfig</name> @@ -6157,12 +6161,13 @@ Valid actions are: <translation type="unfinished"></translation> </message> <message> - <source>Usage: mumble [options] [<url>] + <source>Usage: mumble [options] [<url> | <plugin_list>] <url> specifies a URL to connect to after startup instead of showing the connection window, and has the following form: mumble://[<username>[:<password>]@]<host>[:<port>][/<channel>[/<subchannel>...]][?version=<x.y.z>] +<plugin_list> is a list of plugin files that shall be installed The version query parameter has to be set in order to invoke the correct client version. It currently defaults to 1.2.0. @@ -6482,10 +6487,6 @@ Valid options are: <translation type="unfinished"></translation> </message> <message> - <source>Download plugin and overlay updates on startup</source> - <translation type="unfinished"></translation> - </message> - <message> <source>Privacy</source> <translation type="unfinished"></translation> </message> @@ -6510,6 +6511,14 @@ Prevents the client from sending potentially identifying information about the o <source>Hide public server list</source> <translation type="unfinished"></translation> </message> + <message> + <source>Check for plugin updates on startup</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Automatically download and install plugin updates</source> + <translation type="unfinished"></translation> + </message> </context> <context> <name>Overlay</name> @@ -7060,30 +7069,188 @@ To upgrade these files to their latest versions, click the button below.</source <translation type="unfinished">Name</translation> </message> <message> - <source>Enabled</source> + <source>Enable</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>PA</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>KeyEvents</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Install a plugin from a local file</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Install plugin...</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unload the currently selected plugin. This will remove it from the plugin list for the current session.</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unload</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>The plugin was installed successfully</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to deactivate all requested features for plugin "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to load plugin "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Whether the positional audio feature of this plugin should be enabled</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>This plugin does not provide support for positional audio</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Whether this plugin should be enabled</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>PluginInstaller</name> + <message> + <source>PluginInstaller</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>You are about to install the plugin listed below. Do you wish to proceed?</source> + <translation type="unfinished"></translation> + </message> + <message> + <source><html><head/><body><p><span style=" font-weight:600;">Name:</span></p></body></html></source> + <translation type="unfinished"></translation> + </message> + <message> + <source><html><head/><body><p><span style=" font-weight:600;">Version:</span></p></body></html></source> + <translation type="unfinished"></translation> + </message> + <message> + <source><html><head/><body><p><span style=" font-weight:600;">Author(s):</span></p></body></html></source> + <translation type="unfinished"></translation> + </message> + <message> + <source><html><head/><body><p><span style=" font-weight:600;">Description:</span></p></body></html></source> + <translation type="unfinished"></translation> + </message> + <message> + <source>&No</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>&Yes</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>The file "%1" is not a valid plugin file!</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Found more than one plugin library for the current OS in "%1" ("%2" and "%3")!</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to find a plugin for the current OS in "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to load plugin "%1" - check the plugin interface!</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to delete old plugin at "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to copy plugin library from "%1" to "%2"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to move plugin library to "%1"</source> <translation type="unfinished"></translation> </message> </context> <context> - <name>Plugins</name> + <name>PluginManager</name> + <message> + <source>%1 lost link</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>%1 linked</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Plugin "%1" encountered a permanent error in positional data gathering</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Non-plugin found in plugin directory: "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Failed at loading manual plugin: %1</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>PluginUpdater</name> + <message> + <source>PluginUpdater</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>The following plugins can be updated.</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Select all</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Plugin</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Download-URL</source> + <translation type="unfinished"></translation> + </message> <message> - <source>Skipping plugin update in debug mode.</source> + <source>Do you want to update the selected plugins?</source> <translation type="unfinished"></translation> </message> <message> - <source>Downloaded new or updated plugin to %1.</source> + <source>Unable to download plugin update for "%1" from "%2" (%3)</source> <translation type="unfinished"></translation> </message> <message> - <source>Failed to install new plugin to %1.</source> + <source>Update for plugin "%1" failed due to too many redirects</source> <translation type="unfinished"></translation> </message> <message> - <source>%1 lost link.</source> + <source>Unable to download plugin update for "%1" from "%2" (HTTP status code %3)</source> <translation type="unfinished"></translation> </message> <message> - <source>%1 linked.</source> + <source>Successfully updated plugin "%1"</source> <translation type="unfinished"></translation> </message> </context> diff --git a/src/mumble/mumble_eo.ts b/src/mumble/mumble_eo.ts index 614e15add..91f0f740a 100644 --- a/src/mumble/mumble_eo.ts +++ b/src/mumble/mumble_eo.ts @@ -3786,6 +3786,10 @@ This field describes the size of an LCD device. The size is given either in pixe <source>User stopped listening to channel</source> <translation type="unfinished"></translation> </message> + <message> + <source>Plugin message</source> + <translation type="unfinished"></translation> + </message> </context> <context> <name>LogConfig</name> @@ -6130,12 +6134,13 @@ Valid actions are: <translation type="unfinished"></translation> </message> <message> - <source>Usage: mumble [options] [<url>] + <source>Usage: mumble [options] [<url> | <plugin_list>] <url> specifies a URL to connect to after startup instead of showing the connection window, and has the following form: mumble://[<username>[:<password>]@]<host>[:<port>][/<channel>[/<subchannel>...]][?version=<x.y.z>] +<plugin_list> is a list of plugin files that shall be installed The version query parameter has to be set in order to invoke the correct client version. It currently defaults to 1.2.0. @@ -6455,10 +6460,6 @@ Valid options are: <translation type="unfinished"></translation> </message> <message> - <source>Download plugin and overlay updates on startup</source> - <translation type="unfinished"></translation> - </message> - <message> <source>Privacy</source> <translation>Privateco</translation> </message> @@ -6483,6 +6484,14 @@ Prevents the client from sending potentially identifying information about the o <source>Hide public server list</source> <translation>Kaŝi la publikan servilaliston</translation> </message> + <message> + <source>Check for plugin updates on startup</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Automatically download and install plugin updates</source> + <translation type="unfinished"></translation> + </message> </context> <context> <name>Overlay</name> @@ -7033,30 +7042,188 @@ To upgrade these files to their latest versions, click the button below.</source <translation>Nomo</translation> </message> <message> - <source>Enabled</source> - <translation type="unfinished">Enŝaltite</translation> + <source>Enable</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>PA</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>KeyEvents</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Install a plugin from a local file</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Install plugin...</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unload the currently selected plugin. This will remove it from the plugin list for the current session.</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unload</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>The plugin was installed successfully</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to deactivate all requested features for plugin "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to load plugin "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Whether the positional audio feature of this plugin should be enabled</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>This plugin does not provide support for positional audio</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Whether this plugin should be enabled</source> + <translation type="unfinished"></translation> </message> </context> <context> - <name>Plugins</name> + <name>PluginInstaller</name> + <message> + <source>PluginInstaller</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>You are about to install the plugin listed below. Do you wish to proceed?</source> + <translation type="unfinished"></translation> + </message> + <message> + <source><html><head/><body><p><span style=" font-weight:600;">Name:</span></p></body></html></source> + <translation type="unfinished"></translation> + </message> + <message> + <source><html><head/><body><p><span style=" font-weight:600;">Version:</span></p></body></html></source> + <translation type="unfinished"></translation> + </message> + <message> + <source><html><head/><body><p><span style=" font-weight:600;">Author(s):</span></p></body></html></source> + <translation type="unfinished"></translation> + </message> + <message> + <source><html><head/><body><p><span style=" font-weight:600;">Description:</span></p></body></html></source> + <translation type="unfinished"></translation> + </message> + <message> + <source>&No</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>&Yes</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>The file "%1" is not a valid plugin file!</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Found more than one plugin library for the current OS in "%1" ("%2" and "%3")!</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to find a plugin for the current OS in "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to load plugin "%1" - check the plugin interface!</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to delete old plugin at "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to copy plugin library from "%1" to "%2"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to move plugin library to "%1"</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>PluginManager</name> + <message> + <source>%1 lost link</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>%1 linked</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Plugin "%1" encountered a permanent error in positional data gathering</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Non-plugin found in plugin directory: "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Failed at loading manual plugin: %1</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>PluginUpdater</name> + <message> + <source>PluginUpdater</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>The following plugins can be updated.</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Select all</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Plugin</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Download-URL</source> + <translation type="unfinished"></translation> + </message> <message> - <source>Skipping plugin update in debug mode.</source> + <source>Do you want to update the selected plugins?</source> <translation type="unfinished"></translation> </message> <message> - <source>Downloaded new or updated plugin to %1.</source> + <source>Unable to download plugin update for "%1" from "%2" (%3)</source> <translation type="unfinished"></translation> </message> <message> - <source>Failed to install new plugin to %1.</source> + <source>Update for plugin "%1" failed due to too many redirects</source> <translation type="unfinished"></translation> </message> <message> - <source>%1 lost link.</source> + <source>Unable to download plugin update for "%1" from "%2" (HTTP status code %3)</source> <translation type="unfinished"></translation> </message> <message> - <source>%1 linked.</source> + <source>Successfully updated plugin "%1"</source> <translation type="unfinished"></translation> </message> </context> diff --git a/src/mumble/mumble_es.ts b/src/mumble/mumble_es.ts index 14299e098..9bfdc9a17 100644 --- a/src/mumble/mumble_es.ts +++ b/src/mumble/mumble_es.ts @@ -3838,6 +3838,10 @@ Este campo describe el tamaño de un dispositivo LCD. El tamaño se da, o bien e <source>User stopped listening to channel</source> <translation>El usuario dejó de escuchar en su canal</translation> </message> + <message> + <source>Plugin message</source> + <translation type="unfinished"></translation> + </message> </context> <context> <name>LogConfig</name> @@ -6191,12 +6195,13 @@ Valid actions are: <translation type="unfinished"></translation> </message> <message> - <source>Usage: mumble [options] [<url>] + <source>Usage: mumble [options] [<url> | <plugin_list>] <url> specifies a URL to connect to after startup instead of showing the connection window, and has the following form: mumble://[<username>[:<password>]@]<host>[:<port>][/<channel>[/<subchannel>...]][?version=<x.y.z>] +<plugin_list> is a list of plugin files that shall be installed The version query parameter has to be set in order to invoke the correct client version. It currently defaults to 1.2.0. @@ -6516,10 +6521,6 @@ Valid options are: <translation>Reconectar al último servidor al inicio</translation> </message> <message> - <source>Download plugin and overlay updates on startup</source> - <translation>Descargar actualizaciones de los complementos y la superposición al inicio</translation> - </message> - <message> <source>Privacy</source> <translation>Privacidad</translation> </message> @@ -6545,6 +6546,14 @@ Impide que el cliente envíe información potencialmente identificable sobre el <source>Hide public server list</source> <translation>Esconder la lista de servidores públicos</translation> </message> + <message> + <source>Check for plugin updates on startup</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Automatically download and install plugin updates</source> + <translation type="unfinished"></translation> + </message> </context> <context> <name>Overlay</name> @@ -7099,31 +7108,189 @@ Para actualizar estos ficheros a la última versión, haga clic en el botón inf <translation>Nombre</translation> </message> <message> - <source>Enabled</source> - <translation>Habilitado</translation> + <source>Enable</source> + <translation type="unfinished">Habilitar</translation> + </message> + <message> + <source>PA</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>KeyEvents</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Install a plugin from a local file</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Install plugin...</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unload the currently selected plugin. This will remove it from the plugin list for the current session.</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unload</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>The plugin was installed successfully</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to deactivate all requested features for plugin "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to load plugin "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Whether the positional audio feature of this plugin should be enabled</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>This plugin does not provide support for positional audio</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Whether this plugin should be enabled</source> + <translation type="unfinished"></translation> </message> </context> <context> - <name>Plugins</name> + <name>PluginInstaller</name> <message> - <source>Skipping plugin update in debug mode.</source> - <translation>Omitiendo la actualización de los complementos en el modo de depuración.</translation> + <source>PluginInstaller</source> + <translation type="unfinished"></translation> </message> <message> - <source>Downloaded new or updated plugin to %1.</source> - <translation>Se ha descargado un complemento nuevo o actualizado para %1.</translation> + <source>You are about to install the plugin listed below. Do you wish to proceed?</source> + <translation type="unfinished"></translation> </message> <message> - <source>Failed to install new plugin to %1.</source> - <translation>No se pudo instalar un nuevo complemento para %1.</translation> + <source><html><head/><body><p><span style=" font-weight:600;">Name:</span></p></body></html></source> + <translation type="unfinished"></translation> </message> <message> - <source>%1 lost link.</source> - <translation>%1 perdió el vínculo.</translation> + <source><html><head/><body><p><span style=" font-weight:600;">Version:</span></p></body></html></source> + <translation type="unfinished"></translation> + </message> + <message> + <source><html><head/><body><p><span style=" font-weight:600;">Author(s):</span></p></body></html></source> + <translation type="unfinished"></translation> + </message> + <message> + <source><html><head/><body><p><span style=" font-weight:600;">Description:</span></p></body></html></source> + <translation type="unfinished"></translation> + </message> + <message> + <source>&No</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>&Yes</source> + <translation type="unfinished"></translation> </message> <message> - <source>%1 linked.</source> - <translation>%1 vinculado.</translation> + <source>The file "%1" is not a valid plugin file!</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Found more than one plugin library for the current OS in "%1" ("%2" and "%3")!</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to find a plugin for the current OS in "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to load plugin "%1" - check the plugin interface!</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to delete old plugin at "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to copy plugin library from "%1" to "%2"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to move plugin library to "%1"</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>PluginManager</name> + <message> + <source>%1 lost link</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>%1 linked</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Plugin "%1" encountered a permanent error in positional data gathering</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Non-plugin found in plugin directory: "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Failed at loading manual plugin: %1</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>PluginUpdater</name> + <message> + <source>PluginUpdater</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>The following plugins can be updated.</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Select all</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Plugin</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Download-URL</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Do you want to update the selected plugins?</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to download plugin update for "%1" from "%2" (%3)</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Update for plugin "%1" failed due to too many redirects</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to download plugin update for "%1" from "%2" (HTTP status code %3)</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Successfully updated plugin "%1"</source> + <translation type="unfinished"></translation> </message> </context> <context> diff --git a/src/mumble/mumble_et.ts b/src/mumble/mumble_et.ts index 7277cff3d..8170471b1 100644 --- a/src/mumble/mumble_et.ts +++ b/src/mumble/mumble_et.ts @@ -3779,6 +3779,10 @@ This field describes the size of an LCD device. The size is given either in pixe <source>User stopped listening to channel</source> <translation type="unfinished"></translation> </message> + <message> + <source>Plugin message</source> + <translation type="unfinished"></translation> + </message> </context> <context> <name>LogConfig</name> @@ -6122,12 +6126,13 @@ Valid actions are: <translation type="unfinished"></translation> </message> <message> - <source>Usage: mumble [options] [<url>] + <source>Usage: mumble [options] [<url> | <plugin_list>] <url> specifies a URL to connect to after startup instead of showing the connection window, and has the following form: mumble://[<username>[:<password>]@]<host>[:<port>][/<channel>[/<subchannel>...]][?version=<x.y.z>] +<plugin_list> is a list of plugin files that shall be installed The version query parameter has to be set in order to invoke the correct client version. It currently defaults to 1.2.0. @@ -6447,10 +6452,6 @@ Valid options are: <translation type="unfinished"></translation> </message> <message> - <source>Download plugin and overlay updates on startup</source> - <translation type="unfinished"></translation> - </message> - <message> <source>Privacy</source> <translation>Privaatsus</translation> </message> @@ -6475,6 +6476,14 @@ Prevents the client from sending potentially identifying information about the o <source>Hide public server list</source> <translation type="unfinished"></translation> </message> + <message> + <source>Check for plugin updates on startup</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Automatically download and install plugin updates</source> + <translation type="unfinished"></translation> + </message> </context> <context> <name>Overlay</name> @@ -7025,31 +7034,189 @@ To upgrade these files to their latest versions, click the button below.</source <translation>Nimi</translation> </message> <message> - <source>Enabled</source> - <translation>Lubatud</translation> + <source>Enable</source> + <translation type="unfinished">Luba</translation> + </message> + <message> + <source>PA</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>KeyEvents</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Install a plugin from a local file</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Install plugin...</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unload the currently selected plugin. This will remove it from the plugin list for the current session.</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unload</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>The plugin was installed successfully</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to deactivate all requested features for plugin "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to load plugin "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Whether the positional audio feature of this plugin should be enabled</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>This plugin does not provide support for positional audio</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Whether this plugin should be enabled</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>PluginInstaller</name> + <message> + <source>PluginInstaller</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>You are about to install the plugin listed below. Do you wish to proceed?</source> + <translation type="unfinished"></translation> + </message> + <message> + <source><html><head/><body><p><span style=" font-weight:600;">Name:</span></p></body></html></source> + <translation type="unfinished"></translation> + </message> + <message> + <source><html><head/><body><p><span style=" font-weight:600;">Version:</span></p></body></html></source> + <translation type="unfinished"></translation> + </message> + <message> + <source><html><head/><body><p><span style=" font-weight:600;">Author(s):</span></p></body></html></source> + <translation type="unfinished"></translation> + </message> + <message> + <source><html><head/><body><p><span style=" font-weight:600;">Description:</span></p></body></html></source> + <translation type="unfinished"></translation> + </message> + <message> + <source>&No</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>&Yes</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>The file "%1" is not a valid plugin file!</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Found more than one plugin library for the current OS in "%1" ("%2" and "%3")!</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to find a plugin for the current OS in "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to load plugin "%1" - check the plugin interface!</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to delete old plugin at "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to copy plugin library from "%1" to "%2"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to move plugin library to "%1"</source> + <translation type="unfinished"></translation> </message> </context> <context> - <name>Plugins</name> + <name>PluginManager</name> <message> - <source>Skipping plugin update in debug mode.</source> + <source>%1 lost link</source> <translation type="unfinished"></translation> </message> <message> - <source>Downloaded new or updated plugin to %1.</source> + <source>%1 linked</source> <translation type="unfinished"></translation> </message> <message> - <source>Failed to install new plugin to %1.</source> + <source>Plugin "%1" encountered a permanent error in positional data gathering</source> <translation type="unfinished"></translation> </message> <message> - <source>%1 lost link.</source> + <source>Non-plugin found in plugin directory: "%1"</source> <translation type="unfinished"></translation> </message> <message> - <source>%1 linked.</source> - <translation>%1 lingitud.</translation> + <source>Failed at loading manual plugin: %1</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>PluginUpdater</name> + <message> + <source>PluginUpdater</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>The following plugins can be updated.</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Select all</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Plugin</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Download-URL</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Do you want to update the selected plugins?</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to download plugin update for "%1" from "%2" (%3)</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Update for plugin "%1" failed due to too many redirects</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to download plugin update for "%1" from "%2" (HTTP status code %3)</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Successfully updated plugin "%1"</source> + <translation type="unfinished"></translation> </message> </context> <context> diff --git a/src/mumble/mumble_eu.ts b/src/mumble/mumble_eu.ts index f7038bb97..21a710694 100644 --- a/src/mumble/mumble_eu.ts +++ b/src/mumble/mumble_eu.ts @@ -3796,6 +3796,10 @@ This field describes the size of an LCD device. The size is given either in pixe <source>User stopped listening to channel</source> <translation type="unfinished"></translation> </message> + <message> + <source>Plugin message</source> + <translation type="unfinished"></translation> + </message> </context> <context> <name>LogConfig</name> @@ -6141,12 +6145,13 @@ Valid actions are: <translation type="unfinished"></translation> </message> <message> - <source>Usage: mumble [options] [<url>] + <source>Usage: mumble [options] [<url> | <plugin_list>] <url> specifies a URL to connect to after startup instead of showing the connection window, and has the following form: mumble://[<username>[:<password>]@]<host>[:<port>][/<channel>[/<subchannel>...]][?version=<x.y.z>] +<plugin_list> is a list of plugin files that shall be installed The version query parameter has to be set in order to invoke the correct client version. It currently defaults to 1.2.0. @@ -6466,10 +6471,6 @@ Valid options are: <translation>Birkonektatu azken zerbitzarira hastean</translation> </message> <message> - <source>Download plugin and overlay updates on startup</source> - <translation type="unfinished"></translation> - </message> - <message> <source>Privacy</source> <translation type="unfinished"></translation> </message> @@ -6494,6 +6495,14 @@ Prevents the client from sending potentially identifying information about the o <source>Hide public server list</source> <translation type="unfinished"></translation> </message> + <message> + <source>Check for plugin updates on startup</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Automatically download and install plugin updates</source> + <translation type="unfinished"></translation> + </message> </context> <context> <name>Overlay</name> @@ -7044,31 +7053,189 @@ To upgrade these files to their latest versions, click the button below.</source <translation>Izena</translation> </message> <message> - <source>Enabled</source> - <translation>Gaituta</translation> + <source>Enable</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>PA</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>KeyEvents</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Install a plugin from a local file</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Install plugin...</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unload the currently selected plugin. This will remove it from the plugin list for the current session.</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unload</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>The plugin was installed successfully</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to deactivate all requested features for plugin "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to load plugin "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Whether the positional audio feature of this plugin should be enabled</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>This plugin does not provide support for positional audio</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Whether this plugin should be enabled</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>PluginInstaller</name> + <message> + <source>PluginInstaller</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>You are about to install the plugin listed below. Do you wish to proceed?</source> + <translation type="unfinished"></translation> + </message> + <message> + <source><html><head/><body><p><span style=" font-weight:600;">Name:</span></p></body></html></source> + <translation type="unfinished"></translation> + </message> + <message> + <source><html><head/><body><p><span style=" font-weight:600;">Version:</span></p></body></html></source> + <translation type="unfinished"></translation> + </message> + <message> + <source><html><head/><body><p><span style=" font-weight:600;">Author(s):</span></p></body></html></source> + <translation type="unfinished"></translation> + </message> + <message> + <source><html><head/><body><p><span style=" font-weight:600;">Description:</span></p></body></html></source> + <translation type="unfinished"></translation> + </message> + <message> + <source>&No</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>&Yes</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>The file "%1" is not a valid plugin file!</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Found more than one plugin library for the current OS in "%1" ("%2" and "%3")!</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to find a plugin for the current OS in "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to load plugin "%1" - check the plugin interface!</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to delete old plugin at "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to copy plugin library from "%1" to "%2"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to move plugin library to "%1"</source> + <translation type="unfinished"></translation> </message> </context> <context> - <name>Plugins</name> + <name>PluginManager</name> <message> - <source>Skipping plugin update in debug mode.</source> + <source>%1 lost link</source> <translation type="unfinished"></translation> </message> <message> - <source>Downloaded new or updated plugin to %1.</source> - <translation>Eguneratutako edo berria den gehigarria deskargatuta %1 -era.</translation> + <source>%1 linked</source> + <translation type="unfinished"></translation> </message> <message> - <source>Failed to install new plugin to %1.</source> - <translation>%1-era gehigarri berria instalatzean errorea.</translation> + <source>Plugin "%1" encountered a permanent error in positional data gathering</source> + <translation type="unfinished"></translation> </message> <message> - <source>%1 lost link.</source> - <translation>%1 lotura galdua.</translation> + <source>Non-plugin found in plugin directory: "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Failed at loading manual plugin: %1</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>PluginUpdater</name> + <message> + <source>PluginUpdater</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>The following plugins can be updated.</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Select all</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Plugin</source> + <translation type="unfinished"></translation> </message> <message> - <source>%1 linked.</source> - <translation>%1 lotuta.</translation> + <source>Download-URL</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Do you want to update the selected plugins?</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to download plugin update for "%1" from "%2" (%3)</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Update for plugin "%1" failed due to too many redirects</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to download plugin update for "%1" from "%2" (HTTP status code %3)</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Successfully updated plugin "%1"</source> + <translation type="unfinished"></translation> </message> </context> <context> diff --git a/src/mumble/mumble_fa_IR.ts b/src/mumble/mumble_fa_IR.ts index c139066bd..1d1e9b2f4 100644 --- a/src/mumble/mumble_fa_IR.ts +++ b/src/mumble/mumble_fa_IR.ts @@ -3777,6 +3777,10 @@ This field describes the size of an LCD device. The size is given either in pixe <source>User stopped listening to channel</source> <translation type="unfinished"></translation> </message> + <message> + <source>Plugin message</source> + <translation type="unfinished"></translation> + </message> </context> <context> <name>LogConfig</name> @@ -6120,12 +6124,13 @@ Valid actions are: <translation type="unfinished"></translation> </message> <message> - <source>Usage: mumble [options] [<url>] + <source>Usage: mumble [options] [<url> | <plugin_list>] <url> specifies a URL to connect to after startup instead of showing the connection window, and has the following form: mumble://[<username>[:<password>]@]<host>[:<port>][/<channel>[/<subchannel>...]][?version=<x.y.z>] +<plugin_list> is a list of plugin files that shall be installed The version query parameter has to be set in order to invoke the correct client version. It currently defaults to 1.2.0. @@ -6445,10 +6450,6 @@ Valid options are: <translation type="unfinished"></translation> </message> <message> - <source>Download plugin and overlay updates on startup</source> - <translation type="unfinished"></translation> - </message> - <message> <source>Privacy</source> <translation type="unfinished"></translation> </message> @@ -6473,6 +6474,14 @@ Prevents the client from sending potentially identifying information about the o <source>Hide public server list</source> <translation type="unfinished"></translation> </message> + <message> + <source>Check for plugin updates on startup</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Automatically download and install plugin updates</source> + <translation type="unfinished"></translation> + </message> </context> <context> <name>Overlay</name> @@ -7023,30 +7032,188 @@ To upgrade these files to their latest versions, click the button below.</source <translation type="unfinished">نام</translation> </message> <message> - <source>Enabled</source> + <source>Enable</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>PA</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>KeyEvents</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Install a plugin from a local file</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Install plugin...</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unload the currently selected plugin. This will remove it from the plugin list for the current session.</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unload</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>The plugin was installed successfully</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to deactivate all requested features for plugin "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to load plugin "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Whether the positional audio feature of this plugin should be enabled</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>This plugin does not provide support for positional audio</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Whether this plugin should be enabled</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>PluginInstaller</name> + <message> + <source>PluginInstaller</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>You are about to install the plugin listed below. Do you wish to proceed?</source> + <translation type="unfinished"></translation> + </message> + <message> + <source><html><head/><body><p><span style=" font-weight:600;">Name:</span></p></body></html></source> + <translation type="unfinished"></translation> + </message> + <message> + <source><html><head/><body><p><span style=" font-weight:600;">Version:</span></p></body></html></source> + <translation type="unfinished"></translation> + </message> + <message> + <source><html><head/><body><p><span style=" font-weight:600;">Author(s):</span></p></body></html></source> + <translation type="unfinished"></translation> + </message> + <message> + <source><html><head/><body><p><span style=" font-weight:600;">Description:</span></p></body></html></source> + <translation type="unfinished"></translation> + </message> + <message> + <source>&No</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>&Yes</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>The file "%1" is not a valid plugin file!</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Found more than one plugin library for the current OS in "%1" ("%2" and "%3")!</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to find a plugin for the current OS in "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to load plugin "%1" - check the plugin interface!</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to delete old plugin at "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to copy plugin library from "%1" to "%2"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to move plugin library to "%1"</source> <translation type="unfinished"></translation> </message> </context> <context> - <name>Plugins</name> + <name>PluginManager</name> + <message> + <source>%1 lost link</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>%1 linked</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Plugin "%1" encountered a permanent error in positional data gathering</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Non-plugin found in plugin directory: "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Failed at loading manual plugin: %1</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>PluginUpdater</name> + <message> + <source>PluginUpdater</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>The following plugins can be updated.</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Select all</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Plugin</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Download-URL</source> + <translation type="unfinished"></translation> + </message> <message> - <source>Skipping plugin update in debug mode.</source> + <source>Do you want to update the selected plugins?</source> <translation type="unfinished"></translation> </message> <message> - <source>Downloaded new or updated plugin to %1.</source> + <source>Unable to download plugin update for "%1" from "%2" (%3)</source> <translation type="unfinished"></translation> </message> <message> - <source>Failed to install new plugin to %1.</source> + <source>Update for plugin "%1" failed due to too many redirects</source> <translation type="unfinished"></translation> </message> <message> - <source>%1 lost link.</source> + <source>Unable to download plugin update for "%1" from "%2" (HTTP status code %3)</source> <translation type="unfinished"></translation> </message> <message> - <source>%1 linked.</source> + <source>Successfully updated plugin "%1"</source> <translation type="unfinished"></translation> </message> </context> diff --git a/src/mumble/mumble_fi.ts b/src/mumble/mumble_fi.ts index 252b08ab2..8948e902c 100644 --- a/src/mumble/mumble_fi.ts +++ b/src/mumble/mumble_fi.ts @@ -3838,6 +3838,10 @@ Kenttä kuvaa LCD-laitteen koon. Koko annetaan joko pikseleinä (graafinen LCD) <source>User stopped listening to channel</source> <translation>Käyttäjä lopetti kanavan kuuntelun</translation> </message> + <message> + <source>Plugin message</source> + <translation type="unfinished"></translation> + </message> </context> <context> <name>LogConfig</name> @@ -6231,12 +6235,13 @@ Päteviä valintoja ovat: </translation> </message> <message> - <source>Usage: mumble [options] [<url>] + <source>Usage: mumble [options] [<url> | <plugin_list>] <url> specifies a URL to connect to after startup instead of showing the connection window, and has the following form: mumble://[<username>[:<password>]@]<host>[:<port>][/<channel>[/<subchannel>...]][?version=<x.y.z>] +<plugin_list> is a list of plugin files that shall be installed The version query parameter has to be set in order to invoke the correct client version. It currently defaults to 1.2.0. @@ -6556,10 +6561,6 @@ Valid options are: <translation>Yhdistä viimeisimpään palvelimeen käynnistymisen yhteydessä</translation> </message> <message> - <source>Download plugin and overlay updates on startup</source> - <translation>Lataa liitännäisten ja overlayn päivitykset ohjelman käynnistymisen yhteydessä</translation> - </message> - <message> <source>Privacy</source> <translation>Yksityisyys</translation> </message> @@ -6585,6 +6586,14 @@ Estää mahdollisesti tunnistamista helpottavien tietojen, koskien käyttöjärj <source>Hide public server list</source> <translation>Piilota julkisten palvelinten lista</translation> </message> + <message> + <source>Check for plugin updates on startup</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Automatically download and install plugin updates</source> + <translation type="unfinished"></translation> + </message> </context> <context> <name>Overlay</name> @@ -7139,31 +7148,189 @@ Paina alapuolen napista päivittääksesi Overlayn tiedostot viimeisimpään ver <translation>Nimi</translation> </message> <message> - <source>Enabled</source> - <translation>Käytössä</translation> + <source>Enable</source> + <translation type="unfinished">Käytä</translation> + </message> + <message> + <source>PA</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>KeyEvents</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Install a plugin from a local file</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Install plugin...</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unload the currently selected plugin. This will remove it from the plugin list for the current session.</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unload</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>The plugin was installed successfully</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to deactivate all requested features for plugin "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to load plugin "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Whether the positional audio feature of this plugin should be enabled</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>This plugin does not provide support for positional audio</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Whether this plugin should be enabled</source> + <translation type="unfinished"></translation> </message> </context> <context> - <name>Plugins</name> + <name>PluginInstaller</name> <message> - <source>Skipping plugin update in debug mode.</source> - <translation>Liitännäispäivitys ohitetaan debug-tilassa.</translation> + <source>PluginInstaller</source> + <translation type="unfinished"></translation> </message> <message> - <source>Downloaded new or updated plugin to %1.</source> - <translation>Ladattiin uusi tai päivitettiin liitännäinnen %1.</translation> + <source>You are about to install the plugin listed below. Do you wish to proceed?</source> + <translation type="unfinished"></translation> </message> <message> - <source>Failed to install new plugin to %1.</source> - <translation>Uuden liitännäisen asennus epäonnistui %1.</translation> + <source><html><head/><body><p><span style=" font-weight:600;">Name:</span></p></body></html></source> + <translation type="unfinished"></translation> </message> <message> - <source>%1 lost link.</source> - <translation>%1 yhteys kadotettu.</translation> + <source><html><head/><body><p><span style=" font-weight:600;">Version:</span></p></body></html></source> + <translation type="unfinished"></translation> + </message> + <message> + <source><html><head/><body><p><span style=" font-weight:600;">Author(s):</span></p></body></html></source> + <translation type="unfinished"></translation> + </message> + <message> + <source><html><head/><body><p><span style=" font-weight:600;">Description:</span></p></body></html></source> + <translation type="unfinished"></translation> + </message> + <message> + <source>&No</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>&Yes</source> + <translation type="unfinished"></translation> </message> <message> - <source>%1 linked.</source> - <translation>%1 yhdistetty.</translation> + <source>The file "%1" is not a valid plugin file!</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Found more than one plugin library for the current OS in "%1" ("%2" and "%3")!</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to find a plugin for the current OS in "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to load plugin "%1" - check the plugin interface!</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to delete old plugin at "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to copy plugin library from "%1" to "%2"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to move plugin library to "%1"</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>PluginManager</name> + <message> + <source>%1 lost link</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>%1 linked</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Plugin "%1" encountered a permanent error in positional data gathering</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Non-plugin found in plugin directory: "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Failed at loading manual plugin: %1</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>PluginUpdater</name> + <message> + <source>PluginUpdater</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>The following plugins can be updated.</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Select all</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Plugin</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Download-URL</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Do you want to update the selected plugins?</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to download plugin update for "%1" from "%2" (%3)</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Update for plugin "%1" failed due to too many redirects</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to download plugin update for "%1" from "%2" (HTTP status code %3)</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Successfully updated plugin "%1"</source> + <translation type="unfinished"></translation> </message> </context> <context> diff --git a/src/mumble/mumble_fr.ts b/src/mumble/mumble_fr.ts index f559e2ec4..69846001c 100644 --- a/src/mumble/mumble_fr.ts +++ b/src/mumble/mumble_fr.ts @@ -3837,6 +3837,10 @@ This field describes the size of an LCD device. The size is given either in pixe <source>User stopped listening to channel</source> <translation>L'utilisateur a cessé d'écouter le salon</translation> </message> + <message> + <source>Plugin message</source> + <translation type="unfinished"></translation> + </message> </context> <context> <name>LogConfig</name> @@ -6191,12 +6195,13 @@ Valid actions are: <translation type="unfinished"></translation> </message> <message> - <source>Usage: mumble [options] [<url>] + <source>Usage: mumble [options] [<url> | <plugin_list>] <url> specifies a URL to connect to after startup instead of showing the connection window, and has the following form: mumble://[<username>[:<password>]@]<host>[:<port>][/<channel>[/<subchannel>...]][?version=<x.y.z>] +<plugin_list> is a list of plugin files that shall be installed The version query parameter has to be set in order to invoke the correct client version. It currently defaults to 1.2.0. @@ -6516,10 +6521,6 @@ Valid options are: <translation>Se reconnecter au dernier serveur utilisé au démarrage</translation> </message> <message> - <source>Download plugin and overlay updates on startup</source> - <translation>Télécharger les mises à jour des plugins et de l'overlay au démarrage</translation> - </message> - <message> <source>Privacy</source> <translation>Confidentialité</translation> </message> @@ -6545,6 +6546,14 @@ Empêche le client d'envoyer des informations pouvant identifier le systèm <source>Hide public server list</source> <translation>Cacher la liste des serveurs publics</translation> </message> + <message> + <source>Check for plugin updates on startup</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Automatically download and install plugin updates</source> + <translation type="unfinished"></translation> + </message> </context> <context> <name>Overlay</name> @@ -7099,31 +7108,189 @@ Pour mettre à jour l'overlay, cliquez sur le bouton ci-dessous.</translati <translation>Nom</translation> </message> <message> - <source>Enabled</source> - <translation>Activé</translation> + <source>Enable</source> + <translation type="unfinished">Activer</translation> + </message> + <message> + <source>PA</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>KeyEvents</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Install a plugin from a local file</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Install plugin...</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unload the currently selected plugin. This will remove it from the plugin list for the current session.</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unload</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>The plugin was installed successfully</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to deactivate all requested features for plugin "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to load plugin "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Whether the positional audio feature of this plugin should be enabled</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>This plugin does not provide support for positional audio</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Whether this plugin should be enabled</source> + <translation type="unfinished"></translation> </message> </context> <context> - <name>Plugins</name> + <name>PluginInstaller</name> <message> - <source>Skipping plugin update in debug mode.</source> - <translation>Passe la mise à jour des plugins en mode débogage.</translation> + <source>PluginInstaller</source> + <translation type="unfinished"></translation> </message> <message> - <source>Downloaded new or updated plugin to %1.</source> - <translation>Téléchargé le nouveau plugin ou mis à jour vers %1.</translation> + <source>You are about to install the plugin listed below. Do you wish to proceed?</source> + <translation type="unfinished"></translation> </message> <message> - <source>Failed to install new plugin to %1.</source> - <translation>Échec de l'installation du nouveau plugin dans %1.</translation> + <source><html><head/><body><p><span style=" font-weight:600;">Name:</span></p></body></html></source> + <translation type="unfinished"></translation> </message> <message> - <source>%1 lost link.</source> - <translation>%1 est désactivé.</translation> + <source><html><head/><body><p><span style=" font-weight:600;">Version:</span></p></body></html></source> + <translation type="unfinished"></translation> + </message> + <message> + <source><html><head/><body><p><span style=" font-weight:600;">Author(s):</span></p></body></html></source> + <translation type="unfinished"></translation> + </message> + <message> + <source><html><head/><body><p><span style=" font-weight:600;">Description:</span></p></body></html></source> + <translation type="unfinished"></translation> + </message> + <message> + <source>&No</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>&Yes</source> + <translation type="unfinished"></translation> </message> <message> - <source>%1 linked.</source> - <translation>%1 est activé.</translation> + <source>The file "%1" is not a valid plugin file!</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Found more than one plugin library for the current OS in "%1" ("%2" and "%3")!</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to find a plugin for the current OS in "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to load plugin "%1" - check the plugin interface!</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to delete old plugin at "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to copy plugin library from "%1" to "%2"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to move plugin library to "%1"</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>PluginManager</name> + <message> + <source>%1 lost link</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>%1 linked</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Plugin "%1" encountered a permanent error in positional data gathering</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Non-plugin found in plugin directory: "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Failed at loading manual plugin: %1</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>PluginUpdater</name> + <message> + <source>PluginUpdater</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>The following plugins can be updated.</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Select all</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Plugin</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Download-URL</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Do you want to update the selected plugins?</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to download plugin update for "%1" from "%2" (%3)</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Update for plugin "%1" failed due to too many redirects</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to download plugin update for "%1" from "%2" (HTTP status code %3)</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Successfully updated plugin "%1"</source> + <translation type="unfinished"></translation> </message> </context> <context> diff --git a/src/mumble/mumble_gl.ts b/src/mumble/mumble_gl.ts index a18a12f93..efb2ebbd0 100644 --- a/src/mumble/mumble_gl.ts +++ b/src/mumble/mumble_gl.ts @@ -3780,6 +3780,10 @@ This field describes the size of an LCD device. The size is given either in pixe <source>User stopped listening to channel</source> <translation type="unfinished"></translation> </message> + <message> + <source>Plugin message</source> + <translation type="unfinished"></translation> + </message> </context> <context> <name>LogConfig</name> @@ -6123,12 +6127,13 @@ Valid actions are: <translation type="unfinished"></translation> </message> <message> - <source>Usage: mumble [options] [<url>] + <source>Usage: mumble [options] [<url> | <plugin_list>] <url> specifies a URL to connect to after startup instead of showing the connection window, and has the following form: mumble://[<username>[:<password>]@]<host>[:<port>][/<channel>[/<subchannel>...]][?version=<x.y.z>] +<plugin_list> is a list of plugin files that shall be installed The version query parameter has to be set in order to invoke the correct client version. It currently defaults to 1.2.0. @@ -6448,10 +6453,6 @@ Valid options are: <translation type="unfinished"></translation> </message> <message> - <source>Download plugin and overlay updates on startup</source> - <translation type="unfinished"></translation> - </message> - <message> <source>Privacy</source> <translation type="unfinished"></translation> </message> @@ -6476,6 +6477,14 @@ Prevents the client from sending potentially identifying information about the o <source>Hide public server list</source> <translation type="unfinished"></translation> </message> + <message> + <source>Check for plugin updates on startup</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Automatically download and install plugin updates</source> + <translation type="unfinished"></translation> + </message> </context> <context> <name>Overlay</name> @@ -7026,30 +7035,188 @@ To upgrade these files to their latest versions, click the button below.</source <translation type="unfinished">Nome</translation> </message> <message> - <source>Enabled</source> + <source>Enable</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>PA</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>KeyEvents</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Install a plugin from a local file</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Install plugin...</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unload the currently selected plugin. This will remove it from the plugin list for the current session.</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unload</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>The plugin was installed successfully</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to deactivate all requested features for plugin "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to load plugin "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Whether the positional audio feature of this plugin should be enabled</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>This plugin does not provide support for positional audio</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Whether this plugin should be enabled</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>PluginInstaller</name> + <message> + <source>PluginInstaller</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>You are about to install the plugin listed below. Do you wish to proceed?</source> + <translation type="unfinished"></translation> + </message> + <message> + <source><html><head/><body><p><span style=" font-weight:600;">Name:</span></p></body></html></source> + <translation type="unfinished"></translation> + </message> + <message> + <source><html><head/><body><p><span style=" font-weight:600;">Version:</span></p></body></html></source> + <translation type="unfinished"></translation> + </message> + <message> + <source><html><head/><body><p><span style=" font-weight:600;">Author(s):</span></p></body></html></source> + <translation type="unfinished"></translation> + </message> + <message> + <source><html><head/><body><p><span style=" font-weight:600;">Description:</span></p></body></html></source> + <translation type="unfinished"></translation> + </message> + <message> + <source>&No</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>&Yes</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>The file "%1" is not a valid plugin file!</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Found more than one plugin library for the current OS in "%1" ("%2" and "%3")!</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to find a plugin for the current OS in "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to load plugin "%1" - check the plugin interface!</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to delete old plugin at "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to copy plugin library from "%1" to "%2"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to move plugin library to "%1"</source> <translation type="unfinished"></translation> </message> </context> <context> - <name>Plugins</name> + <name>PluginManager</name> + <message> + <source>%1 lost link</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>%1 linked</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Plugin "%1" encountered a permanent error in positional data gathering</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Non-plugin found in plugin directory: "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Failed at loading manual plugin: %1</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>PluginUpdater</name> + <message> + <source>PluginUpdater</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>The following plugins can be updated.</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Select all</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Plugin</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Download-URL</source> + <translation type="unfinished"></translation> + </message> <message> - <source>Skipping plugin update in debug mode.</source> + <source>Do you want to update the selected plugins?</source> <translation type="unfinished"></translation> </message> <message> - <source>Downloaded new or updated plugin to %1.</source> + <source>Unable to download plugin update for "%1" from "%2" (%3)</source> <translation type="unfinished"></translation> </message> <message> - <source>Failed to install new plugin to %1.</source> + <source>Update for plugin "%1" failed due to too many redirects</source> <translation type="unfinished"></translation> </message> <message> - <source>%1 lost link.</source> + <source>Unable to download plugin update for "%1" from "%2" (HTTP status code %3)</source> <translation type="unfinished"></translation> </message> <message> - <source>%1 linked.</source> + <source>Successfully updated plugin "%1"</source> <translation type="unfinished"></translation> </message> </context> diff --git a/src/mumble/mumble_he.ts b/src/mumble/mumble_he.ts index df3fd72d2..0e7680463 100644 --- a/src/mumble/mumble_he.ts +++ b/src/mumble/mumble_he.ts @@ -3829,6 +3829,10 @@ This field describes the size of an LCD device. The size is given either in pixe <source>User stopped listening to channel</source> <translation type="unfinished"></translation> </message> + <message> + <source>Plugin message</source> + <translation type="unfinished"></translation> + </message> </context> <context> <name>LogConfig</name> @@ -6174,12 +6178,13 @@ Valid actions are: <translation type="unfinished"></translation> </message> <message> - <source>Usage: mumble [options] [<url>] + <source>Usage: mumble [options] [<url> | <plugin_list>] <url> specifies a URL to connect to after startup instead of showing the connection window, and has the following form: mumble://[<username>[:<password>]@]<host>[:<port>][/<channel>[/<subchannel>...]][?version=<x.y.z>] +<plugin_list> is a list of plugin files that shall be installed The version query parameter has to be set in order to invoke the correct client version. It currently defaults to 1.2.0. @@ -6499,10 +6504,6 @@ Valid options are: <translation>התחבר שוב אל שרת אחרון בעת הפעלה</translation> </message> <message> - <source>Download plugin and overlay updates on startup</source> - <translation>הורד עדכונים עבור תוספים וממשק-המשחק</translation> - </message> - <message> <source>Privacy</source> <translation type="unfinished"></translation> </message> @@ -6527,6 +6528,14 @@ Prevents the client from sending potentially identifying information about the o <source>Hide public server list</source> <translation type="unfinished"></translation> </message> + <message> + <source>Check for plugin updates on startup</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Automatically download and install plugin updates</source> + <translation type="unfinished"></translation> + </message> </context> <context> <name>Overlay</name> @@ -7081,32 +7090,189 @@ To upgrade these files to their latest versions, click the button below.</source <translation>שם</translation> </message> <message> - <source>Enabled</source> - <translation>מאופשרת</translation> + <source>Enable</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>PA</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>KeyEvents</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Install a plugin from a local file</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Install plugin...</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unload the currently selected plugin. This will remove it from the plugin list for the current session.</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unload</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>The plugin was installed successfully</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to deactivate all requested features for plugin "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to load plugin "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Whether the positional audio feature of this plugin should be enabled</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>This plugin does not provide support for positional audio</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Whether this plugin should be enabled</source> + <translation type="unfinished"></translation> </message> </context> <context> - <name>Plugins</name> + <name>PluginInstaller</name> <message> - <source>Skipping plugin update in debug mode.</source> - <translation>מדלג על עדכון תוספים במצב ניפוי-שגיאות.</translation> + <source>PluginInstaller</source> + <translation type="unfinished"></translation> </message> <message> - <source>Downloaded new or updated plugin to %1.</source> - <translation>מוריד או מעדכן תוסף עבור %1. -</translation> + <source>You are about to install the plugin listed below. Do you wish to proceed?</source> + <translation type="unfinished"></translation> + </message> + <message> + <source><html><head/><body><p><span style=" font-weight:600;">Name:</span></p></body></html></source> + <translation type="unfinished"></translation> + </message> + <message> + <source><html><head/><body><p><span style=" font-weight:600;">Version:</span></p></body></html></source> + <translation type="unfinished"></translation> + </message> + <message> + <source><html><head/><body><p><span style=" font-weight:600;">Author(s):</span></p></body></html></source> + <translation type="unfinished"></translation> + </message> + <message> + <source><html><head/><body><p><span style=" font-weight:600;">Description:</span></p></body></html></source> + <translation type="unfinished"></translation> + </message> + <message> + <source>&No</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>&Yes</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>The file "%1" is not a valid plugin file!</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Found more than one plugin library for the current OS in "%1" ("%2" and "%3")!</source> + <translation type="unfinished"></translation> </message> <message> - <source>Failed to install new plugin to %1.</source> - <translation>נכשל בהתקנת תוסף חדש ל-%1.</translation> + <source>Unable to find a plugin for the current OS in "%1"</source> + <translation type="unfinished"></translation> </message> <message> - <source>%1 lost link.</source> - <translation>%1 איבד קישור.</translation> + <source>Unable to load plugin "%1" - check the plugin interface!</source> + <translation type="unfinished"></translation> </message> <message> - <source>%1 linked.</source> - <translation>%1 קושר.</translation> + <source>Unable to delete old plugin at "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to copy plugin library from "%1" to "%2"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to move plugin library to "%1"</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>PluginManager</name> + <message> + <source>%1 lost link</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>%1 linked</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Plugin "%1" encountered a permanent error in positional data gathering</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Non-plugin found in plugin directory: "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Failed at loading manual plugin: %1</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>PluginUpdater</name> + <message> + <source>PluginUpdater</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>The following plugins can be updated.</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Select all</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Plugin</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Download-URL</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Do you want to update the selected plugins?</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to download plugin update for "%1" from "%2" (%3)</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Update for plugin "%1" failed due to too many redirects</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to download plugin update for "%1" from "%2" (HTTP status code %3)</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Successfully updated plugin "%1"</source> + <translation type="unfinished"></translation> </message> </context> <context> diff --git a/src/mumble/mumble_hu.ts b/src/mumble/mumble_hu.ts index d92ca1cc1..0e6933174 100644 --- a/src/mumble/mumble_hu.ts +++ b/src/mumble/mumble_hu.ts @@ -3826,6 +3826,10 @@ Ez a mező mutatja egy LCD eszköz méretét. A méret vagy pixelben (a grafikus <source>User stopped listening to channel</source> <translation>User stopped listening to channel</translation> </message> + <message> + <source>Plugin message</source> + <translation type="unfinished"></translation> + </message> </context> <context> <name>LogConfig</name> @@ -6171,12 +6175,13 @@ Valid actions are: <translation type="unfinished"></translation> </message> <message> - <source>Usage: mumble [options] [<url>] + <source>Usage: mumble [options] [<url> | <plugin_list>] <url> specifies a URL to connect to after startup instead of showing the connection window, and has the following form: mumble://[<username>[:<password>]@]<host>[:<port>][/<channel>[/<subchannel>...]][?version=<x.y.z>] +<plugin_list> is a list of plugin files that shall be installed The version query parameter has to be set in order to invoke the correct client version. It currently defaults to 1.2.0. @@ -6496,10 +6501,6 @@ Valid options are: <translation>Újracsatlakozás az utoljára használt kiszolgálóhoz indításkor</translation> </message> <message> - <source>Download plugin and overlay updates on startup</source> - <translation>Bővítmények és képátfedés frissítése indításkor</translation> - </message> - <message> <source>Privacy</source> <translation>Adatvédelem</translation> </message> @@ -6525,6 +6526,14 @@ Ez a beállítás meggátolja, hogy a Mumble érzékeny adatokat továbbítson a <source>Hide public server list</source> <translation>Nyilvános kiszolgálók listájának elrejtése</translation> </message> + <message> + <source>Check for plugin updates on startup</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Automatically download and install plugin updates</source> + <translation type="unfinished"></translation> + </message> </context> <context> <name>Overlay</name> @@ -7075,31 +7084,189 @@ To upgrade these files to their latest versions, click the button below.</source <translation>Név</translation> </message> <message> - <source>Enabled</source> - <translation>Engedélyezett</translation> + <source>Enable</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>PA</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>KeyEvents</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Install a plugin from a local file</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Install plugin...</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unload the currently selected plugin. This will remove it from the plugin list for the current session.</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unload</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>The plugin was installed successfully</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to deactivate all requested features for plugin "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to load plugin "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Whether the positional audio feature of this plugin should be enabled</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>This plugin does not provide support for positional audio</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Whether this plugin should be enabled</source> + <translation type="unfinished"></translation> </message> </context> <context> - <name>Plugins</name> + <name>PluginInstaller</name> <message> - <source>Skipping plugin update in debug mode.</source> - <translation>Bővítmény frissítésének mellőzése hibakeresési módban.</translation> + <source>PluginInstaller</source> + <translation type="unfinished"></translation> </message> <message> - <source>Downloaded new or updated plugin to %1.</source> - <translation>Új plugin letöltve vagy frissítve: %1.</translation> + <source>You are about to install the plugin listed below. Do you wish to proceed?</source> + <translation type="unfinished"></translation> </message> <message> - <source>Failed to install new plugin to %1.</source> - <translation>Nem sikerült új plugin telepítése: %1.</translation> + <source><html><head/><body><p><span style=" font-weight:600;">Name:</span></p></body></html></source> + <translation type="unfinished"></translation> </message> <message> - <source>%1 lost link.</source> - <translation>%1 elveszett kapcsolat.</translation> + <source><html><head/><body><p><span style=" font-weight:600;">Version:</span></p></body></html></source> + <translation type="unfinished"></translation> + </message> + <message> + <source><html><head/><body><p><span style=" font-weight:600;">Author(s):</span></p></body></html></source> + <translation type="unfinished"></translation> + </message> + <message> + <source><html><head/><body><p><span style=" font-weight:600;">Description:</span></p></body></html></source> + <translation type="unfinished"></translation> + </message> + <message> + <source>&No</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>&Yes</source> + <translation type="unfinished"></translation> </message> <message> - <source>%1 linked.</source> - <translation>%1 kapcsolva.</translation> + <source>The file "%1" is not a valid plugin file!</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Found more than one plugin library for the current OS in "%1" ("%2" and "%3")!</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to find a plugin for the current OS in "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to load plugin "%1" - check the plugin interface!</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to delete old plugin at "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to copy plugin library from "%1" to "%2"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to move plugin library to "%1"</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>PluginManager</name> + <message> + <source>%1 lost link</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>%1 linked</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Plugin "%1" encountered a permanent error in positional data gathering</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Non-plugin found in plugin directory: "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Failed at loading manual plugin: %1</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>PluginUpdater</name> + <message> + <source>PluginUpdater</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>The following plugins can be updated.</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Select all</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Plugin</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Download-URL</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Do you want to update the selected plugins?</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to download plugin update for "%1" from "%2" (%3)</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Update for plugin "%1" failed due to too many redirects</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to download plugin update for "%1" from "%2" (HTTP status code %3)</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Successfully updated plugin "%1"</source> + <translation type="unfinished"></translation> </message> </context> <context> diff --git a/src/mumble/mumble_it.ts b/src/mumble/mumble_it.ts index 312571797..d178feb5f 100644 --- a/src/mumble/mumble_it.ts +++ b/src/mumble/mumble_it.ts @@ -3838,6 +3838,10 @@ Questo campo descrive la dimensione di un dispositivo LCD. La dimensione è espr <source>User stopped listening to channel</source> <translation>Un utente ha smesso di ascoltare il tuo canale</translation> </message> + <message> + <source>Plugin message</source> + <translation type="unfinished"></translation> + </message> </context> <context> <name>LogConfig</name> @@ -6215,12 +6219,13 @@ Azioni valide: </translation> </message> <message> - <source>Usage: mumble [options] [<url>] + <source>Usage: mumble [options] [<url> | <plugin_list>] <url> specifies a URL to connect to after startup instead of showing the connection window, and has the following form: mumble://[<username>[:<password>]@]<host>[:<port>][/<channel>[/<subchannel>...]][?version=<x.y.z>] +<plugin_list> is a list of plugin files that shall be installed The version query parameter has to be set in order to invoke the correct client version. It currently defaults to 1.2.0. @@ -6270,59 +6275,7 @@ Valid options are: Otherwise the locale will be permanently saved to Mumble's settings. </source> - <translation>Utilizzo: mumble [opzioni] [<url>] - -<url> specifica un URL a cui connettersi dopo l'avvio invece di visualizzare la finestra di connessione e ha la seguente forma: -mumble: // [<username> [: <password>] @] <host> [: <port>] [/ <channel> [/ <subchannel> ...]] [? version = <x.y.z>] - -Il parametro di query della versione deve essere impostato per richiamare la versione client corretta. Attualmente il valore predefinito è 1.2.0. - -Le opzioni valide sono: - -h, --help Mostra questo testo di aiuto ed esce. - -m, --multiple - Consenti l'avvio di più istanze del client. - -c, --config - Specificare un file di configurazione alternativo. - Se lo usi per eseguire più istanze di Mumble contemporaneamente, - assicurati di impostare un valore "database" alternativo nel file config. - -n, --noidentity - Sopprimi il caricamento dei file di identità (ad esempio, certificati). - -jn, --jackname <arg> - Imposta il nome del client Jack personalizzato. - --licenza - Mostra la licenza di Mumble. - --autori - Mostra gli autori di Mumble. - - licenze di terze parti - Mostra le licenze per il software di terze parti utilizzato da Mumble. - --window-title-ext <arg> - Imposta un'estensione del titolo della finestra personalizzata. - --dump-input-stream - Esegui il dump dei flussi PCM in varie parti della catena di input - (utile per scopi di debug) - - ingresso microfono grezzo - - rilettura dell'altoparlante per la cancellazione dell'eco - - ingresso microfono elaborato - --print-echocancel-queue - Stampa su stdout lo stato della coda di cancellazione dell'eco - (utile per scopi di debug) - --translation-dir <dir> - Specifica una traduzione aggiuntiva fir <dir> in cui - Mumble cercherà i file di traduzione che sovrascrivono - quelli in bundle - Le directory aggiunte in questo modo hanno una priorità maggiore delle - posizioni predefinite utilizzate altrimenti - --print-translation-dirs - Stampa i percorsi in cui Mumble cercherà - file di traduzione che sovrascrivono quelli in bundle. - (Utile per i traduttori che testano le loro traduzioni) - --locale <locale> - Sovrascrivi le impostazioni locali nelle impostazioni di Mumble con un file - locale che corrisponde alla stringa di locale specificata. - Se il formato non è valido, Mumble genererà un errore. - In caso contrario, la locale verrà salvata in modo permanente nelle - impostazioni di Mumble. -</translation> + <translation type="unfinished"></translation> </message> </context> <context> @@ -6592,10 +6545,6 @@ Le opzioni valide sono: <translation>All'avvio connettiti all'ultimo server visitato</translation> </message> <message> - <source>Download plugin and overlay updates on startup</source> - <translation>Aggiorna plugin e sovrapposizione all'avvio</translation> - </message> - <message> <source>Privacy</source> <translation>Privacy</translation> </message> @@ -6621,6 +6570,14 @@ Previene l'invio da parte del client di informazioni potenzialmente identif <source>Hide public server list</source> <translation>Nascondi lista server pubblici</translation> </message> + <message> + <source>Check for plugin updates on startup</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Automatically download and install plugin updates</source> + <translation type="unfinished"></translation> + </message> </context> <context> <name>Overlay</name> @@ -7175,31 +7132,189 @@ Per aggiornare questi file all'ultima versione, premi il pulsante sottostan <translation>Nome</translation> </message> <message> - <source>Enabled</source> - <translation>Abilitato</translation> + <source>Enable</source> + <translation type="unfinished">Abilita</translation> + </message> + <message> + <source>PA</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>KeyEvents</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Install a plugin from a local file</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Install plugin...</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unload the currently selected plugin. This will remove it from the plugin list for the current session.</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unload</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>The plugin was installed successfully</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to deactivate all requested features for plugin "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to load plugin "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Whether the positional audio feature of this plugin should be enabled</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>This plugin does not provide support for positional audio</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Whether this plugin should be enabled</source> + <translation type="unfinished"></translation> </message> </context> <context> - <name>Plugins</name> + <name>PluginInstaller</name> <message> - <source>Skipping plugin update in debug mode.</source> - <translation>I plugin non verranno aggiornati in modalità Debug.</translation> + <source>PluginInstaller</source> + <translation type="unfinished"></translation> </message> <message> - <source>Downloaded new or updated plugin to %1.</source> - <translation>Aggiunto o aggiornato il seguente plugin: %1.</translation> + <source>You are about to install the plugin listed below. Do you wish to proceed?</source> + <translation type="unfinished"></translation> </message> <message> - <source>Failed to install new plugin to %1.</source> - <translation>Installazione del seguente plugin fallita: %1.</translation> + <source><html><head/><body><p><span style=" font-weight:600;">Name:</span></p></body></html></source> + <translation type="unfinished"></translation> </message> <message> - <source>%1 lost link.</source> - <translation>Collegamento perso con %1.</translation> + <source><html><head/><body><p><span style=" font-weight:600;">Version:</span></p></body></html></source> + <translation type="unfinished"></translation> </message> <message> - <source>%1 linked.</source> - <translation>collegato con %1.</translation> + <source><html><head/><body><p><span style=" font-weight:600;">Author(s):</span></p></body></html></source> + <translation type="unfinished"></translation> + </message> + <message> + <source><html><head/><body><p><span style=" font-weight:600;">Description:</span></p></body></html></source> + <translation type="unfinished"></translation> + </message> + <message> + <source>&No</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>&Yes</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>The file "%1" is not a valid plugin file!</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Found more than one plugin library for the current OS in "%1" ("%2" and "%3")!</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to find a plugin for the current OS in "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to load plugin "%1" - check the plugin interface!</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to delete old plugin at "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to copy plugin library from "%1" to "%2"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to move plugin library to "%1"</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>PluginManager</name> + <message> + <source>%1 lost link</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>%1 linked</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Plugin "%1" encountered a permanent error in positional data gathering</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Non-plugin found in plugin directory: "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Failed at loading manual plugin: %1</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>PluginUpdater</name> + <message> + <source>PluginUpdater</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>The following plugins can be updated.</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Select all</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Plugin</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Download-URL</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Do you want to update the selected plugins?</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to download plugin update for "%1" from "%2" (%3)</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Update for plugin "%1" failed due to too many redirects</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to download plugin update for "%1" from "%2" (HTTP status code %3)</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Successfully updated plugin "%1"</source> + <translation type="unfinished"></translation> </message> </context> <context> diff --git a/src/mumble/mumble_ja.ts b/src/mumble/mumble_ja.ts index 00cbf7ba6..add00ec4f 100644 --- a/src/mumble/mumble_ja.ts +++ b/src/mumble/mumble_ja.ts @@ -3826,6 +3826,10 @@ This field describes the size of an LCD device. The size is given either in pixe <source>User stopped listening to channel</source> <translation type="unfinished"></translation> </message> + <message> + <source>Plugin message</source> + <translation type="unfinished"></translation> + </message> </context> <context> <name>LogConfig</name> @@ -6170,12 +6174,13 @@ Valid actions are: <translation type="unfinished"></translation> </message> <message> - <source>Usage: mumble [options] [<url>] + <source>Usage: mumble [options] [<url> | <plugin_list>] <url> specifies a URL to connect to after startup instead of showing the connection window, and has the following form: mumble://[<username>[:<password>]@]<host>[:<port>][/<channel>[/<subchannel>...]][?version=<x.y.z>] +<plugin_list> is a list of plugin files that shall be installed The version query parameter has to be set in order to invoke the correct client version. It currently defaults to 1.2.0. @@ -6495,10 +6500,6 @@ Valid options are: <translation>起動時に最後に接続したサーバに再接続する</translation> </message> <message> - <source>Download plugin and overlay updates on startup</source> - <translation>開始時にプラグインとオーバレイの更新をダウンロードする</translation> - </message> - <message> <source>Privacy</source> <translation type="unfinished"></translation> </message> @@ -6523,6 +6524,14 @@ Prevents the client from sending potentially identifying information about the o <source>Hide public server list</source> <translation type="unfinished"></translation> </message> + <message> + <source>Check for plugin updates on startup</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Automatically download and install plugin updates</source> + <translation type="unfinished"></translation> + </message> </context> <context> <name>Overlay</name> @@ -7077,31 +7086,189 @@ To upgrade these files to their latest versions, click the button below.</source <translation>名前</translation> </message> <message> - <source>Enabled</source> - <translation>有効化</translation> + <source>Enable</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>PA</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>KeyEvents</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Install a plugin from a local file</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Install plugin...</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unload the currently selected plugin. This will remove it from the plugin list for the current session.</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unload</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>The plugin was installed successfully</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to deactivate all requested features for plugin "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to load plugin "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Whether the positional audio feature of this plugin should be enabled</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>This plugin does not provide support for positional audio</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Whether this plugin should be enabled</source> + <translation type="unfinished"></translation> </message> </context> <context> - <name>Plugins</name> + <name>PluginInstaller</name> <message> - <source>Skipping plugin update in debug mode.</source> - <translation>デバッグモードではプラグインのアップデートはスキップします。</translation> + <source>PluginInstaller</source> + <translation type="unfinished"></translation> </message> <message> - <source>Downloaded new or updated plugin to %1.</source> - <translation>更新されたプラグインを %1 にダウンロードしました。</translation> + <source>You are about to install the plugin listed below. Do you wish to proceed?</source> + <translation type="unfinished"></translation> </message> <message> - <source>Failed to install new plugin to %1.</source> - <translation>%1 に新しいプラグインをインストールできません。</translation> + <source><html><head/><body><p><span style=" font-weight:600;">Name:</span></p></body></html></source> + <translation type="unfinished"></translation> </message> <message> - <source>%1 lost link.</source> - <translation>%1 はリンクを失いました。</translation> + <source><html><head/><body><p><span style=" font-weight:600;">Version:</span></p></body></html></source> + <translation type="unfinished"></translation> + </message> + <message> + <source><html><head/><body><p><span style=" font-weight:600;">Author(s):</span></p></body></html></source> + <translation type="unfinished"></translation> + </message> + <message> + <source><html><head/><body><p><span style=" font-weight:600;">Description:</span></p></body></html></source> + <translation type="unfinished"></translation> + </message> + <message> + <source>&No</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>&Yes</source> + <translation type="unfinished"></translation> </message> <message> - <source>%1 linked.</source> - <translation>%1 はリンクされました。</translation> + <source>The file "%1" is not a valid plugin file!</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Found more than one plugin library for the current OS in "%1" ("%2" and "%3")!</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to find a plugin for the current OS in "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to load plugin "%1" - check the plugin interface!</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to delete old plugin at "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to copy plugin library from "%1" to "%2"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to move plugin library to "%1"</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>PluginManager</name> + <message> + <source>%1 lost link</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>%1 linked</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Plugin "%1" encountered a permanent error in positional data gathering</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Non-plugin found in plugin directory: "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Failed at loading manual plugin: %1</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>PluginUpdater</name> + <message> + <source>PluginUpdater</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>The following plugins can be updated.</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Select all</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Plugin</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Download-URL</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Do you want to update the selected plugins?</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to download plugin update for "%1" from "%2" (%3)</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Update for plugin "%1" failed due to too many redirects</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to download plugin update for "%1" from "%2" (HTTP status code %3)</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Successfully updated plugin "%1"</source> + <translation type="unfinished"></translation> </message> </context> <context> diff --git a/src/mumble/mumble_ko.ts b/src/mumble/mumble_ko.ts index 6cd8662fe..420312974 100644 --- a/src/mumble/mumble_ko.ts +++ b/src/mumble/mumble_ko.ts @@ -3809,6 +3809,10 @@ This field describes the size of an LCD device. The size is given either in pixe <source>User stopped listening to channel</source> <translation type="unfinished"></translation> </message> + <message> + <source>Plugin message</source> + <translation type="unfinished"></translation> + </message> </context> <context> <name>LogConfig</name> @@ -6152,12 +6156,13 @@ Valid actions are: <translation type="unfinished"></translation> </message> <message> - <source>Usage: mumble [options] [<url>] + <source>Usage: mumble [options] [<url> | <plugin_list>] <url> specifies a URL to connect to after startup instead of showing the connection window, and has the following form: mumble://[<username>[:<password>]@]<host>[:<port>][/<channel>[/<subchannel>...]][?version=<x.y.z>] +<plugin_list> is a list of plugin files that shall be installed The version query parameter has to be set in order to invoke the correct client version. It currently defaults to 1.2.0. @@ -6478,10 +6483,6 @@ Valid options are: <translation>시작시 마지막으로 접속한 서버에 다시 접속한다</translation> </message> <message> - <source>Download plugin and overlay updates on startup</source> - <translation>시작시 플러그 인과 오버레이의 업데이트를 다운로드한다.</translation> - </message> - <message> <source>Privacy</source> <translation type="unfinished"></translation> </message> @@ -6506,6 +6507,14 @@ Prevents the client from sending potentially identifying information about the o <source>Hide public server list</source> <translation type="unfinished"></translation> </message> + <message> + <source>Check for plugin updates on startup</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Automatically download and install plugin updates</source> + <translation type="unfinished"></translation> + </message> </context> <context> <name>Overlay</name> @@ -7060,31 +7069,189 @@ To upgrade these files to their latest versions, click the button below.</source <translation>이름</translation> </message> <message> - <source>Enabled</source> - <translation>유효</translation> + <source>Enable</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>PA</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>KeyEvents</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Install a plugin from a local file</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Install plugin...</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unload the currently selected plugin. This will remove it from the plugin list for the current session.</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unload</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>The plugin was installed successfully</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to deactivate all requested features for plugin "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to load plugin "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Whether the positional audio feature of this plugin should be enabled</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>This plugin does not provide support for positional audio</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Whether this plugin should be enabled</source> + <translation type="unfinished"></translation> </message> </context> <context> - <name>Plugins</name> + <name>PluginInstaller</name> <message> - <source>Skipping plugin update in debug mode.</source> - <translation>디버깅 모드에서는 플러그인의 업데이트가 스킵 됩니다.</translation> + <source>PluginInstaller</source> + <translation type="unfinished"></translation> </message> <message> - <source>Downloaded new or updated plugin to %1.</source> - <translation>업데이트된 플러그인을 %1에 다운 받았습니다.</translation> + <source>You are about to install the plugin listed below. Do you wish to proceed?</source> + <translation type="unfinished"></translation> </message> <message> - <source>Failed to install new plugin to %1.</source> - <translation>%1에 새로운 플러그인을 설치할 수 없습니다.</translation> + <source><html><head/><body><p><span style=" font-weight:600;">Name:</span></p></body></html></source> + <translation type="unfinished"></translation> </message> <message> - <source>%1 lost link.</source> - <translation>%1은 링크가 소실 되었습니다.</translation> + <source><html><head/><body><p><span style=" font-weight:600;">Version:</span></p></body></html></source> + <translation type="unfinished"></translation> + </message> + <message> + <source><html><head/><body><p><span style=" font-weight:600;">Author(s):</span></p></body></html></source> + <translation type="unfinished"></translation> + </message> + <message> + <source><html><head/><body><p><span style=" font-weight:600;">Description:</span></p></body></html></source> + <translation type="unfinished"></translation> + </message> + <message> + <source>&No</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>&Yes</source> + <translation type="unfinished"></translation> </message> <message> - <source>%1 linked.</source> - <translation>%1은 링크되었습니다.</translation> + <source>The file "%1" is not a valid plugin file!</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Found more than one plugin library for the current OS in "%1" ("%2" and "%3")!</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to find a plugin for the current OS in "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to load plugin "%1" - check the plugin interface!</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to delete old plugin at "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to copy plugin library from "%1" to "%2"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to move plugin library to "%1"</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>PluginManager</name> + <message> + <source>%1 lost link</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>%1 linked</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Plugin "%1" encountered a permanent error in positional data gathering</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Non-plugin found in plugin directory: "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Failed at loading manual plugin: %1</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>PluginUpdater</name> + <message> + <source>PluginUpdater</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>The following plugins can be updated.</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Select all</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Plugin</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Download-URL</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Do you want to update the selected plugins?</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to download plugin update for "%1" from "%2" (%3)</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Update for plugin "%1" failed due to too many redirects</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to download plugin update for "%1" from "%2" (HTTP status code %3)</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Successfully updated plugin "%1"</source> + <translation type="unfinished"></translation> </message> </context> <context> diff --git a/src/mumble/mumble_lt.ts b/src/mumble/mumble_lt.ts index abd115c97..4c7d4c039 100644 --- a/src/mumble/mumble_lt.ts +++ b/src/mumble/mumble_lt.ts @@ -3809,6 +3809,10 @@ This field describes the size of an LCD device. The size is given either in pixe <source>User stopped listening to channel</source> <translation type="unfinished"></translation> </message> + <message> + <source>Plugin message</source> + <translation type="unfinished"></translation> + </message> </context> <context> <name>LogConfig</name> @@ -6154,12 +6158,13 @@ Valid actions are: <translation type="unfinished"></translation> </message> <message> - <source>Usage: mumble [options] [<url>] + <source>Usage: mumble [options] [<url> | <plugin_list>] <url> specifies a URL to connect to after startup instead of showing the connection window, and has the following form: mumble://[<username>[:<password>]@]<host>[:<port>][/<channel>[/<subchannel>...]][?version=<x.y.z>] +<plugin_list> is a list of plugin files that shall be installed The version query parameter has to be set in order to invoke the correct client version. It currently defaults to 1.2.0. @@ -6479,10 +6484,6 @@ Valid options are: <translation>Paleidus programą, iš naujo prisijungti prie paskutinio serverio</translation> </message> <message> - <source>Download plugin and overlay updates on startup</source> - <translation type="unfinished"></translation> - </message> - <message> <source>Privacy</source> <translation>Privatumas</translation> </message> @@ -6507,6 +6508,14 @@ Prevents the client from sending potentially identifying information about the o <source>Hide public server list</source> <translation type="unfinished"></translation> </message> + <message> + <source>Check for plugin updates on startup</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Automatically download and install plugin updates</source> + <translation type="unfinished"></translation> + </message> </context> <context> <name>Overlay</name> @@ -7061,30 +7070,188 @@ Norėdami naujinti šiuos failus į naujausią versiją, spustelėkite mygtuką <translation>Pavadinimas</translation> </message> <message> - <source>Enabled</source> - <translation>Įjungta</translation> + <source>Enable</source> + <translation type="unfinished">Įjungti</translation> + </message> + <message> + <source>PA</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>KeyEvents</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Install a plugin from a local file</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Install plugin...</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unload the currently selected plugin. This will remove it from the plugin list for the current session.</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unload</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>The plugin was installed successfully</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to deactivate all requested features for plugin "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to load plugin "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Whether the positional audio feature of this plugin should be enabled</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>This plugin does not provide support for positional audio</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Whether this plugin should be enabled</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>PluginInstaller</name> + <message> + <source>PluginInstaller</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>You are about to install the plugin listed below. Do you wish to proceed?</source> + <translation type="unfinished"></translation> + </message> + <message> + <source><html><head/><body><p><span style=" font-weight:600;">Name:</span></p></body></html></source> + <translation type="unfinished"></translation> + </message> + <message> + <source><html><head/><body><p><span style=" font-weight:600;">Version:</span></p></body></html></source> + <translation type="unfinished"></translation> + </message> + <message> + <source><html><head/><body><p><span style=" font-weight:600;">Author(s):</span></p></body></html></source> + <translation type="unfinished"></translation> + </message> + <message> + <source><html><head/><body><p><span style=" font-weight:600;">Description:</span></p></body></html></source> + <translation type="unfinished"></translation> + </message> + <message> + <source>&No</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>&Yes</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>The file "%1" is not a valid plugin file!</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Found more than one plugin library for the current OS in "%1" ("%2" and "%3")!</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to find a plugin for the current OS in "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to load plugin "%1" - check the plugin interface!</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to delete old plugin at "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to copy plugin library from "%1" to "%2"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to move plugin library to "%1"</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>PluginManager</name> + <message> + <source>%1 lost link</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>%1 linked</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Plugin "%1" encountered a permanent error in positional data gathering</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Non-plugin found in plugin directory: "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Failed at loading manual plugin: %1</source> + <translation type="unfinished"></translation> </message> </context> <context> - <name>Plugins</name> + <name>PluginUpdater</name> + <message> + <source>PluginUpdater</source> + <translation type="unfinished"></translation> + </message> <message> - <source>Skipping plugin update in debug mode.</source> - <translation>Derinimo veiksenoje, praleidžiamas įskiepio atnaujinimas.</translation> + <source>The following plugins can be updated.</source> + <translation type="unfinished"></translation> </message> <message> - <source>Downloaded new or updated plugin to %1.</source> - <translation>Atsisiųstas naujas ar atnaujintas įskiepis į %1.</translation> + <source>Select all</source> + <translation type="unfinished"></translation> </message> <message> - <source>Failed to install new plugin to %1.</source> - <translation>Nepavyko įdiegti naujo įskiepio į %1.</translation> + <source>Plugin</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Download-URL</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Do you want to update the selected plugins?</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to download plugin update for "%1" from "%2" (%3)</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Update for plugin "%1" failed due to too many redirects</source> + <translation type="unfinished"></translation> </message> <message> - <source>%1 lost link.</source> + <source>Unable to download plugin update for "%1" from "%2" (HTTP status code %3)</source> <translation type="unfinished"></translation> </message> <message> - <source>%1 linked.</source> + <source>Successfully updated plugin "%1"</source> <translation type="unfinished"></translation> </message> </context> diff --git a/src/mumble/mumble_nl.ts b/src/mumble/mumble_nl.ts index 774988679..894779254 100644 --- a/src/mumble/mumble_nl.ts +++ b/src/mumble/mumble_nl.ts @@ -3838,6 +3838,10 @@ Veld beschrijft LCD-apparaatgrootte aangeduid in pixels (voor Grafische LCD&apos <source>User stopped listening to channel</source> <translation>Gebruiker luistert niet meer naar kanaal</translation> </message> + <message> + <source>Plugin message</source> + <translation type="unfinished"></translation> + </message> </context> <context> <name>LogConfig</name> @@ -6214,12 +6218,13 @@ Valide acties zijn: </translation> </message> <message> - <source>Usage: mumble [options] [<url>] + <source>Usage: mumble [options] [<url> | <plugin_list>] <url> specifies a URL to connect to after startup instead of showing the connection window, and has the following form: mumble://[<username>[:<password>]@]<host>[:<port>][/<channel>[/<subchannel>...]][?version=<x.y.z>] +<plugin_list> is a list of plugin files that shall be installed The version query parameter has to be set in order to invoke the correct client version. It currently defaults to 1.2.0. @@ -6269,60 +6274,7 @@ Valid options are: Otherwise the locale will be permanently saved to Mumble's settings. </source> - <translation>Gebruik: mumble [opties] [<url>] - -<url> specifieert een URL om mee te verbinden bij het opstarten i.p.v. -het venster om te verbinden te tonen, en heeft het volgende formaat: -mumble://[<gebruikersnaam>[:<wachtwoord>]@]<serveradres>[:<poort>][/<kanaal>[/<subkanaal>...]][?versie=<x.y.z>] - -De versie-parameter moet ingesteld worden om de correcte versie -van Mumble te kiezen. Momenteel is dit standaard 1.2.0. - -Valide opties zijn: - -h, --help Toont deze hulptekst en sluit vervolgens af. - -m, --multiple - Laat toe dat meerdere instanties van de applicatie tegelijk draaien. - -c, --config - Geef een alternatief configuratiebestand op. - Als je dit gebruikt om meerdere instanties van Mumble tegelijk te draaien, - vergeet dan niet in dit bestand een aparte waarde voor 'database' op te geven. - -n, --noidentity - Voorkom het laden van identiteitsbestanden (bv. certificaten). - -jn, --jackname <argument> - Stel een zelfgekozen client-naam in voor Jack. - --license - Toon de licentie van Mumble. - --authors - Toon een overzicht van de auteurs van Mumble. - --third-party-licenses - Toon licenties van software van derde partijen die gebruikt wordt door Mumble. - --window-title-ext <argument> - Stelt een zelfgekozen achtervoegsel in voor de titel van het venster. - --dump-input-streams - Dump PCM streams op verschillende plaatsen gedurende de invoerverwerking - (nuttig bij het opsporen van bugs) - - Onverwerkte microfooninvoer - - Teruglezen van spraak bij echo-opheffing - - Verwerkte microfooninvoer - --print-echocancel-queue - Stuur de staat van de wachtrij die gebruikt wordt voor echo-opheffing naar stdout - (nuttig bij het opsporen van bugs) - --translation-dir <map> - Geeft een bijkomende vertalingsmap <map> op waarin Mumble moet zoeken op - vertalingsbestanden, die vervolgens voorrang krijgen op de ingebouwde. - Deze mappen krijgen een hogere prioriteit dan de standaardlocaties die anders - gebruikt worden. - --print-translation-dirs - Print de paden die Mumble doorzoekt voor vertalingsbestanden die voorrang - krijgen op de ingebouwde vertalingen af. - (Nuttig voor vertalers die hun vertalingen willen testen) - --locale <taalgebied> - Overschrijf het taalgebied uit de instellingen van Mumble met een taalgebied - dat overeenkomt met de opgegeven identificator. - Als het formaat ongeldig is, zal Mumble een fout geven. - Indien er geen fout is, zal dit taalgebied permanent opgeslagen worden in de - instellingen van Mumble. -</translation> + <translation type="unfinished"></translation> </message> </context> <context> @@ -6592,10 +6544,6 @@ Valide opties zijn: <translation>Verbind opnieuw met laatst bezochte server bij opstarten</translation> </message> <message> - <source>Download plugin and overlay updates on startup</source> - <translation>Plug-in/nieuwe overlay-updates downloaden bij starten</translation> - </message> - <message> <source>Privacy</source> <translation>Privacy</translation> </message> @@ -6621,6 +6569,14 @@ Voorkomt dat Mumble potentieel identificerende informatie over het besturingssys <source>Hide public server list</source> <translation>Verberg publieke server-lijst</translation> </message> + <message> + <source>Check for plugin updates on startup</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Automatically download and install plugin updates</source> + <translation type="unfinished"></translation> + </message> </context> <context> <name>Overlay</name> @@ -7175,31 +7131,189 @@ Klik op de onderstaande knop om deze bestanden naar de laatste versie bij te wer <translation>Naam</translation> </message> <message> - <source>Enabled</source> - <translation>Geactiveerd</translation> + <source>Enable</source> + <translation type="unfinished">Activeren</translation> + </message> + <message> + <source>PA</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>KeyEvents</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Install a plugin from a local file</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Install plugin...</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unload the currently selected plugin. This will remove it from the plugin list for the current session.</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unload</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>The plugin was installed successfully</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to deactivate all requested features for plugin "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to load plugin "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Whether the positional audio feature of this plugin should be enabled</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>This plugin does not provide support for positional audio</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Whether this plugin should be enabled</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>PluginInstaller</name> + <message> + <source>PluginInstaller</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>You are about to install the plugin listed below. Do you wish to proceed?</source> + <translation type="unfinished"></translation> + </message> + <message> + <source><html><head/><body><p><span style=" font-weight:600;">Name:</span></p></body></html></source> + <translation type="unfinished"></translation> + </message> + <message> + <source><html><head/><body><p><span style=" font-weight:600;">Version:</span></p></body></html></source> + <translation type="unfinished"></translation> + </message> + <message> + <source><html><head/><body><p><span style=" font-weight:600;">Author(s):</span></p></body></html></source> + <translation type="unfinished"></translation> + </message> + <message> + <source><html><head/><body><p><span style=" font-weight:600;">Description:</span></p></body></html></source> + <translation type="unfinished"></translation> + </message> + <message> + <source>&No</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>&Yes</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>The file "%1" is not a valid plugin file!</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Found more than one plugin library for the current OS in "%1" ("%2" and "%3")!</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to find a plugin for the current OS in "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to load plugin "%1" - check the plugin interface!</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to delete old plugin at "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to copy plugin library from "%1" to "%2"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to move plugin library to "%1"</source> + <translation type="unfinished"></translation> </message> </context> <context> - <name>Plugins</name> + <name>PluginManager</name> <message> - <source>Skipping plugin update in debug mode.</source> - <translation>Plugins bijwerken overslaan in foutopsporende-modus.</translation> + <source>%1 lost link</source> + <translation type="unfinished"></translation> </message> <message> - <source>Downloaded new or updated plugin to %1.</source> - <translation>Nieuwe of bijgewerkte plug-in naar %1 gedownload.</translation> + <source>%1 linked</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Plugin "%1" encountered a permanent error in positional data gathering</source> + <translation type="unfinished"></translation> </message> <message> - <source>Failed to install new plugin to %1.</source> - <translation>Nieuwe plug-in naar %1 installeren mislukt.</translation> + <source>Non-plugin found in plugin directory: "%1"</source> + <translation type="unfinished"></translation> </message> <message> - <source>%1 lost link.</source> - <translation>%1 verloor verbinding.</translation> + <source>Failed at loading manual plugin: %1</source> + <translation type="unfinished"></translation> </message> +</context> +<context> + <name>PluginUpdater</name> <message> - <source>%1 linked.</source> - <translation>%1 verbonden.</translation> + <source>PluginUpdater</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>The following plugins can be updated.</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Select all</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Plugin</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Download-URL</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Do you want to update the selected plugins?</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to download plugin update for "%1" from "%2" (%3)</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Update for plugin "%1" failed due to too many redirects</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to download plugin update for "%1" from "%2" (HTTP status code %3)</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Successfully updated plugin "%1"</source> + <translation type="unfinished"></translation> </message> </context> <context> diff --git a/src/mumble/mumble_no.ts b/src/mumble/mumble_no.ts index 62db26e34..07486d581 100644 --- a/src/mumble/mumble_no.ts +++ b/src/mumble/mumble_no.ts @@ -3850,6 +3850,10 @@ Dette feltet beskriver størrelsen på en LCD-enhet. Enten gitt i piksler (for g <source>User stopped listening to channel</source> <translation>Bruker stoppet å lytte til kanalen</translation> </message> + <message> + <source>Plugin message</source> + <translation type="unfinished"></translation> + </message> </context> <context> <name>LogConfig</name> @@ -6204,12 +6208,13 @@ Valid actions are: <translation type="unfinished"></translation> </message> <message> - <source>Usage: mumble [options] [<url>] + <source>Usage: mumble [options] [<url> | <plugin_list>] <url> specifies a URL to connect to after startup instead of showing the connection window, and has the following form: mumble://[<username>[:<password>]@]<host>[:<port>][/<channel>[/<subchannel>...]][?version=<x.y.z>] +<plugin_list> is a list of plugin files that shall be installed The version query parameter has to be set in order to invoke the correct client version. It currently defaults to 1.2.0. @@ -6529,10 +6534,6 @@ Valid options are: <translation>Koble til tjeneren som sist ble brukt ved oppstart</translation> </message> <message> - <source>Download plugin and overlay updates on startup</source> - <translation>Last ned programtillegg og forgrunnsinformasjonsoppdateringer ved oppstart</translation> - </message> - <message> <source>Privacy</source> <translation>Personvern</translation> </message> @@ -6558,6 +6559,14 @@ Forhindrer klienten fra å sende potensielt identifiserende informasjon om opera <source>Hide public server list</source> <translation>Skjul offentlig tjenerliste</translation> </message> + <message> + <source>Check for plugin updates on startup</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Automatically download and install plugin updates</source> + <translation type="unfinished"></translation> + </message> </context> <context> <name>Overlay</name> @@ -7112,31 +7121,189 @@ Trykk på knappen nedefor for å oppgradere.</translation> <translation>Navn</translation> </message> <message> - <source>Enabled</source> - <translation>Påskrudd</translation> + <source>Enable</source> + <translation type="unfinished">Skru på</translation> + </message> + <message> + <source>PA</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>KeyEvents</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Install a plugin from a local file</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Install plugin...</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unload the currently selected plugin. This will remove it from the plugin list for the current session.</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unload</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>The plugin was installed successfully</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to deactivate all requested features for plugin "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to load plugin "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Whether the positional audio feature of this plugin should be enabled</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>This plugin does not provide support for positional audio</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Whether this plugin should be enabled</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>PluginInstaller</name> + <message> + <source>PluginInstaller</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>You are about to install the plugin listed below. Do you wish to proceed?</source> + <translation type="unfinished"></translation> + </message> + <message> + <source><html><head/><body><p><span style=" font-weight:600;">Name:</span></p></body></html></source> + <translation type="unfinished"></translation> + </message> + <message> + <source><html><head/><body><p><span style=" font-weight:600;">Version:</span></p></body></html></source> + <translation type="unfinished"></translation> + </message> + <message> + <source><html><head/><body><p><span style=" font-weight:600;">Author(s):</span></p></body></html></source> + <translation type="unfinished"></translation> + </message> + <message> + <source><html><head/><body><p><span style=" font-weight:600;">Description:</span></p></body></html></source> + <translation type="unfinished"></translation> + </message> + <message> + <source>&No</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>&Yes</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>The file "%1" is not a valid plugin file!</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Found more than one plugin library for the current OS in "%1" ("%2" and "%3")!</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to find a plugin for the current OS in "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to load plugin "%1" - check the plugin interface!</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to delete old plugin at "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to copy plugin library from "%1" to "%2"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to move plugin library to "%1"</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>PluginManager</name> + <message> + <source>%1 lost link</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>%1 linked</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Plugin "%1" encountered a permanent error in positional data gathering</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Non-plugin found in plugin directory: "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Failed at loading manual plugin: %1</source> + <translation type="unfinished"></translation> </message> </context> <context> - <name>Plugins</name> + <name>PluginUpdater</name> + <message> + <source>PluginUpdater</source> + <translation type="unfinished"></translation> + </message> <message> - <source>Skipping plugin update in debug mode.</source> - <translation>Hopper over programoppdatering i feilrettingsmodus.</translation> + <source>The following plugins can be updated.</source> + <translation type="unfinished"></translation> </message> <message> - <source>Downloaded new or updated plugin to %1.</source> - <translation>Programtillegg lastet ned eller oppdatert til %1.</translation> + <source>Select all</source> + <translation type="unfinished"></translation> </message> <message> - <source>Failed to install new plugin to %1.</source> - <translation>Klarte ikke å installere nytt programtillegg til %1.</translation> + <source>Plugin</source> + <translation type="unfinished"></translation> </message> <message> - <source>%1 lost link.</source> - <translation>%1 mistet lenke.</translation> + <source>Download-URL</source> + <translation type="unfinished"></translation> </message> <message> - <source>%1 linked.</source> - <translation>%1 er lenket.</translation> + <source>Do you want to update the selected plugins?</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to download plugin update for "%1" from "%2" (%3)</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Update for plugin "%1" failed due to too many redirects</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to download plugin update for "%1" from "%2" (HTTP status code %3)</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Successfully updated plugin "%1"</source> + <translation type="unfinished"></translation> </message> </context> <context> diff --git a/src/mumble/mumble_oc.ts b/src/mumble/mumble_oc.ts index 34f7cec51..c5b0ce572 100644 --- a/src/mumble/mumble_oc.ts +++ b/src/mumble/mumble_oc.ts @@ -3779,6 +3779,10 @@ This field describes the size of an LCD device. The size is given either in pixe <source>User stopped listening to channel</source> <translation type="unfinished"></translation> </message> + <message> + <source>Plugin message</source> + <translation type="unfinished"></translation> + </message> </context> <context> <name>LogConfig</name> @@ -6122,12 +6126,13 @@ Valid actions are: <translation type="unfinished"></translation> </message> <message> - <source>Usage: mumble [options] [<url>] + <source>Usage: mumble [options] [<url> | <plugin_list>] <url> specifies a URL to connect to after startup instead of showing the connection window, and has the following form: mumble://[<username>[:<password>]@]<host>[:<port>][/<channel>[/<subchannel>...]][?version=<x.y.z>] +<plugin_list> is a list of plugin files that shall be installed The version query parameter has to be set in order to invoke the correct client version. It currently defaults to 1.2.0. @@ -6447,10 +6452,6 @@ Valid options are: <translation type="unfinished"></translation> </message> <message> - <source>Download plugin and overlay updates on startup</source> - <translation type="unfinished"></translation> - </message> - <message> <source>Privacy</source> <translation type="unfinished"></translation> </message> @@ -6475,6 +6476,14 @@ Prevents the client from sending potentially identifying information about the o <source>Hide public server list</source> <translation type="unfinished"></translation> </message> + <message> + <source>Check for plugin updates on startup</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Automatically download and install plugin updates</source> + <translation type="unfinished"></translation> + </message> </context> <context> <name>Overlay</name> @@ -7025,30 +7034,188 @@ To upgrade these files to their latest versions, click the button below.</source <translation type="unfinished">Nom</translation> </message> <message> - <source>Enabled</source> + <source>Enable</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>PA</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>KeyEvents</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Install a plugin from a local file</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Install plugin...</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unload the currently selected plugin. This will remove it from the plugin list for the current session.</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unload</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>The plugin was installed successfully</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to deactivate all requested features for plugin "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to load plugin "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Whether the positional audio feature of this plugin should be enabled</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>This plugin does not provide support for positional audio</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Whether this plugin should be enabled</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>PluginInstaller</name> + <message> + <source>PluginInstaller</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>You are about to install the plugin listed below. Do you wish to proceed?</source> + <translation type="unfinished"></translation> + </message> + <message> + <source><html><head/><body><p><span style=" font-weight:600;">Name:</span></p></body></html></source> + <translation type="unfinished"></translation> + </message> + <message> + <source><html><head/><body><p><span style=" font-weight:600;">Version:</span></p></body></html></source> + <translation type="unfinished"></translation> + </message> + <message> + <source><html><head/><body><p><span style=" font-weight:600;">Author(s):</span></p></body></html></source> + <translation type="unfinished"></translation> + </message> + <message> + <source><html><head/><body><p><span style=" font-weight:600;">Description:</span></p></body></html></source> + <translation type="unfinished"></translation> + </message> + <message> + <source>&No</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>&Yes</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>The file "%1" is not a valid plugin file!</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Found more than one plugin library for the current OS in "%1" ("%2" and "%3")!</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to find a plugin for the current OS in "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to load plugin "%1" - check the plugin interface!</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to delete old plugin at "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to copy plugin library from "%1" to "%2"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to move plugin library to "%1"</source> <translation type="unfinished"></translation> </message> </context> <context> - <name>Plugins</name> + <name>PluginManager</name> + <message> + <source>%1 lost link</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>%1 linked</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Plugin "%1" encountered a permanent error in positional data gathering</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Non-plugin found in plugin directory: "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Failed at loading manual plugin: %1</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>PluginUpdater</name> + <message> + <source>PluginUpdater</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>The following plugins can be updated.</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Select all</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Plugin</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Download-URL</source> + <translation type="unfinished"></translation> + </message> <message> - <source>Skipping plugin update in debug mode.</source> + <source>Do you want to update the selected plugins?</source> <translation type="unfinished"></translation> </message> <message> - <source>Downloaded new or updated plugin to %1.</source> + <source>Unable to download plugin update for "%1" from "%2" (%3)</source> <translation type="unfinished"></translation> </message> <message> - <source>Failed to install new plugin to %1.</source> + <source>Update for plugin "%1" failed due to too many redirects</source> <translation type="unfinished"></translation> </message> <message> - <source>%1 lost link.</source> + <source>Unable to download plugin update for "%1" from "%2" (HTTP status code %3)</source> <translation type="unfinished"></translation> </message> <message> - <source>%1 linked.</source> + <source>Successfully updated plugin "%1"</source> <translation type="unfinished"></translation> </message> </context> diff --git a/src/mumble/mumble_pl.ts b/src/mumble/mumble_pl.ts index 65267a939..05d9c5b9b 100644 --- a/src/mumble/mumble_pl.ts +++ b/src/mumble/mumble_pl.ts @@ -3839,6 +3839,10 @@ Te pole opisuje rozmiar urządzenia LCD. Rozmiar jest podany w pikselach lub w z <source>User stopped listening to channel</source> <translation>Użytkownik przestał słuchać kanału</translation> </message> + <message> + <source>Plugin message</source> + <translation type="unfinished"></translation> + </message> </context> <context> <name>LogConfig</name> @@ -6215,12 +6219,13 @@ toggledeaf </translation> </message> <message> - <source>Usage: mumble [options] [<url>] + <source>Usage: mumble [options] [<url> | <plugin_list>] <url> specifies a URL to connect to after startup instead of showing the connection window, and has the following form: mumble://[<username>[:<password>]@]<host>[:<port>][/<channel>[/<subchannel>...]][?version=<x.y.z>] +<plugin_list> is a list of plugin files that shall be installed The version query parameter has to be set in order to invoke the correct client version. It currently defaults to 1.2.0. @@ -6270,61 +6275,7 @@ Valid options are: Otherwise the locale will be permanently saved to Mumble's settings. </source> - <translation>Użycie: mumble [opcje] [<url>] - -<url> określa adres URL, z którym należy się połączyć po uruchomieniu zamiast pokazywać -okno połączenia, ma następującą postać: -mumble://[<nazwa użytkownika>[:<hasło>]@]<host>[:<port>][/<kanał>[/<podkanał>...]][?wersja=<x.y.z>] - -Aby wywołać poprawną wersję klienta, należy ustawić parametr -zapytania o wersję. Obecnie domyślnie jest to 1.2.0. - -Prawidłowe opcje to: - -h, --help Pokaż ten tekst pomocy i zakończ. - -m, --multiple - Zezwalaj na uruchamianie wielu instancji klienta. - -c, --config - Określ alternatywny plik konfiguracyjny. - Jeśli używasz tego do uruchamiania wielu wystąpień Mumble jednocześnie, - upewnij się, że ustawiono alternatywną wartość „bazy danych” w konfiguracji. - -n, --noidentity - Blokuj ładowanie plików tożsamości (tj. certyfikatów). - -jn, --jackname <arg> - Ustaw niestandardową nazwę klienta Jack. - --license - Pokaż licencję Mumble. - --authors - Pokaż autorów Mumble. - --third-party-licenses - Pokaż licencje na oprogramowanie innych firm używane przez Mumble. - --window-title-ext <arg> - Ustawia niestandardowe rozszerzenie tytułu okna. - --dump-input-stream - Zrzuca strumienie PCM w różnych częściach łańcucha wejściowego - (przydatne do debugowania) - - surowe wejście mikrofonowe - - odczyt głośnika w celu usunięcia echa - - przetworzone wejście mikrofonowe - --print-echocancel-queue - Wyświetl na stdout stan kolejki anulowania echa - (przydatne do debugowania) - --translation-dir <dir> - Określa dodatkowe tłumaczenie dla <kat>, w którym - Mumble wyszuka pliki tłumaczeń, które nadpiszą - dołączone - Katalogi dodane w ten sposób mają wyższy priorytet niż - domyślne lokalizacje używane w inny sposób - --print-translation-dirs - Wyświetl ścieżki, których będzie szukał Mumble - plików tłumaczeń, które zastępują dołączone pliki. - (Przydatne dla tłumaczy testujących swoje tłumaczenia) - --locale <ust. reg,> - Nadpisz ustawienia regionalne w ustawieniach Mumble za pomocą - ustawień reg., które odpowiada podanemu łańcuchowi ustawień reg. - Jeśli format jest nieprawidłowy, Mumble wyświetli błąd. - W przeciwnym razie ustawienia regionalne zostaną trwale zapisane w - ustawieniach Mumble. -</translation> + <translation type="unfinished"></translation> </message> </context> <context> @@ -6594,10 +6545,6 @@ Prawidłowe opcje to: <translation>Przy starcie połącz ponownie do ostatniego serwera</translation> </message> <message> - <source>Download plugin and overlay updates on startup</source> - <translation>Pobieraj aktualizacje nakładki oraz wtyczek przy starcie</translation> - </message> - <message> <source>Privacy</source> <translation>Prywatność</translation> </message> @@ -6623,6 +6570,14 @@ Uniemożliwia klientowi wysyłanie potencjalnie identyfikujących informacji o s <source>Hide public server list</source> <translation>Ukryj listę serwerów publicznych</translation> </message> + <message> + <source>Check for plugin updates on startup</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Automatically download and install plugin updates</source> + <translation type="unfinished"></translation> + </message> </context> <context> <name>Overlay</name> @@ -7177,31 +7132,189 @@ Aby uaktualnić pliki do najnowszych wersji, kliknij przycisk poniżej.</transla <translation>Nazwa</translation> </message> <message> - <source>Enabled</source> - <translation>Włączona</translation> + <source>Enable</source> + <translation type="unfinished">Włącz</translation> + </message> + <message> + <source>PA</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>KeyEvents</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Install a plugin from a local file</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Install plugin...</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unload the currently selected plugin. This will remove it from the plugin list for the current session.</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unload</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>The plugin was installed successfully</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to deactivate all requested features for plugin "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to load plugin "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Whether the positional audio feature of this plugin should be enabled</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>This plugin does not provide support for positional audio</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Whether this plugin should be enabled</source> + <translation type="unfinished"></translation> </message> </context> <context> - <name>Plugins</name> + <name>PluginInstaller</name> <message> - <source>Skipping plugin update in debug mode.</source> - <translation>Pomijanie aktualizacji wtyczek w trybie debugowania.</translation> + <source>PluginInstaller</source> + <translation type="unfinished"></translation> </message> <message> - <source>Downloaded new or updated plugin to %1.</source> - <translation>Pobrano nowy lub uaktualniony plugin do %1.</translation> + <source>You are about to install the plugin listed below. Do you wish to proceed?</source> + <translation type="unfinished"></translation> </message> <message> - <source>Failed to install new plugin to %1.</source> - <translation>Nie udało się zainstalować nowego pluginu do %1.</translation> + <source><html><head/><body><p><span style=" font-weight:600;">Name:</span></p></body></html></source> + <translation type="unfinished"></translation> </message> <message> - <source>%1 lost link.</source> - <translation>%1 utracił połączenie.</translation> + <source><html><head/><body><p><span style=" font-weight:600;">Version:</span></p></body></html></source> + <translation type="unfinished"></translation> </message> <message> - <source>%1 linked.</source> - <translation>%1 połączony.</translation> + <source><html><head/><body><p><span style=" font-weight:600;">Author(s):</span></p></body></html></source> + <translation type="unfinished"></translation> + </message> + <message> + <source><html><head/><body><p><span style=" font-weight:600;">Description:</span></p></body></html></source> + <translation type="unfinished"></translation> + </message> + <message> + <source>&No</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>&Yes</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>The file "%1" is not a valid plugin file!</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Found more than one plugin library for the current OS in "%1" ("%2" and "%3")!</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to find a plugin for the current OS in "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to load plugin "%1" - check the plugin interface!</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to delete old plugin at "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to copy plugin library from "%1" to "%2"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to move plugin library to "%1"</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>PluginManager</name> + <message> + <source>%1 lost link</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>%1 linked</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Plugin "%1" encountered a permanent error in positional data gathering</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Non-plugin found in plugin directory: "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Failed at loading manual plugin: %1</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>PluginUpdater</name> + <message> + <source>PluginUpdater</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>The following plugins can be updated.</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Select all</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Plugin</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Download-URL</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Do you want to update the selected plugins?</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to download plugin update for "%1" from "%2" (%3)</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Update for plugin "%1" failed due to too many redirects</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to download plugin update for "%1" from "%2" (HTTP status code %3)</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Successfully updated plugin "%1"</source> + <translation type="unfinished"></translation> </message> </context> <context> diff --git a/src/mumble/mumble_pt_BR.ts b/src/mumble/mumble_pt_BR.ts index 451b875f6..e5e34bf6f 100644 --- a/src/mumble/mumble_pt_BR.ts +++ b/src/mumble/mumble_pt_BR.ts @@ -3838,6 +3838,10 @@ Este campo descreve o tamanho de um dispositivo LCD. O tamanho é dado em pixels <source>User stopped listening to channel</source> <translation>Usuário parou de ouvir o canal</translation> </message> + <message> + <source>Plugin message</source> + <translation type="unfinished"></translation> + </message> </context> <context> <name>LogConfig</name> @@ -6192,12 +6196,13 @@ Valid actions are: <translation type="unfinished"></translation> </message> <message> - <source>Usage: mumble [options] [<url>] + <source>Usage: mumble [options] [<url> | <plugin_list>] <url> specifies a URL to connect to after startup instead of showing the connection window, and has the following form: mumble://[<username>[:<password>]@]<host>[:<port>][/<channel>[/<subchannel>...]][?version=<x.y.z>] +<plugin_list> is a list of plugin files that shall be installed The version query parameter has to be set in order to invoke the correct client version. It currently defaults to 1.2.0. @@ -6517,10 +6522,6 @@ Valid options are: <translation>Reconectar ao último servidor ao iniciar</translation> </message> <message> - <source>Download plugin and overlay updates on startup</source> - <translation>Baixar atualizações de complementos e sobreimpressão ao iniciar</translation> - </message> - <message> <source>Privacy</source> <translation>Privacidade</translation> </message> @@ -6546,6 +6547,14 @@ Evita que o cliente envie informações potencialmente capazes de identificaçã <source>Hide public server list</source> <translation>Ocultar lista de servidores públicos</translation> </message> + <message> + <source>Check for plugin updates on startup</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Automatically download and install plugin updates</source> + <translation type="unfinished"></translation> + </message> </context> <context> <name>Overlay</name> @@ -7100,31 +7109,189 @@ Para atualizar estes arquivos para suas últimas versões, clique no botão abai <translation>Nome</translation> </message> <message> - <source>Enabled</source> - <translation>Ativo</translation> + <source>Enable</source> + <translation type="unfinished">Ativar</translation> + </message> + <message> + <source>PA</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>KeyEvents</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Install a plugin from a local file</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Install plugin...</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unload the currently selected plugin. This will remove it from the plugin list for the current session.</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unload</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>The plugin was installed successfully</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to deactivate all requested features for plugin "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to load plugin "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Whether the positional audio feature of this plugin should be enabled</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>This plugin does not provide support for positional audio</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Whether this plugin should be enabled</source> + <translation type="unfinished"></translation> </message> </context> <context> - <name>Plugins</name> + <name>PluginInstaller</name> <message> - <source>Skipping plugin update in debug mode.</source> - <translation>Pulando atualização de complementos no modo de depuração.</translation> + <source>PluginInstaller</source> + <translation type="unfinished"></translation> </message> <message> - <source>Downloaded new or updated plugin to %1.</source> - <translation>Baixou complemento novo ou atualizado para %1.</translation> + <source>You are about to install the plugin listed below. Do you wish to proceed?</source> + <translation type="unfinished"></translation> </message> <message> - <source>Failed to install new plugin to %1.</source> - <translation>Falha ao instalar a novo complemento para %1.</translation> + <source><html><head/><body><p><span style=" font-weight:600;">Name:</span></p></body></html></source> + <translation type="unfinished"></translation> </message> <message> - <source>%1 lost link.</source> - <translation>%1 perdeu vínculo.</translation> + <source><html><head/><body><p><span style=" font-weight:600;">Version:</span></p></body></html></source> + <translation type="unfinished"></translation> + </message> + <message> + <source><html><head/><body><p><span style=" font-weight:600;">Author(s):</span></p></body></html></source> + <translation type="unfinished"></translation> + </message> + <message> + <source><html><head/><body><p><span style=" font-weight:600;">Description:</span></p></body></html></source> + <translation type="unfinished"></translation> + </message> + <message> + <source>&No</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>&Yes</source> + <translation type="unfinished"></translation> </message> <message> - <source>%1 linked.</source> - <translation>%1 vinculado.</translation> + <source>The file "%1" is not a valid plugin file!</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Found more than one plugin library for the current OS in "%1" ("%2" and "%3")!</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to find a plugin for the current OS in "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to load plugin "%1" - check the plugin interface!</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to delete old plugin at "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to copy plugin library from "%1" to "%2"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to move plugin library to "%1"</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>PluginManager</name> + <message> + <source>%1 lost link</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>%1 linked</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Plugin "%1" encountered a permanent error in positional data gathering</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Non-plugin found in plugin directory: "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Failed at loading manual plugin: %1</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>PluginUpdater</name> + <message> + <source>PluginUpdater</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>The following plugins can be updated.</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Select all</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Plugin</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Download-URL</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Do you want to update the selected plugins?</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to download plugin update for "%1" from "%2" (%3)</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Update for plugin "%1" failed due to too many redirects</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to download plugin update for "%1" from "%2" (HTTP status code %3)</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Successfully updated plugin "%1"</source> + <translation type="unfinished"></translation> </message> </context> <context> diff --git a/src/mumble/mumble_pt_PT.ts b/src/mumble/mumble_pt_PT.ts index dfa50ea20..f683c817d 100644 --- a/src/mumble/mumble_pt_PT.ts +++ b/src/mumble/mumble_pt_PT.ts @@ -3824,6 +3824,10 @@ Este campo descreve o tamanho de um dispositivo LCD. O tamanho é dado em pixels <source>User stopped listening to channel</source> <translation type="unfinished"></translation> </message> + <message> + <source>Plugin message</source> + <translation type="unfinished"></translation> + </message> </context> <context> <name>LogConfig</name> @@ -6175,12 +6179,13 @@ Valid actions are: <translation type="unfinished"></translation> </message> <message> - <source>Usage: mumble [options] [<url>] + <source>Usage: mumble [options] [<url> | <plugin_list>] <url> specifies a URL to connect to after startup instead of showing the connection window, and has the following form: mumble://[<username>[:<password>]@]<host>[:<port>][/<channel>[/<subchannel>...]][?version=<x.y.z>] +<plugin_list> is a list of plugin files that shall be installed The version query parameter has to be set in order to invoke the correct client version. It currently defaults to 1.2.0. @@ -6500,10 +6505,6 @@ Valid options are: <translation>Ligar novamente ao último servidor ao iniciar</translation> </message> <message> - <source>Download plugin and overlay updates on startup</source> - <translation>Tranferir atualizações de plugins e sobreposição ao iniciar</translation> - </message> - <message> <source>Privacy</source> <translation type="unfinished"></translation> </message> @@ -6528,6 +6529,14 @@ Prevents the client from sending potentially identifying information about the o <source>Hide public server list</source> <translation type="unfinished"></translation> </message> + <message> + <source>Check for plugin updates on startup</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Automatically download and install plugin updates</source> + <translation type="unfinished"></translation> + </message> </context> <context> <name>Overlay</name> @@ -7082,31 +7091,189 @@ Para atualizar estes ficheiros para suas últimas versões, clique no botão aba <translation>Nome</translation> </message> <message> - <source>Enabled</source> - <translation>Ativo</translation> + <source>Enable</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>PA</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>KeyEvents</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Install a plugin from a local file</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Install plugin...</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unload the currently selected plugin. This will remove it from the plugin list for the current session.</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unload</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>The plugin was installed successfully</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to deactivate all requested features for plugin "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to load plugin "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Whether the positional audio feature of this plugin should be enabled</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>This plugin does not provide support for positional audio</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Whether this plugin should be enabled</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>PluginInstaller</name> + <message> + <source>PluginInstaller</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>You are about to install the plugin listed below. Do you wish to proceed?</source> + <translation type="unfinished"></translation> + </message> + <message> + <source><html><head/><body><p><span style=" font-weight:600;">Name:</span></p></body></html></source> + <translation type="unfinished"></translation> + </message> + <message> + <source><html><head/><body><p><span style=" font-weight:600;">Version:</span></p></body></html></source> + <translation type="unfinished"></translation> + </message> + <message> + <source><html><head/><body><p><span style=" font-weight:600;">Author(s):</span></p></body></html></source> + <translation type="unfinished"></translation> + </message> + <message> + <source><html><head/><body><p><span style=" font-weight:600;">Description:</span></p></body></html></source> + <translation type="unfinished"></translation> + </message> + <message> + <source>&No</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>&Yes</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>The file "%1" is not a valid plugin file!</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Found more than one plugin library for the current OS in "%1" ("%2" and "%3")!</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to find a plugin for the current OS in "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to load plugin "%1" - check the plugin interface!</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to delete old plugin at "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to copy plugin library from "%1" to "%2"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to move plugin library to "%1"</source> + <translation type="unfinished"></translation> </message> </context> <context> - <name>Plugins</name> + <name>PluginManager</name> <message> - <source>Skipping plugin update in debug mode.</source> - <translation>Saltar atualização de plugin no modo de depuração.</translation> + <source>%1 lost link</source> + <translation type="unfinished"></translation> </message> <message> - <source>Downloaded new or updated plugin to %1.</source> - <translation>Transferido plugin novo ou atualizado para %1.</translation> + <source>%1 linked</source> + <translation type="unfinished"></translation> </message> <message> - <source>Failed to install new plugin to %1.</source> - <translation>Falha ao instalar a novo plugin para %1.</translation> + <source>Plugin "%1" encountered a permanent error in positional data gathering</source> + <translation type="unfinished"></translation> </message> <message> - <source>%1 lost link.</source> - <translation>%1 perdeu ligação.</translation> + <source>Non-plugin found in plugin directory: "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Failed at loading manual plugin: %1</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>PluginUpdater</name> + <message> + <source>PluginUpdater</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>The following plugins can be updated.</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Select all</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Plugin</source> + <translation type="unfinished"></translation> </message> <message> - <source>%1 linked.</source> - <translation>%1 ligado.</translation> + <source>Download-URL</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Do you want to update the selected plugins?</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to download plugin update for "%1" from "%2" (%3)</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Update for plugin "%1" failed due to too many redirects</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to download plugin update for "%1" from "%2" (HTTP status code %3)</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Successfully updated plugin "%1"</source> + <translation type="unfinished"></translation> </message> </context> <context> diff --git a/src/mumble/mumble_ro.ts b/src/mumble/mumble_ro.ts index ce4203450..be4b70950 100644 --- a/src/mumble/mumble_ro.ts +++ b/src/mumble/mumble_ro.ts @@ -3783,6 +3783,10 @@ This field describes the size of an LCD device. The size is given either in pixe <source>User stopped listening to channel</source> <translation type="unfinished"></translation> </message> + <message> + <source>Plugin message</source> + <translation type="unfinished"></translation> + </message> </context> <context> <name>LogConfig</name> @@ -6126,12 +6130,13 @@ Valid actions are: <translation type="unfinished"></translation> </message> <message> - <source>Usage: mumble [options] [<url>] + <source>Usage: mumble [options] [<url> | <plugin_list>] <url> specifies a URL to connect to after startup instead of showing the connection window, and has the following form: mumble://[<username>[:<password>]@]<host>[:<port>][/<channel>[/<subchannel>...]][?version=<x.y.z>] +<plugin_list> is a list of plugin files that shall be installed The version query parameter has to be set in order to invoke the correct client version. It currently defaults to 1.2.0. @@ -6451,10 +6456,6 @@ Valid options are: <translation type="unfinished"></translation> </message> <message> - <source>Download plugin and overlay updates on startup</source> - <translation type="unfinished"></translation> - </message> - <message> <source>Privacy</source> <translation type="unfinished"></translation> </message> @@ -6479,6 +6480,14 @@ Prevents the client from sending potentially identifying information about the o <source>Hide public server list</source> <translation type="unfinished"></translation> </message> + <message> + <source>Check for plugin updates on startup</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Automatically download and install plugin updates</source> + <translation type="unfinished"></translation> + </message> </context> <context> <name>Overlay</name> @@ -7029,30 +7038,188 @@ To upgrade these files to their latest versions, click the button below.</source <translation type="unfinished">Nume</translation> </message> <message> - <source>Enabled</source> + <source>Enable</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>PA</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>KeyEvents</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Install a plugin from a local file</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Install plugin...</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unload the currently selected plugin. This will remove it from the plugin list for the current session.</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unload</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>The plugin was installed successfully</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to deactivate all requested features for plugin "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to load plugin "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Whether the positional audio feature of this plugin should be enabled</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>This plugin does not provide support for positional audio</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Whether this plugin should be enabled</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>PluginInstaller</name> + <message> + <source>PluginInstaller</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>You are about to install the plugin listed below. Do you wish to proceed?</source> + <translation type="unfinished"></translation> + </message> + <message> + <source><html><head/><body><p><span style=" font-weight:600;">Name:</span></p></body></html></source> + <translation type="unfinished"></translation> + </message> + <message> + <source><html><head/><body><p><span style=" font-weight:600;">Version:</span></p></body></html></source> + <translation type="unfinished"></translation> + </message> + <message> + <source><html><head/><body><p><span style=" font-weight:600;">Author(s):</span></p></body></html></source> + <translation type="unfinished"></translation> + </message> + <message> + <source><html><head/><body><p><span style=" font-weight:600;">Description:</span></p></body></html></source> + <translation type="unfinished"></translation> + </message> + <message> + <source>&No</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>&Yes</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>The file "%1" is not a valid plugin file!</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Found more than one plugin library for the current OS in "%1" ("%2" and "%3")!</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to find a plugin for the current OS in "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to load plugin "%1" - check the plugin interface!</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to delete old plugin at "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to copy plugin library from "%1" to "%2"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to move plugin library to "%1"</source> <translation type="unfinished"></translation> </message> </context> <context> - <name>Plugins</name> + <name>PluginManager</name> + <message> + <source>%1 lost link</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>%1 linked</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Plugin "%1" encountered a permanent error in positional data gathering</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Non-plugin found in plugin directory: "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Failed at loading manual plugin: %1</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>PluginUpdater</name> + <message> + <source>PluginUpdater</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>The following plugins can be updated.</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Select all</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Plugin</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Download-URL</source> + <translation type="unfinished"></translation> + </message> <message> - <source>Skipping plugin update in debug mode.</source> + <source>Do you want to update the selected plugins?</source> <translation type="unfinished"></translation> </message> <message> - <source>Downloaded new or updated plugin to %1.</source> + <source>Unable to download plugin update for "%1" from "%2" (%3)</source> <translation type="unfinished"></translation> </message> <message> - <source>Failed to install new plugin to %1.</source> + <source>Update for plugin "%1" failed due to too many redirects</source> <translation type="unfinished"></translation> </message> <message> - <source>%1 lost link.</source> + <source>Unable to download plugin update for "%1" from "%2" (HTTP status code %3)</source> <translation type="unfinished"></translation> </message> <message> - <source>%1 linked.</source> + <source>Successfully updated plugin "%1"</source> <translation type="unfinished"></translation> </message> </context> diff --git a/src/mumble/mumble_ru.ts b/src/mumble/mumble_ru.ts index a1957a4a2..007d7a382 100644 --- a/src/mumble/mumble_ru.ts +++ b/src/mumble/mumble_ru.ts @@ -3792,6 +3792,10 @@ This field describes the size of an LCD device. The size is given either in pixe <source>User stopped listening to channel</source> <translation> Пользователь перестал слушать канал</translation> </message> + <message> + <source>Plugin message</source> + <translation type="unfinished"></translation> + </message> </context> <context> <name>LogConfig</name> @@ -6168,12 +6172,13 @@ Valid actions are: </translation> </message> <message> - <source>Usage: mumble [options] [<url>] + <source>Usage: mumble [options] [<url> | <plugin_list>] <url> specifies a URL to connect to after startup instead of showing the connection window, and has the following form: mumble://[<username>[:<password>]@]<host>[:<port>][/<channel>[/<subchannel>...]][?version=<x.y.z>] +<plugin_list> is a list of plugin files that shall be installed The version query parameter has to be set in order to invoke the correct client version. It currently defaults to 1.2.0. @@ -6493,10 +6498,6 @@ Valid options are: <translation>Подключаться к последнему серверу при запуске</translation> </message> <message> - <source>Download plugin and overlay updates on startup</source> - <translation>Загружать обновления плагинов и табло при запуске</translation> - </message> - <message> <source>Privacy</source> <translation>Конфиденциальность</translation> </message> @@ -6521,6 +6522,14 @@ Prevents the client from sending potentially identifying information about the o <source>Hide public server list</source> <translation>Скрыть список публичных серверов</translation> </message> + <message> + <source>Check for plugin updates on startup</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Automatically download and install plugin updates</source> + <translation type="unfinished"></translation> + </message> </context> <context> <name>Overlay</name> @@ -7075,31 +7084,189 @@ To upgrade these files to their latest versions, click the button below.</source <translation>Имя</translation> </message> <message> - <source>Enabled</source> - <translation>Включено</translation> + <source>Enable</source> + <translation type="unfinished">Включить</translation> + </message> + <message> + <source>PA</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>KeyEvents</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Install a plugin from a local file</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Install plugin...</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unload the currently selected plugin. This will remove it from the plugin list for the current session.</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unload</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>The plugin was installed successfully</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to deactivate all requested features for plugin "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to load plugin "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Whether the positional audio feature of this plugin should be enabled</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>This plugin does not provide support for positional audio</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Whether this plugin should be enabled</source> + <translation type="unfinished"></translation> </message> </context> <context> - <name>Plugins</name> + <name>PluginInstaller</name> <message> - <source>Skipping plugin update in debug mode.</source> - <translation>Пропустить обновление плагина в режиме отладки.</translation> + <source>PluginInstaller</source> + <translation type="unfinished"></translation> </message> <message> - <source>Downloaded new or updated plugin to %1.</source> - <translation>Загрузка нового или обновленного плагина в %1.</translation> + <source>You are about to install the plugin listed below. Do you wish to proceed?</source> + <translation type="unfinished"></translation> </message> <message> - <source>Failed to install new plugin to %1.</source> - <translation>Ошибка при установке нового плагина в %1.</translation> + <source><html><head/><body><p><span style=" font-weight:600;">Name:</span></p></body></html></source> + <translation type="unfinished"></translation> </message> <message> - <source>%1 lost link.</source> - <translation>%1 потерял связь.</translation> + <source><html><head/><body><p><span style=" font-weight:600;">Version:</span></p></body></html></source> + <translation type="unfinished"></translation> + </message> + <message> + <source><html><head/><body><p><span style=" font-weight:600;">Author(s):</span></p></body></html></source> + <translation type="unfinished"></translation> + </message> + <message> + <source><html><head/><body><p><span style=" font-weight:600;">Description:</span></p></body></html></source> + <translation type="unfinished"></translation> + </message> + <message> + <source>&No</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>&Yes</source> + <translation type="unfinished"></translation> </message> <message> - <source>%1 linked.</source> - <translation>%1 подключен.</translation> + <source>The file "%1" is not a valid plugin file!</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Found more than one plugin library for the current OS in "%1" ("%2" and "%3")!</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to find a plugin for the current OS in "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to load plugin "%1" - check the plugin interface!</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to delete old plugin at "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to copy plugin library from "%1" to "%2"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to move plugin library to "%1"</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>PluginManager</name> + <message> + <source>%1 lost link</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>%1 linked</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Plugin "%1" encountered a permanent error in positional data gathering</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Non-plugin found in plugin directory: "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Failed at loading manual plugin: %1</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>PluginUpdater</name> + <message> + <source>PluginUpdater</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>The following plugins can be updated.</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Select all</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Plugin</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Download-URL</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Do you want to update the selected plugins?</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to download plugin update for "%1" from "%2" (%3)</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Update for plugin "%1" failed due to too many redirects</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to download plugin update for "%1" from "%2" (HTTP status code %3)</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Successfully updated plugin "%1"</source> + <translation type="unfinished"></translation> </message> </context> <context> diff --git a/src/mumble/mumble_si.ts b/src/mumble/mumble_si.ts index 21842df7b..967c0a939 100644 --- a/src/mumble/mumble_si.ts +++ b/src/mumble/mumble_si.ts @@ -3753,6 +3753,10 @@ This field describes the size of an LCD device. The size is given either in pixe <source>%1 link</source> <translation type="unfinished"></translation> </message> + <message> + <source>Plugin message</source> + <translation type="unfinished"></translation> + </message> </context> <context> <name>LogConfig</name> @@ -6084,12 +6088,13 @@ Otherwise abort and check your certificate and username.</source> <translation type="unfinished"></translation> </message> <message> - <source>Usage: mumble [options] [<url>] + <source>Usage: mumble [options] [<url> | <plugin_list>] <url> specifies a URL to connect to after startup instead of showing the connection window, and has the following form: mumble://[<username>[:<password>]@]<host>[:<port>][/<channel>[/<subchannel>...]][?version=<x.y.z>] +<plugin_list> is a list of plugin files that shall be installed The version query parameter has to be set in order to invoke the correct client version. It currently defaults to 1.2.0. @@ -6409,10 +6414,6 @@ Prevents the client from sending potentially identifying information about the o <translation type="unfinished"></translation> </message> <message> - <source>Download plugin and overlay updates on startup</source> - <translation type="unfinished"></translation> - </message> - <message> <source>Submit anonymous statistics to the Mumble project</source> <translation type="unfinished"></translation> </message> @@ -6436,6 +6437,14 @@ Prevents the client from sending potentially identifying information about the o <source>Network</source> <translation type="unfinished"></translation> </message> + <message> + <source>Check for plugin updates on startup</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Automatically download and install plugin updates</source> + <translation type="unfinished"></translation> + </message> </context> <context> <name>Overlay</name> @@ -6939,10 +6948,6 @@ To upgrade these files to their latest versions, click the button below.</source <translation type="unfinished"></translation> </message> <message> - <source>Enabled</source> - <translation type="unfinished"></translation> - </message> - <message> <source>Reloads all plugins</source> <translation type="unfinished"></translation> </message> @@ -6986,27 +6991,189 @@ To upgrade these files to their latest versions, click the button below.</source <source>Plugin has no about function.</source> <translation type="unfinished"></translation> </message> + <message> + <source>Enable</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>PA</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>KeyEvents</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Install a plugin from a local file</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Install plugin...</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unload the currently selected plugin. This will remove it from the plugin list for the current session.</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unload</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>The plugin was installed successfully</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to deactivate all requested features for plugin "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to load plugin "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Whether the positional audio feature of this plugin should be enabled</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>This plugin does not provide support for positional audio</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Whether this plugin should be enabled</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>PluginInstaller</name> + <message> + <source>PluginInstaller</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>You are about to install the plugin listed below. Do you wish to proceed?</source> + <translation type="unfinished"></translation> + </message> + <message> + <source><html><head/><body><p><span style=" font-weight:600;">Name:</span></p></body></html></source> + <translation type="unfinished"></translation> + </message> + <message> + <source><html><head/><body><p><span style=" font-weight:600;">Version:</span></p></body></html></source> + <translation type="unfinished"></translation> + </message> + <message> + <source><html><head/><body><p><span style=" font-weight:600;">Author(s):</span></p></body></html></source> + <translation type="unfinished"></translation> + </message> + <message> + <source><html><head/><body><p><span style=" font-weight:600;">Description:</span></p></body></html></source> + <translation type="unfinished"></translation> + </message> + <message> + <source>&No</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>&Yes</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>The file "%1" is not a valid plugin file!</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Found more than one plugin library for the current OS in "%1" ("%2" and "%3")!</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to find a plugin for the current OS in "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to load plugin "%1" - check the plugin interface!</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to delete old plugin at "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to copy plugin library from "%1" to "%2"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to move plugin library to "%1"</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>PluginManager</name> + <message> + <source>%1 lost link</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>%1 linked</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Plugin "%1" encountered a permanent error in positional data gathering</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Non-plugin found in plugin directory: "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Failed at loading manual plugin: %1</source> + <translation type="unfinished"></translation> + </message> </context> <context> - <name>Plugins</name> + <name>PluginUpdater</name> + <message> + <source>PluginUpdater</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>The following plugins can be updated.</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Select all</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Plugin</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Download-URL</source> + <translation type="unfinished"></translation> + </message> <message> - <source>%1 lost link.</source> + <source>Do you want to update the selected plugins?</source> <translation type="unfinished"></translation> </message> <message> - <source>%1 linked.</source> + <source>Unable to download plugin update for "%1" from "%2" (%3)</source> <translation type="unfinished"></translation> </message> <message> - <source>Skipping plugin update in debug mode.</source> + <source>Update for plugin "%1" failed due to too many redirects</source> <translation type="unfinished"></translation> </message> <message> - <source>Downloaded new or updated plugin to %1.</source> + <source>Unable to download plugin update for "%1" from "%2" (HTTP status code %3)</source> <translation type="unfinished"></translation> </message> <message> - <source>Failed to install new plugin to %1.</source> + <source>Successfully updated plugin "%1"</source> <translation type="unfinished"></translation> </message> </context> diff --git a/src/mumble/mumble_sv.ts b/src/mumble/mumble_sv.ts index d6df92fe9..3f4d1708c 100644 --- a/src/mumble/mumble_sv.ts +++ b/src/mumble/mumble_sv.ts @@ -3838,6 +3838,10 @@ Detta fält beskriver storleken av en LCD-enhet. Storleken mäts i pixlar (för <source>User stopped listening to channel</source> <translation>Anändare slutade lyssna på kanalen</translation> </message> + <message> + <source>Plugin message</source> + <translation type="unfinished"></translation> + </message> </context> <context> <name>LogConfig</name> @@ -6214,12 +6218,13 @@ Giltiga åtgärder är: </translation> </message> <message> - <source>Usage: mumble [options] [<url>] + <source>Usage: mumble [options] [<url> | <plugin_list>] <url> specifies a URL to connect to after startup instead of showing the connection window, and has the following form: mumble://[<username>[:<password>]@]<host>[:<port>][/<channel>[/<subchannel>...]][?version=<x.y.z>] +<plugin_list> is a list of plugin files that shall be installed The version query parameter has to be set in order to invoke the correct client version. It currently defaults to 1.2.0. @@ -6269,61 +6274,7 @@ Valid options are: Otherwise the locale will be permanently saved to Mumble's settings. </source> - <translation>Användning: mumble [options] [<url>] - -<url> anger en URL som ska anslutas till efter uppstart istället för att visa -anslutningsfönstret, och har följande form: -mumble://[<användarnamn>[:<lösenord>]@]<host>[:<port>][/<kanal>[/<underkanal>...]][?version=<x.y.z>] - -Förfrågningsparametern version måste ställas in för att kunna åberopa -rätt klientversion. För närvarande är standardvärdet 1.2.0. - -Giltiga alternativ är: - -h, --help Visar denna hjälptext. - -m, --multiple - Tillåt att flera instanser av klienten startas. - -c, --config - Ange en alternativ konfigurationsfil. - Om du använder detta för att köra flera instanser av Mumble på en gång, - se till att ställa in ett alternativt "databas" -värde i konfigurationen. - -n, --noidentity - Dämpa inläsning av identitetsfiler (dvs. certifikat.) - -jn, --jackname <arg> - Ange anpassat Jack-klientnamn. - --license - Visa Mumble-licensen. - --authors - VIsa Mumble-skapare. - --third-party-licenses - Visa licenser för tredje-partmjukvara som används av Mumble. - --window-title-ext <arg> - Ställer in ett anpassat fönstertiteltillägg. - --dump-input-streams - Dumpa PCM-strömmar vid olika delar av ingångskedjan - (användbart för felsökningsändamål) - - rå mikrofoningång - - högtalaravläsning för ekodämpning - - bearbetad mikrofoningång - --print-echocancel-queue - Skriv ut på stdout ekoställningskön - (användbart för felsökningsändamål) ---translation-dir <dir> - Anger en ytterligare översättning för <dir> där - Mumble söker efter översättningsfiler som skrivs över - de medföljande - kataloger som läggs till på detta sätt har högre prioritet än - standardplatserna som används annars ---print-translation-dirs - Skriv ut de vägar som Mumble söker efter - översättningsfiler som skriver över de medföljande filerna. - (Användbart för översättare som testar sina översättningar) - --locale <locale> - Skriver över lokaladressen i Mumbles inställningar med en - lokal som motsvarar den angivna lokalsträngen. - Om formatet är ogiltigt kommer Mumble att göra ett fel. - Annars sparas språket permanent i - Mumbles inställningar. -</translation> + <translation type="unfinished"></translation> </message> </context> <context> @@ -6593,10 +6544,6 @@ Giltiga alternativ är: <translation>Återanslut till den senaste servern vid uppstart</translation> </message> <message> - <source>Download plugin and overlay updates on startup</source> - <translation>Ladda ner uppdateringar för insticksmoduler och överlag vid uppstart</translation> - </message> - <message> <source>Privacy</source> <translation>Sekretess</translation> </message> @@ -6622,6 +6569,14 @@ Förhindrar klienten från att skicka potentiellt identifierande information om <source>Hide public server list</source> <translation>Dölj offentlig serverlista</translation> </message> + <message> + <source>Check for plugin updates on startup</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Automatically download and install plugin updates</source> + <translation type="unfinished"></translation> + </message> </context> <context> <name>Overlay</name> @@ -7176,31 +7131,189 @@ Tryck på knappen nedan för att uppgradera dessa filer till de senaste versione <translation>Namn</translation> </message> <message> - <source>Enabled</source> - <translation>Aktiverad</translation> + <source>Enable</source> + <translation type="unfinished">Aktivera</translation> + </message> + <message> + <source>PA</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>KeyEvents</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Install a plugin from a local file</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Install plugin...</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unload the currently selected plugin. This will remove it from the plugin list for the current session.</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unload</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>The plugin was installed successfully</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to deactivate all requested features for plugin "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to load plugin "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Whether the positional audio feature of this plugin should be enabled</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>This plugin does not provide support for positional audio</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Whether this plugin should be enabled</source> + <translation type="unfinished"></translation> </message> </context> <context> - <name>Plugins</name> + <name>PluginInstaller</name> <message> - <source>Skipping plugin update in debug mode.</source> - <translation>Hoppar över uppdatering för insticksmoduler i felsökningsläge.</translation> + <source>PluginInstaller</source> + <translation type="unfinished"></translation> </message> <message> - <source>Downloaded new or updated plugin to %1.</source> - <translation>Laddade ner ny/uppdaterad insticksmodul till %1.</translation> + <source>You are about to install the plugin listed below. Do you wish to proceed?</source> + <translation type="unfinished"></translation> </message> <message> - <source>Failed to install new plugin to %1.</source> - <translation>Kude inte installera ny insticksmodul till %1.</translation> + <source><html><head/><body><p><span style=" font-weight:600;">Name:</span></p></body></html></source> + <translation type="unfinished"></translation> </message> <message> - <source>%1 lost link.</source> - <translation>%1 förlorade länk.</translation> + <source><html><head/><body><p><span style=" font-weight:600;">Version:</span></p></body></html></source> + <translation type="unfinished"></translation> </message> <message> - <source>%1 linked.</source> - <translation>%1 länkad.</translation> + <source><html><head/><body><p><span style=" font-weight:600;">Author(s):</span></p></body></html></source> + <translation type="unfinished"></translation> + </message> + <message> + <source><html><head/><body><p><span style=" font-weight:600;">Description:</span></p></body></html></source> + <translation type="unfinished"></translation> + </message> + <message> + <source>&No</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>&Yes</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>The file "%1" is not a valid plugin file!</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Found more than one plugin library for the current OS in "%1" ("%2" and "%3")!</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to find a plugin for the current OS in "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to load plugin "%1" - check the plugin interface!</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to delete old plugin at "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to copy plugin library from "%1" to "%2"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to move plugin library to "%1"</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>PluginManager</name> + <message> + <source>%1 lost link</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>%1 linked</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Plugin "%1" encountered a permanent error in positional data gathering</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Non-plugin found in plugin directory: "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Failed at loading manual plugin: %1</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>PluginUpdater</name> + <message> + <source>PluginUpdater</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>The following plugins can be updated.</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Select all</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Plugin</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Download-URL</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Do you want to update the selected plugins?</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to download plugin update for "%1" from "%2" (%3)</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Update for plugin "%1" failed due to too many redirects</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to download plugin update for "%1" from "%2" (HTTP status code %3)</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Successfully updated plugin "%1"</source> + <translation type="unfinished"></translation> </message> </context> <context> diff --git a/src/mumble/mumble_te.ts b/src/mumble/mumble_te.ts index 5615b89c2..3c894a928 100644 --- a/src/mumble/mumble_te.ts +++ b/src/mumble/mumble_te.ts @@ -3790,6 +3790,10 @@ This field describes the size of an LCD device. The size is given either in pixe <source>User stopped listening to channel</source> <translation type="unfinished"></translation> </message> + <message> + <source>Plugin message</source> + <translation type="unfinished"></translation> + </message> </context> <context> <name>LogConfig</name> @@ -6133,12 +6137,13 @@ Valid actions are: <translation type="unfinished"></translation> </message> <message> - <source>Usage: mumble [options] [<url>] + <source>Usage: mumble [options] [<url> | <plugin_list>] <url> specifies a URL to connect to after startup instead of showing the connection window, and has the following form: mumble://[<username>[:<password>]@]<host>[:<port>][/<channel>[/<subchannel>...]][?version=<x.y.z>] +<plugin_list> is a list of plugin files that shall be installed The version query parameter has to be set in order to invoke the correct client version. It currently defaults to 1.2.0. @@ -6464,10 +6469,6 @@ Valid options are: <translation type="unfinished"></translation> </message> <message> - <source>Download plugin and overlay updates on startup</source> - <translation type="unfinished"></translation> - </message> - <message> <source>Privacy</source> <translation type="unfinished"></translation> </message> @@ -6492,6 +6493,14 @@ Prevents the client from sending potentially identifying information about the o <source>Hide public server list</source> <translation type="unfinished"></translation> </message> + <message> + <source>Check for plugin updates on startup</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Automatically download and install plugin updates</source> + <translation type="unfinished"></translation> + </message> </context> <context> <name>Overlay</name> @@ -7042,30 +7051,188 @@ To upgrade these files to their latest versions, click the button below.</source <translation type="unfinished"> నామము</translation> </message> <message> - <source>Enabled</source> + <source>Enable</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>PA</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>KeyEvents</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Install a plugin from a local file</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Install plugin...</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unload the currently selected plugin. This will remove it from the plugin list for the current session.</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unload</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>The plugin was installed successfully</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to deactivate all requested features for plugin "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to load plugin "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Whether the positional audio feature of this plugin should be enabled</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>This plugin does not provide support for positional audio</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Whether this plugin should be enabled</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>PluginInstaller</name> + <message> + <source>PluginInstaller</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>You are about to install the plugin listed below. Do you wish to proceed?</source> + <translation type="unfinished"></translation> + </message> + <message> + <source><html><head/><body><p><span style=" font-weight:600;">Name:</span></p></body></html></source> + <translation type="unfinished"></translation> + </message> + <message> + <source><html><head/><body><p><span style=" font-weight:600;">Version:</span></p></body></html></source> + <translation type="unfinished"></translation> + </message> + <message> + <source><html><head/><body><p><span style=" font-weight:600;">Author(s):</span></p></body></html></source> + <translation type="unfinished"></translation> + </message> + <message> + <source><html><head/><body><p><span style=" font-weight:600;">Description:</span></p></body></html></source> + <translation type="unfinished"></translation> + </message> + <message> + <source>&No</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>&Yes</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>The file "%1" is not a valid plugin file!</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Found more than one plugin library for the current OS in "%1" ("%2" and "%3")!</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to find a plugin for the current OS in "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to load plugin "%1" - check the plugin interface!</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to delete old plugin at "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to copy plugin library from "%1" to "%2"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to move plugin library to "%1"</source> <translation type="unfinished"></translation> </message> </context> <context> - <name>Plugins</name> + <name>PluginManager</name> + <message> + <source>%1 lost link</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>%1 linked</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Plugin "%1" encountered a permanent error in positional data gathering</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Non-plugin found in plugin directory: "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Failed at loading manual plugin: %1</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>PluginUpdater</name> + <message> + <source>PluginUpdater</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>The following plugins can be updated.</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Select all</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Plugin</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Download-URL</source> + <translation type="unfinished"></translation> + </message> <message> - <source>Skipping plugin update in debug mode.</source> + <source>Do you want to update the selected plugins?</source> <translation type="unfinished"></translation> </message> <message> - <source>Downloaded new or updated plugin to %1.</source> + <source>Unable to download plugin update for "%1" from "%2" (%3)</source> <translation type="unfinished"></translation> </message> <message> - <source>Failed to install new plugin to %1.</source> + <source>Update for plugin "%1" failed due to too many redirects</source> <translation type="unfinished"></translation> </message> <message> - <source>%1 lost link.</source> + <source>Unable to download plugin update for "%1" from "%2" (HTTP status code %3)</source> <translation type="unfinished"></translation> </message> <message> - <source>%1 linked.</source> + <source>Successfully updated plugin "%1"</source> <translation type="unfinished"></translation> </message> </context> diff --git a/src/mumble/mumble_th.ts b/src/mumble/mumble_th.ts index c85c22f6d..bcff56ff3 100644 --- a/src/mumble/mumble_th.ts +++ b/src/mumble/mumble_th.ts @@ -3777,6 +3777,10 @@ This field describes the size of an LCD device. The size is given either in pixe <source>User stopped listening to channel</source> <translation type="unfinished"></translation> </message> + <message> + <source>Plugin message</source> + <translation type="unfinished"></translation> + </message> </context> <context> <name>LogConfig</name> @@ -6120,12 +6124,13 @@ Valid actions are: <translation type="unfinished"></translation> </message> <message> - <source>Usage: mumble [options] [<url>] + <source>Usage: mumble [options] [<url> | <plugin_list>] <url> specifies a URL to connect to after startup instead of showing the connection window, and has the following form: mumble://[<username>[:<password>]@]<host>[:<port>][/<channel>[/<subchannel>...]][?version=<x.y.z>] +<plugin_list> is a list of plugin files that shall be installed The version query parameter has to be set in order to invoke the correct client version. It currently defaults to 1.2.0. @@ -6445,10 +6450,6 @@ Valid options are: <translation type="unfinished"></translation> </message> <message> - <source>Download plugin and overlay updates on startup</source> - <translation type="unfinished"></translation> - </message> - <message> <source>Privacy</source> <translation type="unfinished"></translation> </message> @@ -6473,6 +6474,14 @@ Prevents the client from sending potentially identifying information about the o <source>Hide public server list</source> <translation type="unfinished"></translation> </message> + <message> + <source>Check for plugin updates on startup</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Automatically download and install plugin updates</source> + <translation type="unfinished"></translation> + </message> </context> <context> <name>Overlay</name> @@ -7023,30 +7032,188 @@ To upgrade these files to their latest versions, click the button below.</source <translation type="unfinished">ชื่อ</translation> </message> <message> - <source>Enabled</source> + <source>Enable</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>PA</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>KeyEvents</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Install a plugin from a local file</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Install plugin...</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unload the currently selected plugin. This will remove it from the plugin list for the current session.</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unload</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>The plugin was installed successfully</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to deactivate all requested features for plugin "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to load plugin "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Whether the positional audio feature of this plugin should be enabled</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>This plugin does not provide support for positional audio</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Whether this plugin should be enabled</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>PluginInstaller</name> + <message> + <source>PluginInstaller</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>You are about to install the plugin listed below. Do you wish to proceed?</source> + <translation type="unfinished"></translation> + </message> + <message> + <source><html><head/><body><p><span style=" font-weight:600;">Name:</span></p></body></html></source> + <translation type="unfinished"></translation> + </message> + <message> + <source><html><head/><body><p><span style=" font-weight:600;">Version:</span></p></body></html></source> + <translation type="unfinished"></translation> + </message> + <message> + <source><html><head/><body><p><span style=" font-weight:600;">Author(s):</span></p></body></html></source> + <translation type="unfinished"></translation> + </message> + <message> + <source><html><head/><body><p><span style=" font-weight:600;">Description:</span></p></body></html></source> + <translation type="unfinished"></translation> + </message> + <message> + <source>&No</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>&Yes</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>The file "%1" is not a valid plugin file!</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Found more than one plugin library for the current OS in "%1" ("%2" and "%3")!</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to find a plugin for the current OS in "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to load plugin "%1" - check the plugin interface!</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to delete old plugin at "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to copy plugin library from "%1" to "%2"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to move plugin library to "%1"</source> <translation type="unfinished"></translation> </message> </context> <context> - <name>Plugins</name> + <name>PluginManager</name> + <message> + <source>%1 lost link</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>%1 linked</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Plugin "%1" encountered a permanent error in positional data gathering</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Non-plugin found in plugin directory: "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Failed at loading manual plugin: %1</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>PluginUpdater</name> + <message> + <source>PluginUpdater</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>The following plugins can be updated.</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Select all</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Plugin</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Download-URL</source> + <translation type="unfinished"></translation> + </message> <message> - <source>Skipping plugin update in debug mode.</source> + <source>Do you want to update the selected plugins?</source> <translation type="unfinished"></translation> </message> <message> - <source>Downloaded new or updated plugin to %1.</source> + <source>Unable to download plugin update for "%1" from "%2" (%3)</source> <translation type="unfinished"></translation> </message> <message> - <source>Failed to install new plugin to %1.</source> + <source>Update for plugin "%1" failed due to too many redirects</source> <translation type="unfinished"></translation> </message> <message> - <source>%1 lost link.</source> + <source>Unable to download plugin update for "%1" from "%2" (HTTP status code %3)</source> <translation type="unfinished"></translation> </message> <message> - <source>%1 linked.</source> + <source>Successfully updated plugin "%1"</source> <translation type="unfinished"></translation> </message> </context> diff --git a/src/mumble/mumble_tr.ts b/src/mumble/mumble_tr.ts index 8c44ac139..524a04bf1 100644 --- a/src/mumble/mumble_tr.ts +++ b/src/mumble/mumble_tr.ts @@ -3836,6 +3836,10 @@ Bu alan LCD aygıtın boyutunu belirtir. Boyut ya piksel olarak (Grafik LCD ekra <source>User stopped listening to channel</source> <translation>Kullanıcı kanalı dinlemeye son verdi</translation> </message> + <message> + <source>Plugin message</source> + <translation type="unfinished"></translation> + </message> </context> <context> <name>LogConfig</name> @@ -6188,12 +6192,13 @@ Valid actions are: <translation type="unfinished"></translation> </message> <message> - <source>Usage: mumble [options] [<url>] + <source>Usage: mumble [options] [<url> | <plugin_list>] <url> specifies a URL to connect to after startup instead of showing the connection window, and has the following form: mumble://[<username>[:<password>]@]<host>[:<port>][/<channel>[/<subchannel>...]][?version=<x.y.z>] +<plugin_list> is a list of plugin files that shall be installed The version query parameter has to be set in order to invoke the correct client version. It currently defaults to 1.2.0. @@ -6513,10 +6518,6 @@ Valid options are: <translation>Başladığında son sunucuya bağlan</translation> </message> <message> - <source>Download plugin and overlay updates on startup</source> - <translation>Başladığında eklenti ve yerpaylaşan güncellemelerini indir</translation> - </message> - <message> <source>Privacy</source> <translation>Gizlilik</translation> </message> @@ -6542,6 +6543,14 @@ Prevents the client from sending potentially identifying information about the o <source>Hide public server list</source> <translation type="unfinished"></translation> </message> + <message> + <source>Check for plugin updates on startup</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Automatically download and install plugin updates</source> + <translation type="unfinished"></translation> + </message> </context> <context> <name>Overlay</name> @@ -7096,31 +7105,189 @@ Bu dosyaları son sürümlerine güncellemek için aşağıdaki düğmeyi tıkla <translation>İsim</translation> </message> <message> - <source>Enabled</source> - <translation>Etkin</translation> + <source>Enable</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>PA</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>KeyEvents</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Install a plugin from a local file</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Install plugin...</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unload the currently selected plugin. This will remove it from the plugin list for the current session.</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unload</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>The plugin was installed successfully</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to deactivate all requested features for plugin "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to load plugin "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Whether the positional audio feature of this plugin should be enabled</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>This plugin does not provide support for positional audio</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Whether this plugin should be enabled</source> + <translation type="unfinished"></translation> </message> </context> <context> - <name>Plugins</name> + <name>PluginInstaller</name> <message> - <source>Skipping plugin update in debug mode.</source> - <translation>Hata ayıklama kipinde eklenti güncellemesi atlanıyor.</translation> + <source>PluginInstaller</source> + <translation type="unfinished"></translation> </message> <message> - <source>Downloaded new or updated plugin to %1.</source> - <translation>Yeni ya da güncelleştirilmiş eklenti %1 konumuna indirildi.</translation> + <source>You are about to install the plugin listed below. Do you wish to proceed?</source> + <translation type="unfinished"></translation> </message> <message> - <source>Failed to install new plugin to %1.</source> - <translation>Yeni eklenti %1 konumuna kurulamadı.</translation> + <source><html><head/><body><p><span style=" font-weight:600;">Name:</span></p></body></html></source> + <translation type="unfinished"></translation> </message> <message> - <source>%1 lost link.</source> - <translation>%1 bağlantı kaybetti.</translation> + <source><html><head/><body><p><span style=" font-weight:600;">Version:</span></p></body></html></source> + <translation type="unfinished"></translation> </message> <message> - <source>%1 linked.</source> - <translation>%1 bağlandı.</translation> + <source><html><head/><body><p><span style=" font-weight:600;">Author(s):</span></p></body></html></source> + <translation type="unfinished"></translation> + </message> + <message> + <source><html><head/><body><p><span style=" font-weight:600;">Description:</span></p></body></html></source> + <translation type="unfinished"></translation> + </message> + <message> + <source>&No</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>&Yes</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>The file "%1" is not a valid plugin file!</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Found more than one plugin library for the current OS in "%1" ("%2" and "%3")!</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to find a plugin for the current OS in "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to load plugin "%1" - check the plugin interface!</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to delete old plugin at "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to copy plugin library from "%1" to "%2"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to move plugin library to "%1"</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>PluginManager</name> + <message> + <source>%1 lost link</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>%1 linked</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Plugin "%1" encountered a permanent error in positional data gathering</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Non-plugin found in plugin directory: "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Failed at loading manual plugin: %1</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>PluginUpdater</name> + <message> + <source>PluginUpdater</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>The following plugins can be updated.</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Select all</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Plugin</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Download-URL</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Do you want to update the selected plugins?</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to download plugin update for "%1" from "%2" (%3)</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Update for plugin "%1" failed due to too many redirects</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to download plugin update for "%1" from "%2" (HTTP status code %3)</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Successfully updated plugin "%1"</source> + <translation type="unfinished"></translation> </message> </context> <context> diff --git a/src/mumble/mumble_uk.ts b/src/mumble/mumble_uk.ts index c58ae0fd3..3b075ef7e 100644 --- a/src/mumble/mumble_uk.ts +++ b/src/mumble/mumble_uk.ts @@ -3779,6 +3779,10 @@ This field describes the size of an LCD device. The size is given either in pixe <source>User stopped listening to channel</source> <translation type="unfinished"></translation> </message> + <message> + <source>Plugin message</source> + <translation type="unfinished"></translation> + </message> </context> <context> <name>LogConfig</name> @@ -6122,12 +6126,13 @@ Valid actions are: <translation type="unfinished"></translation> </message> <message> - <source>Usage: mumble [options] [<url>] + <source>Usage: mumble [options] [<url> | <plugin_list>] <url> specifies a URL to connect to after startup instead of showing the connection window, and has the following form: mumble://[<username>[:<password>]@]<host>[:<port>][/<channel>[/<subchannel>...]][?version=<x.y.z>] +<plugin_list> is a list of plugin files that shall be installed The version query parameter has to be set in order to invoke the correct client version. It currently defaults to 1.2.0. @@ -6447,10 +6452,6 @@ Valid options are: <translation type="unfinished"></translation> </message> <message> - <source>Download plugin and overlay updates on startup</source> - <translation type="unfinished"></translation> - </message> - <message> <source>Privacy</source> <translation type="unfinished"></translation> </message> @@ -6475,6 +6476,14 @@ Prevents the client from sending potentially identifying information about the o <source>Hide public server list</source> <translation type="unfinished"></translation> </message> + <message> + <source>Check for plugin updates on startup</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Automatically download and install plugin updates</source> + <translation type="unfinished"></translation> + </message> </context> <context> <name>Overlay</name> @@ -7025,30 +7034,188 @@ To upgrade these files to their latest versions, click the button below.</source <translation type="unfinished">Ім'я</translation> </message> <message> - <source>Enabled</source> + <source>Enable</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>PA</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>KeyEvents</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Install a plugin from a local file</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Install plugin...</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unload the currently selected plugin. This will remove it from the plugin list for the current session.</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unload</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>The plugin was installed successfully</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to deactivate all requested features for plugin "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to load plugin "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Whether the positional audio feature of this plugin should be enabled</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>This plugin does not provide support for positional audio</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Whether this plugin should be enabled</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>PluginInstaller</name> + <message> + <source>PluginInstaller</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>You are about to install the plugin listed below. Do you wish to proceed?</source> + <translation type="unfinished"></translation> + </message> + <message> + <source><html><head/><body><p><span style=" font-weight:600;">Name:</span></p></body></html></source> + <translation type="unfinished"></translation> + </message> + <message> + <source><html><head/><body><p><span style=" font-weight:600;">Version:</span></p></body></html></source> + <translation type="unfinished"></translation> + </message> + <message> + <source><html><head/><body><p><span style=" font-weight:600;">Author(s):</span></p></body></html></source> + <translation type="unfinished"></translation> + </message> + <message> + <source><html><head/><body><p><span style=" font-weight:600;">Description:</span></p></body></html></source> + <translation type="unfinished"></translation> + </message> + <message> + <source>&No</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>&Yes</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>The file "%1" is not a valid plugin file!</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Found more than one plugin library for the current OS in "%1" ("%2" and "%3")!</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to find a plugin for the current OS in "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to load plugin "%1" - check the plugin interface!</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to delete old plugin at "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to copy plugin library from "%1" to "%2"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to move plugin library to "%1"</source> <translation type="unfinished"></translation> </message> </context> <context> - <name>Plugins</name> + <name>PluginManager</name> + <message> + <source>%1 lost link</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>%1 linked</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Plugin "%1" encountered a permanent error in positional data gathering</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Non-plugin found in plugin directory: "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Failed at loading manual plugin: %1</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>PluginUpdater</name> + <message> + <source>PluginUpdater</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>The following plugins can be updated.</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Select all</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Plugin</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Download-URL</source> + <translation type="unfinished"></translation> + </message> <message> - <source>Skipping plugin update in debug mode.</source> + <source>Do you want to update the selected plugins?</source> <translation type="unfinished"></translation> </message> <message> - <source>Downloaded new or updated plugin to %1.</source> + <source>Unable to download plugin update for "%1" from "%2" (%3)</source> <translation type="unfinished"></translation> </message> <message> - <source>Failed to install new plugin to %1.</source> + <source>Update for plugin "%1" failed due to too many redirects</source> <translation type="unfinished"></translation> </message> <message> - <source>%1 lost link.</source> + <source>Unable to download plugin update for "%1" from "%2" (HTTP status code %3)</source> <translation type="unfinished"></translation> </message> <message> - <source>%1 linked.</source> + <source>Successfully updated plugin "%1"</source> <translation type="unfinished"></translation> </message> </context> diff --git a/src/mumble/mumble_zh_CN.ts b/src/mumble/mumble_zh_CN.ts index d3527a07f..df68662bb 100644 --- a/src/mumble/mumble_zh_CN.ts +++ b/src/mumble/mumble_zh_CN.ts @@ -3837,6 +3837,10 @@ This field describes the size of an LCD device. The size is given either in pixe <source>User stopped listening to channel</source> <translation>用户停止监听频道</translation> </message> + <message> + <source>Plugin message</source> + <translation type="unfinished"></translation> + </message> </context> <context> <name>LogConfig</name> @@ -6212,12 +6216,13 @@ Valid actions are: </translation> </message> <message> - <source>Usage: mumble [options] [<url>] + <source>Usage: mumble [options] [<url> | <plugin_list>] <url> specifies a URL to connect to after startup instead of showing the connection window, and has the following form: mumble://[<username>[:<password>]@]<host>[:<port>][/<channel>[/<subchannel>...]][?version=<x.y.z>] +<plugin_list> is a list of plugin files that shall be installed The version query parameter has to be set in order to invoke the correct client version. It currently defaults to 1.2.0. @@ -6267,54 +6272,7 @@ Valid options are: Otherwise the locale will be permanently saved to Mumble's settings. </source> - <translation>用法:mumble [选项] [<URL>] - -<URL> 指定启动时连接的 URL,而不是显示连接窗口,URL 的格式为: -mumble://[<用户名>[:<密码>]@]<主机名>[:<端口>][/<频道名>[/<子频道名>...]][?version=<x.y.z>] - -必须设置 version 请求参数以调用正确的客户端版本,当前的默认值为 1.2.0。 - -可用的选项: - -h, --help 显示此帮助信息并退出。 - -m, --multiple - 允许启动多个客户端实例。 - -c, --config - 指定替代配置文件。 - 如果您使用此参数同时运行多个 Mumble 实例, - 请确保在配置文件内设置替代 'database' 选项。 - -n, --noidentity - 禁止加载身份认证文件(即证书)。 - -jn, --jackname <参数> - 设置自定义 Jack 客户端名称。 - --license - 显示 Mumble 许可。 - --authors - 显示 Mumble 作者。 - --third-party-licenses - 显示 Mumble 使用的第三方软件的许可。 - --window-title-ext <参数> - 设置自定义窗口标题后缀名。 - --dump-input-streams - 转储输入链上各部分的 PCM 流。 - (适用于调试目的) - - 原始麦克风输入 - - 扬声器回声消除重读取 - - 已处理麦克风输入 - --print-echocancel-queue - 向标准输出打印回声消除队列状态。 - (适用于调试目的) - --translation-dir <目录> - 指定一个额外的目录,Mumble 会在其中搜索翻译文件 - 来覆盖内置的翻译。通过此方式添加的目录比其它情况 - 下的默认位置具有更高的优先级。 - --print-translation-dirs - 输出 Mumble 会在哪些目录搜索翻译文件以覆盖内置翻译。 - (适用于译者测试自己的翻译) - --locale <区域语言代码> - 用指定字符串对应的语言覆盖 Mumble 的语言设置。 - 如果字符串格式无效,Mumble 会出错。 - 否则,指定的语言会永久保存到 Mumble 设置中。 -</translation> + <translation type="unfinished"></translation> </message> </context> <context> @@ -6584,10 +6542,6 @@ mumble://[<用户名>[:<密码>]@]<主机名>[:<端口>] <translation>启动时自动连接上次的服务器</translation> </message> <message> - <source>Download plugin and overlay updates on startup</source> - <translation>启动时下载插件和游戏内界面更新</translation> - </message> - <message> <source>Privacy</source> <translation>隐私</translation> </message> @@ -6613,6 +6567,14 @@ Prevents the client from sending potentially identifying information about the o <source>Hide public server list</source> <translation>隐藏公共服务器列表</translation> </message> + <message> + <source>Check for plugin updates on startup</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Automatically download and install plugin updates</source> + <translation type="unfinished"></translation> + </message> </context> <context> <name>Overlay</name> @@ -7167,31 +7129,189 @@ To upgrade these files to their latest versions, click the button below.</source <translation>名称</translation> </message> <message> - <source>Enabled</source> - <translation>启用</translation> + <source>Enable</source> + <translation type="unfinished">启用</translation> + </message> + <message> + <source>PA</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>KeyEvents</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Install a plugin from a local file</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Install plugin...</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unload the currently selected plugin. This will remove it from the plugin list for the current session.</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unload</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>The plugin was installed successfully</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to deactivate all requested features for plugin "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to load plugin "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Whether the positional audio feature of this plugin should be enabled</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>This plugin does not provide support for positional audio</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Whether this plugin should be enabled</source> + <translation type="unfinished"></translation> </message> </context> <context> - <name>Plugins</name> + <name>PluginInstaller</name> <message> - <source>Skipping plugin update in debug mode.</source> - <translation>在调试模式跳过插件更新。</translation> + <source>PluginInstaller</source> + <translation type="unfinished"></translation> </message> <message> - <source>Downloaded new or updated plugin to %1.</source> - <translation>下载新版或升级插件到 %1。</translation> + <source>You are about to install the plugin listed below. Do you wish to proceed?</source> + <translation type="unfinished"></translation> </message> <message> - <source>Failed to install new plugin to %1.</source> - <translation>安装新插件到 %1 失败。</translation> + <source><html><head/><body><p><span style=" font-weight:600;">Name:</span></p></body></html></source> + <translation type="unfinished"></translation> </message> <message> - <source>%1 lost link.</source> - <translation>%1 链接丢失。</translation> + <source><html><head/><body><p><span style=" font-weight:600;">Version:</span></p></body></html></source> + <translation type="unfinished"></translation> </message> <message> - <source>%1 linked.</source> - <translation>%1 已链接。</translation> + <source><html><head/><body><p><span style=" font-weight:600;">Author(s):</span></p></body></html></source> + <translation type="unfinished"></translation> + </message> + <message> + <source><html><head/><body><p><span style=" font-weight:600;">Description:</span></p></body></html></source> + <translation type="unfinished"></translation> + </message> + <message> + <source>&No</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>&Yes</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>The file "%1" is not a valid plugin file!</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Found more than one plugin library for the current OS in "%1" ("%2" and "%3")!</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to find a plugin for the current OS in "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to load plugin "%1" - check the plugin interface!</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to delete old plugin at "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to copy plugin library from "%1" to "%2"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to move plugin library to "%1"</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>PluginManager</name> + <message> + <source>%1 lost link</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>%1 linked</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Plugin "%1" encountered a permanent error in positional data gathering</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Non-plugin found in plugin directory: "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Failed at loading manual plugin: %1</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>PluginUpdater</name> + <message> + <source>PluginUpdater</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>The following plugins can be updated.</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Select all</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Plugin</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Download-URL</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Do you want to update the selected plugins?</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to download plugin update for "%1" from "%2" (%3)</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Update for plugin "%1" failed due to too many redirects</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to download plugin update for "%1" from "%2" (HTTP status code %3)</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Successfully updated plugin "%1"</source> + <translation type="unfinished"></translation> </message> </context> <context> diff --git a/src/mumble/mumble_zh_HK.ts b/src/mumble/mumble_zh_HK.ts index 3f4d56f71..3cfe84124 100644 --- a/src/mumble/mumble_zh_HK.ts +++ b/src/mumble/mumble_zh_HK.ts @@ -3777,6 +3777,10 @@ This field describes the size of an LCD device. The size is given either in pixe <source>User stopped listening to channel</source> <translation type="unfinished"></translation> </message> + <message> + <source>Plugin message</source> + <translation type="unfinished"></translation> + </message> </context> <context> <name>LogConfig</name> @@ -6125,12 +6129,13 @@ Valid actions are: <translation type="unfinished"></translation> </message> <message> - <source>Usage: mumble [options] [<url>] + <source>Usage: mumble [options] [<url> | <plugin_list>] <url> specifies a URL to connect to after startup instead of showing the connection window, and has the following form: mumble://[<username>[:<password>]@]<host>[:<port>][/<channel>[/<subchannel>...]][?version=<x.y.z>] +<plugin_list> is a list of plugin files that shall be installed The version query parameter has to be set in order to invoke the correct client version. It currently defaults to 1.2.0. @@ -6450,10 +6455,6 @@ Valid options are: <translation>啟動時自動連接到最後使用的伺服器</translation> </message> <message> - <source>Download plugin and overlay updates on startup</source> - <translation>啟動時下載外掛與浮動視窗的更新</translation> - </message> - <message> <source>Privacy</source> <translation type="unfinished"></translation> </message> @@ -6478,6 +6479,14 @@ Prevents the client from sending potentially identifying information about the o <source>Hide public server list</source> <translation type="unfinished"></translation> </message> + <message> + <source>Check for plugin updates on startup</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Automatically download and install plugin updates</source> + <translation type="unfinished"></translation> + </message> </context> <context> <name>Overlay</name> @@ -7032,31 +7041,189 @@ To upgrade these files to their latest versions, click the button below.</source <translation>名稱</translation> </message> <message> - <source>Enabled</source> - <translation>啟用</translation> + <source>Enable</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>PA</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>KeyEvents</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Install a plugin from a local file</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Install plugin...</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unload the currently selected plugin. This will remove it from the plugin list for the current session.</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unload</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>The plugin was installed successfully</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to deactivate all requested features for plugin "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to load plugin "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Whether the positional audio feature of this plugin should be enabled</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>This plugin does not provide support for positional audio</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Whether this plugin should be enabled</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>PluginInstaller</name> + <message> + <source>PluginInstaller</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>You are about to install the plugin listed below. Do you wish to proceed?</source> + <translation type="unfinished"></translation> + </message> + <message> + <source><html><head/><body><p><span style=" font-weight:600;">Name:</span></p></body></html></source> + <translation type="unfinished"></translation> + </message> + <message> + <source><html><head/><body><p><span style=" font-weight:600;">Version:</span></p></body></html></source> + <translation type="unfinished"></translation> + </message> + <message> + <source><html><head/><body><p><span style=" font-weight:600;">Author(s):</span></p></body></html></source> + <translation type="unfinished"></translation> + </message> + <message> + <source><html><head/><body><p><span style=" font-weight:600;">Description:</span></p></body></html></source> + <translation type="unfinished"></translation> + </message> + <message> + <source>&No</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>&Yes</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>The file "%1" is not a valid plugin file!</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Found more than one plugin library for the current OS in "%1" ("%2" and "%3")!</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to find a plugin for the current OS in "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to load plugin "%1" - check the plugin interface!</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to delete old plugin at "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to copy plugin library from "%1" to "%2"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to move plugin library to "%1"</source> + <translation type="unfinished"></translation> </message> </context> <context> - <name>Plugins</name> + <name>PluginManager</name> <message> - <source>Skipping plugin update in debug mode.</source> - <translation>在除錯模式中忽略外掛更新訊息。</translation> + <source>%1 lost link</source> + <translation type="unfinished"></translation> </message> <message> - <source>Downloaded new or updated plugin to %1.</source> - <translation>下載或更新 %1 外掛。</translation> + <source>%1 linked</source> + <translation type="unfinished"></translation> </message> <message> - <source>Failed to install new plugin to %1.</source> - <translation>安裝 %1 外掛失敗。</translation> + <source>Plugin "%1" encountered a permanent error in positional data gathering</source> + <translation type="unfinished"></translation> </message> <message> - <source>%1 lost link.</source> - <translation>%1 失去關聯。</translation> + <source>Non-plugin found in plugin directory: "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Failed at loading manual plugin: %1</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>PluginUpdater</name> + <message> + <source>PluginUpdater</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>The following plugins can be updated.</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Select all</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Plugin</source> + <translation type="unfinished"></translation> </message> <message> - <source>%1 linked.</source> - <translation>%1 已關聯。</translation> + <source>Download-URL</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Do you want to update the selected plugins?</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to download plugin update for "%1" from "%2" (%3)</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Update for plugin "%1" failed due to too many redirects</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to download plugin update for "%1" from "%2" (HTTP status code %3)</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Successfully updated plugin "%1"</source> + <translation type="unfinished"></translation> </message> </context> <context> diff --git a/src/mumble/mumble_zh_TW.ts b/src/mumble/mumble_zh_TW.ts index 2ff9d3f8b..979b4f3c8 100644 --- a/src/mumble/mumble_zh_TW.ts +++ b/src/mumble/mumble_zh_TW.ts @@ -3805,6 +3805,10 @@ This field describes the size of an LCD device. The size is given either in pixe <source>User stopped listening to channel</source> <translation type="unfinished"></translation> </message> + <message> + <source>Plugin message</source> + <translation type="unfinished"></translation> + </message> </context> <context> <name>LogConfig</name> @@ -6148,12 +6152,13 @@ Valid actions are: <translation type="unfinished"></translation> </message> <message> - <source>Usage: mumble [options] [<url>] + <source>Usage: mumble [options] [<url> | <plugin_list>] <url> specifies a URL to connect to after startup instead of showing the connection window, and has the following form: mumble://[<username>[:<password>]@]<host>[:<port>][/<channel>[/<subchannel>...]][?version=<x.y.z>] +<plugin_list> is a list of plugin files that shall be installed The version query parameter has to be set in order to invoke the correct client version. It currently defaults to 1.2.0. @@ -6473,10 +6478,6 @@ Valid options are: <translation>啟動時自動連接到最後使用的伺服器</translation> </message> <message> - <source>Download plugin and overlay updates on startup</source> - <translation>啟動時下載外掛與浮動視窗的更新</translation> - </message> - <message> <source>Privacy</source> <translation type="unfinished"></translation> </message> @@ -6501,6 +6502,14 @@ Prevents the client from sending potentially identifying information about the o <source>Hide public server list</source> <translation type="unfinished"></translation> </message> + <message> + <source>Check for plugin updates on startup</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Automatically download and install plugin updates</source> + <translation type="unfinished"></translation> + </message> </context> <context> <name>Overlay</name> @@ -7054,31 +7063,189 @@ To upgrade these files to their latest versions, click the button below.</source <translation>名稱</translation> </message> <message> - <source>Enabled</source> - <translation>已啟用</translation> + <source>Enable</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>PA</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>KeyEvents</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Install a plugin from a local file</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Install plugin...</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unload the currently selected plugin. This will remove it from the plugin list for the current session.</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unload</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>The plugin was installed successfully</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to deactivate all requested features for plugin "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to load plugin "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Whether the positional audio feature of this plugin should be enabled</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>This plugin does not provide support for positional audio</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Whether this plugin has the permission to be listening to all keyboard events that occur while Mumble has focus</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Whether this plugin should be enabled</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>PluginInstaller</name> + <message> + <source>PluginInstaller</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>You are about to install the plugin listed below. Do you wish to proceed?</source> + <translation type="unfinished"></translation> + </message> + <message> + <source><html><head/><body><p><span style=" font-weight:600;">Name:</span></p></body></html></source> + <translation type="unfinished"></translation> + </message> + <message> + <source><html><head/><body><p><span style=" font-weight:600;">Version:</span></p></body></html></source> + <translation type="unfinished"></translation> + </message> + <message> + <source><html><head/><body><p><span style=" font-weight:600;">Author(s):</span></p></body></html></source> + <translation type="unfinished"></translation> + </message> + <message> + <source><html><head/><body><p><span style=" font-weight:600;">Description:</span></p></body></html></source> + <translation type="unfinished"></translation> + </message> + <message> + <source>&No</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>&Yes</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>The file "%1" is not a valid plugin file!</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Found more than one plugin library for the current OS in "%1" ("%2" and "%3")!</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to find a plugin for the current OS in "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to load plugin "%1" - check the plugin interface!</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to delete old plugin at "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to copy plugin library from "%1" to "%2"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to move plugin library to "%1"</source> + <translation type="unfinished"></translation> </message> </context> <context> - <name>Plugins</name> + <name>PluginManager</name> <message> - <source>Skipping plugin update in debug mode.</source> - <translation>在除錯模式忽略外掛更新訊息。</translation> + <source>%1 lost link</source> + <translation type="unfinished"></translation> </message> <message> - <source>Downloaded new or updated plugin to %1.</source> - <translation>下載或更新 %1 外掛。</translation> + <source>%1 linked</source> + <translation type="unfinished"></translation> </message> <message> - <source>Failed to install new plugin to %1.</source> - <translation>安裝 %1 外掛失敗。</translation> + <source>Plugin "%1" encountered a permanent error in positional data gathering</source> + <translation type="unfinished"></translation> </message> <message> - <source>%1 lost link.</source> - <translation>%1 連接遺失。</translation> + <source>Non-plugin found in plugin directory: "%1"</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Failed at loading manual plugin: %1</source> + <translation type="unfinished"></translation> + </message> +</context> +<context> + <name>PluginUpdater</name> + <message> + <source>PluginUpdater</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>The following plugins can be updated.</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Select all</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Plugin</source> + <translation type="unfinished"></translation> </message> <message> - <source>%1 linked.</source> - <translation>%1 已連接。</translation> + <source>Download-URL</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Do you want to update the selected plugins?</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to download plugin update for "%1" from "%2" (%3)</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Update for plugin "%1" failed due to too many redirects</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Unable to download plugin update for "%1" from "%2" (HTTP status code %3)</source> + <translation type="unfinished"></translation> + </message> + <message> + <source>Successfully updated plugin "%1"</source> + <translation type="unfinished"></translation> </message> </context> <context> 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; |