From abfbf41ccabaa48f2a68087c8cfbba9bbbe197db Mon Sep 17 00:00:00 2001 From: Rob-NY Date: Mon, 17 Oct 2022 14:33:38 -0400 Subject: [PATCH] jamulusserver/* RPC methods; connection notifications, chat notify, chat send broadcast/connection --- docs/JSON-RPC.md | 102 ++++++++++++++++++++++++++ src/channel.cpp | 17 ++++- src/main.cpp | 1 + src/rpcserver.h | 37 ++++++++++ src/server.cpp | 59 +++++++++++++-- src/server.h | 12 ++- src/serverrpc.cpp | 181 ++++++++++++++++++++++++++++++++++++++++++++++ src/serverrpc.h | 1 + 8 files changed, 399 insertions(+), 11 deletions(-) diff --git a/docs/JSON-RPC.md b/docs/JSON-RPC.md index be38082d5e..8cd5e31bf1 100644 --- a/docs/JSON-RPC.md +++ b/docs/JSON-RPC.md @@ -297,6 +297,41 @@ Results: | result | string | Always "acknowledged". To check if the recording was restarted or if there is any error, call `jamulusserver/getRecorderStatus` again. | +### jamulusserver/sendBroadcastChat + +Send a chat message to all connected clients. Messages from the server are not escaped and can contain HTML as defined for QTextBrowser. + +Parameters: + +| Name | Type | Description | +| --- | --- | --- | +| params.textMessage | string | The chat message to be sent. | + +Results: + +| Name | Type | Description | +| --- | --- | --- | +| result | string | Always "ok". | + + +### jamulusserver/sendChat + +Send a chat message to the channel identified by a specificc address. The chat should be pre-escaped if necessary prior to calling this method. + +Parameters: + +| Name | Type | Description | +| --- | --- | --- | +| params.address | string | The full channel IP address as a string XXX.XXX.XXX.XXX:PPPPP | +| params.textMessage | string | The chat message to be sent. | + +Results: + +| Name | Type | Description | +| --- | --- | --- | +| result | string | "ok" if channel could be determined and message sent. | + + ### jamulusserver/setRecordingDirectory Sets the server recording directory. @@ -445,3 +480,70 @@ Parameters: | params | object | No parameters (empty object). | +### jamulusserver/chatReceived + +Emitted when a chat text is received. Server-generated chats are not included in this notification. + +Parameters: + +| Name | Type | Description | +| --- | --- | --- | +| params.id | string | The channel ID generating the chat. | +| params.name | string | The user name generating the chat. | +| params.address | number | The address of the channel generating the chat. | +| params.stamp | string | The date/time of the chat (ISO 8601 format, in server configured timezone). | +| params.text | string | The chat text. | + + +### jamulusserver/clientConnect + +Emitted when a new client connects to the server. + +Results: + +| Name | Type | Description | +| --- | --- | --- | +| result.name | string | The client’s name. | +| result.address | string | The client’s address (ip:port). | +| result.instrumentCode | number | The id of the user's instrument. | +| result.instrumentName | number | The text name of the user's instrument. | +| result.city | string | The user's city name. | +| result.countryCode | number | The id of the country specified by the user (see QLocale::Country). | +| result.countryName | number | The text name of the user's country (see QLocale::Country). | +| result.skillLevelCode | number | The user's skill level id. | +| result.skillLevelName | number | The user's skill level text name. | + + +### jamulusserver/clientDisconnect + +Emitted when a client disconnects from the server. + +Results: + +| Name | Type | Description | +| --- | --- | --- | +| result.id | string | The client channel id. | +| result.name | string | The client’s name. | +| result.address | string | The client’s address (ip:port). | + + +### jamulusserver/clientInfoChanged + +Emitted when a client changes their information (name, instrument, country, city, skill level). + +Results: + +| Name | Type | Description | +| --- | --- | --- | +| result.oldName | string | The client’s name just prior to this change. | +| result.name | string | The client’s name (new one, if change). | +| result.address | string | The client’s address (ip:port). | +| result.instrumentCode | number | The id of the user's instrument. | +| result.instrumentName | number | The text name of the user's instrument. | +| result.city | string | The user's city name. | +| result.countryCode | number | The id of the country specified by the user (see QLocale::Country). | +| result.countryName | number | The text name of the user's country (see QLocale::Country). | +| result.skillLevelCode | number | The user's skill level id. | +| result.skillLevelName | number | The user's skill level text name. | + + diff --git a/src/channel.cpp b/src/channel.cpp index c6df1f39b4..36fc8a4bec 100644 --- a/src/channel.cpp +++ b/src/channel.cpp @@ -22,6 +22,7 @@ * \******************************************************************************/ +#include "rpcserver.h" // Here and not in channel.h to avoid circular issue #include "channel.h" // CChannel implementation ***************************************************** @@ -337,10 +338,24 @@ void CChannel::SetChanInfo ( const CChannelCoreInfo& NChanInf ) // apply value (if a new channel or different from previous one) if ( !bIsIdentified || ChannelInfo != NChanInf ) { - bIsIdentified = true; // Indicate we have received channel info + + QString strOldName = ChannelInfo.strName; ChannelInfo = NChanInf; +#ifndef NO_JSON_RPC + if ( !bIsIdentified ) + { + CRpcLogging::getInstance().rpcOnNewConnection ( *this ); + } + else + { + CRpcLogging::getInstance().rpcOnUpdateConnection ( *this, strOldName ); + } +#endif + + bIsIdentified = true; // Indicate we have received channel info + // fire message that the channel info has changed emit ChanInfoHasChanged(); } diff --git a/src/main.cpp b/src/main.cpp index a4a9ca16a0..05416b8aa3 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -980,6 +980,7 @@ int main ( int argc, char** argv ) if ( pRpcServer ) { new CServerRpc ( &Server, pRpcServer, pRpcServer ); + CRpcLogging::getInstance().setRpcEnabled(); } #endif diff --git a/src/rpcserver.h b/src/rpcserver.h index bdedc5499e..f39d764f4c 100644 --- a/src/rpcserver.h +++ b/src/rpcserver.h @@ -62,6 +62,7 @@ class CRpcServer : public QObject // Our errors static const int iErrAuthenticationFailed = 400; static const int iErrUnauthenticated = 401; + static const int iErrChannelNotFound = 402; private: int iPort; @@ -82,3 +83,39 @@ class CRpcServer : public QObject protected slots: void OnNewConnection(); }; + +/** + * Singleton class that will be used as a consolidation point + * for signals emitting from multi-instance classes (ie, channels) + */ + +#include "channel.h" + +class CRpcLogging : public QObject +{ + + Q_OBJECT + +private: + CRpcLogging() {} + + bool rpcEnabled = false; + +public: + static CRpcLogging& getInstance() + { + static CRpcLogging instance; + return instance; + } + + bool isRpcEnabled() { return rpcEnabled; } + void setRpcEnabled() { rpcEnabled = true; } + void setRpcDisabled() { rpcEnabled = false; } + + void rpcOnNewConnection ( CChannel& channel ) { emit rpcClientConnected ( channel ); } + void rpcOnUpdateConnection ( CChannel& channel, const QString strOldName ) { emit rpcUpdateConnection ( channel, strOldName ); } + +signals: + void rpcClientConnected ( CChannel& channel ); + void rpcUpdateConnection ( CChannel& channel, const QString strOldName ); +}; diff --git a/src/server.cpp b/src/server.cpp index 186cc44991..511eb68d78 100644 --- a/src/server.cpp +++ b/src/server.cpp @@ -858,6 +858,9 @@ void CServer::DecodeReceiveData ( const int iChanCnt, const int iNumClients ) // and emit the client disconnected signal if ( eGetStat == GS_CHAN_NOW_DISCONNECTED ) { + // RPC - send the channel object + emit rpcClientDisconnected ( iCurChanID, vecChannels[iCurChanID].GetName(), vecChannels[iCurChanID].GetAddress() ); + if ( JamController.GetRecordingEnabled() ) { emit ClientDisconnected ( iCurChanID ); // TODO do this outside the mutex lock? @@ -1231,28 +1234,69 @@ void CServer::CreateAndSendChanListForThisChan ( const int iCurChanID ) vecChannels[iCurChanID].CreateConClientListMes ( vecChanInfo ); } +bool CServer::CreateAndSendPreEscapedChatText ( const int iChannel, const QString& strChatText ) +{ + if ( iChannel < 0 || iChannel > iMaxNumChannels ) + return false; + + if ( vecChannels[iChannel].IsConnected() ) + { + vecChannels[iChannel].CreateChatTextMes ( strChatText ); + return true; + } + + return false; +} + void CServer::CreateAndSendChatTextForAllConChannels ( const int iCurChanID, const QString& strChatText ) { + QString strActualMessageText; + // Create message which is sent to all connected clients ------------------- - // get client name - QString ChanName = vecChannels[iCurChanID].GetName(); - // add time and name of the client at the beginning of the message text and - // use different colors - QString sCurColor = vstrChatColors[iCurChanID % vstrChatColors.Size()]; + QString strChanName = ( iCurChanID < 0 ) ? "** Server **" : vecChannels[iCurChanID].GetName(); + QString strChanAddr = ( iCurChanID < 0 ) ? "" : vecChannels[iCurChanID].GetAddress().toString ( CHostAddress::SM_IP_PORT ); + QString strChatTime = QDateTime::currentDateTime().toString ( Qt::ISODate ); - const QString strActualMessageText = "(" + QTime::currentTime().toString ( "hh:mm:ss AP" ) + ") " + - ChanName.toHtmlEscaped() + " " + strChatText.toHtmlEscaped(); + if ( iCurChanID < 0 ) // A server-push chat message + { + // Server can send HTML messages, so no escaping here. + + // The decision to prepend "Server Message" to the message here is for consistency of src location. + + strActualMessageText = + "(" + QTime::currentTime().toString ( "hh:mm:ss AP" ) + ") Server Message: " + strChatText; + } + else + { + + // add time and name of the client at the beginning of the message text and + // use different colors + + QString sCurColor = vstrChatColors[iCurChanID % vstrChatColors.Size()]; + + strActualMessageText = "(" + QTime::currentTime().toString ( "hh:mm:ss AP" ) + ") " + + strChanName.toHtmlEscaped() + " " + strChatText.toHtmlEscaped(); + } // Send chat text to all connected clients --------------------------------- for ( int i = 0; i < iMaxNumChannels; i++ ) { + // TODO: This section can be refactored to call CreateAndSendPreEscapedChatText if desired + // as the code was duplicated there... + if ( vecChannels[i].IsConnected() ) { // send message vecChannels[i].CreateChatTextMes ( strActualMessageText ); } } + + // Only emit signal for client generated chats, not server generated + if ( iCurChanID >= 0 ) + { + emit rpcChatSent ( iCurChanID, strChanName, strChanAddr, strChatTime, strChatText ); + } } void CServer::CreateAndSendRecorderStateForAllConChannels() @@ -1490,6 +1534,7 @@ void CServer::GetConCliParam ( CVector& vecHostAddresses, vecsName[i] = vecChannels[i].GetName(); veciJitBufNumFrames[i] = vecChannels[i].GetSockBufNumFrames(); veciNetwFrameSizeFact[i] = vecChannels[i].GetNetwFrameSizeFact(); + vecChanInfo[i] = vecChannels[i].GetChanInfo(); } } } diff --git a/src/server.h b/src/server.h index 89560adeb3..bd53ed598a 100644 --- a/src/server.h +++ b/src/server.h @@ -168,11 +168,16 @@ class CServer : public QObject, public CServerSlots void SetEnableDelayPanning ( bool bDelayPanningOn ) { bDelayPan = bDelayPanningOn; } bool IsDelayPanningEnabled() { return bDelayPan; } + bool CreateAndSendPreEscapedChatText ( const int iChannel, const QString& strChatText ); + + // Methods formally protected, now public so they can be accessed via RPC + virtual void CreateAndSendChatTextForAllConChannels ( const int iCurChanID, const QString& strChatText ); + int FindChannel ( const CHostAddress& CheckAddr, const bool bAllowNew = false ); + protected: // access functions for actual channels bool IsConnected ( const int iChanNum ) { return vecChannels[iChanNum].IsConnected(); } - int FindChannel ( const CHostAddress& CheckAddr, const bool bAllowNew = false ); void InitChannel ( const int iNewChanID, const CHostAddress& InetAddr ); void FreeChannel ( const int iCurChanID ); void DumpChannels ( const QString& title ); @@ -181,8 +186,6 @@ class CServer : public QObject, public CServerSlots virtual void CreateAndSendChanListForAllConChannels(); virtual void CreateAndSendChanListForThisChan ( const int iCurChanID ); - virtual void CreateAndSendChatTextForAllConChannels ( const int iCurChanID, const QString& strChatText ); - virtual void CreateOtherMuteStateChanged ( const int iCurChanID, const int iOtherChanID, const bool bIsMuted ); virtual void CreateAndSendJitBufMessage ( const int iCurChanID, const int iNNumFra ); @@ -311,6 +314,7 @@ class CServer : public QObject, public CServerSlots void Started(); void Stopped(); void ClientDisconnected ( const int iChID ); + void rpcClientDisconnected ( const int iChID, const QString strName, const CHostAddress HostAddress ); void SvrRegStatusChanged(); void AudioFrame ( const int iChID, const QString stChName, @@ -326,6 +330,8 @@ class CServer : public QObject, public CServerSlots void RecordingSessionStarted ( QString sessionDir ); void EndRecorderThread(); + void rpcChatSent ( const int iCurChanID, const QString chanName, const QString chanAddr, const QString chatStamp, const QString strChatMessage ); + public slots: void OnTimer(); diff --git a/src/serverrpc.cpp b/src/serverrpc.cpp index 807abd6865..1d2164a25a 100644 --- a/src/serverrpc.cpp +++ b/src/serverrpc.cpp @@ -27,6 +27,106 @@ CServerRpc::CServerRpc ( CServer* pServer, CRpcServer* pRpcServer, QObject* parent ) : QObject ( parent ) { + + /// @rpc_notification jamulusserver/chatReceived + /// @brief Emitted when a chat text is received. Server-generated chats are not included in this notification. + /// @param {string} params.id - The channel ID generating the chat. + /// @param {string} params.name - The user name generating the chat. + /// @param {number} params.address - The address of the channel generating the chat. + /// @param {string} params.stamp - The date/time of the chat (ISO 8601 format, in server configured timezone). + /// @param {string} params.text - The chat text. + connect ( pServer, &CServer::rpcChatSent, [=] ( int ChanID, QString ChanName, QString ChanAddr, QString ChatStamp, QString strChatText ) { + pRpcServer->BroadcastNotification ( "jamulusserver/chatReceived", + QJsonObject{ + { "id", ChanID }, + { "name", ChanName }, + { "address", ChanAddr }, + { "stamp", ChatStamp }, + { "text", strChatText }, + } ); + } ); + + /// @rpc_notification jamulusserver/clientDisconnect + /// @brief Emitted when a client disconnects from the server. + /// @result {string} result.id - The client channel id. + /// @result {string} result.name - The client’s name. + /// @result {string} result.address - The client’s address (ip:port). + QObject::connect ( pServer, &CServer::rpcClientDisconnected, [=] ( int ChanID, QString ChanName, CHostAddress ChanAddr ) { + pRpcServer->BroadcastNotification ( "jamulusserver/clientDisconnect", + QJsonObject{ + { "id", ChanID }, + { "name", ChanName }, + { "address", ChanAddr.toString ( CHostAddress::SM_IP_PORT ) }, + } ); + } ); + + /// @rpc_notification jamulusserver/clientConnect + /// @brief Emitted when a new client connects to the server. + /// @result {string} result.name - The client’s name. + /// @result {string} result.address - The client’s address (ip:port). + /// @result {number} result.instrumentCode - The id of the user's instrument. + /// @result {number} result.instrumentName - The text name of the user's instrument. + /// @result {string} result.city - The user's city name. + /// @result {number} result.countryCode - The id of the country specified by the user (see QLocale::Country). + /// @result {number} result.countryName - The text name of the user's country (see QLocale::Country). + /// @result {number} result.skillLevelCode - The user's skill level id. + /// @result {number} result.skillLevelName - The user's skill level text name. + QObject::connect ( &CRpcLogging::getInstance(), &CRpcLogging::rpcClientConnected, [=] ( CChannel& channel ) { + CChannelCoreInfo chanInfo = channel.GetChanInfo(); + + // We have to find the channel id ourselves. + int ChanID = pServer->FindChannel ( channel.GetAddress() ); + + pRpcServer->BroadcastNotification ( "jamulusserver/clientConnect", + QJsonObject{ + { "id", ChanID }, + { "name", chanInfo.strName }, + { "address", channel.GetAddress().toString ( CHostAddress::SM_IP_PORT ) }, + { "instrumentCode", chanInfo.iInstrument }, + { "instrumentName", CInstPictures::GetName ( chanInfo.iInstrument ) }, + { "city", chanInfo.strCity }, + { "countryCode", chanInfo.eCountry }, + { "countryName", QLocale::countryToString ( chanInfo.eCountry ) }, + { "skillLevelCode", chanInfo.eSkillLevel }, + { "skillLevelName", SkillLevelToString ( chanInfo.eSkillLevel ) }, + } ); + } ); + + /// @rpc_notification jamulusserver/clientInfoChanged + /// @brief Emitted when a client changes their information (name, instrument, country, city, skill level). + /// @result {string} result.oldName - The client’s name just prior to this change. + /// @result {string} result.name - The client’s name (new one, if change). + /// @result {string} result.address - The client’s address (ip:port). + /// @result {number} result.instrumentCode - The id of the user's instrument. + /// @result {number} result.instrumentName - The text name of the user's instrument. + /// @result {string} result.city - The user's city name. + /// @result {number} result.countryCode - The id of the country specified by the user (see QLocale::Country). + /// @result {number} result.countryName - The text name of the user's country (see QLocale::Country). + /// @result {number} result.skillLevelCode - The user's skill level id. + /// @result {number} result.skillLevelName - The user's skill level text name. + + QObject::connect ( &CRpcLogging::getInstance(), &CRpcLogging::rpcUpdateConnection, [=] ( CChannel& channel, const QString strOldName ) { + CChannelCoreInfo chanInfo = channel.GetChanInfo(); + + // We have to find the channel id ourselves. + int ChanID = pServer->FindChannel ( channel.GetAddress() ); + + pRpcServer->BroadcastNotification ( "jamulusserver/clientInfoChanged", + QJsonObject{ + { "id", ChanID }, + { "oldName", strOldName }, + { "name", chanInfo.strName }, + { "address", channel.GetAddress().toString ( CHostAddress::SM_IP_PORT ) }, + { "instrumentCode", chanInfo.iInstrument }, + { "instrumentName", CInstPictures::GetName ( chanInfo.iInstrument ) }, + { "city", chanInfo.strCity }, + { "countryCode", chanInfo.eCountry }, + { "countryName", QLocale::countryToString ( chanInfo.eCountry ) }, + { "skillLevelCode", chanInfo.eSkillLevel }, + { "skillLevelName", SkillLevelToString ( chanInfo.eSkillLevel ) }, + } ); + } ); + // API doc already part of CClientRpc pRpcServer->HandleMethod ( "jamulus/getMode", [=] ( const QJsonObject& params, QJsonObject& response ) { QJsonObject result{ { "mode", "server" } }; @@ -34,6 +134,87 @@ CServerRpc::CServerRpc ( CServer* pServer, CRpcServer* pRpcServer, QObject* pare Q_UNUSED ( params ); } ); + /// @rpc_method jamulusserver/sendBroadcastChat + /// @brief Send a chat message to all connected clients. Messages from the server are not escaped and can contain HTML as defined for + /// QTextBrowser. + /// @param {string} params.textMessage - The chat message to be sent. + /// @result {string} result - Always "ok". + pRpcServer->HandleMethod ( "jamulusserver/sendBroadcastChat", [=] ( const QJsonObject& params, QJsonObject& response ) { + auto chatMessage = params["textMessage"]; + + if ( !chatMessage.isString() ) + { + response["error"] = CRpcServer::CreateJsonRpcError ( CRpcServer::iErrInvalidParams, "Invalid params: textMessage is not a string" ); + return; + } + + pServer->CreateAndSendChatTextForAllConChannels ( -1, QString ( chatMessage.toString() ) ); + + response["result"] = "ok"; + } ); + + /// @rpc_method jamulusserver/sendChat + /// @brief Send a chat message to the channel identified by a specificc address. The chat should be pre-escaped if necessary prior to calling this + /// method. + /// @param {string} params.address - The full channel IP address as a string XXX.XXX.XXX.XXX:PPPPP + /// @param {string} params.textMessage - The chat message to be sent. + /// @result {string} result - "ok" if channel could be determined and message sent. + pRpcServer->HandleMethod ( "jamulusserver/sendChat", [=] ( const QJsonObject& params, QJsonObject& response ) { + auto strAddress = params["address"]; + auto chatMessage = params["textMessage"]; + + bool boolStatus; + CVector vecHostAddresses; + CVector vecsName; + CVector veciJitBufNumFrames; + CVector veciNetwFrameSizeFact; + CVector vecChanInfo; + + if ( !chatMessage.isString() ) + { + response["error"] = CRpcServer::CreateJsonRpcError ( CRpcServer::iErrInvalidParams, "Invalid params: textMessage is not a string" ); + return; + } + + if ( !strAddress.isString() ) + { + response["error"] = CRpcServer::CreateJsonRpcError ( CRpcServer::iErrInvalidParams, "Invalid params: address is not a string" ); + return; + } + + // Find the channel number from the address provided. + + pServer->GetConCliParam ( vecHostAddresses, vecsName, veciJitBufNumFrames, veciNetwFrameSizeFact, vecChanInfo ); + + const int iNumChannels = vecHostAddresses.Size(); + + // fill list with connected clients + for ( int i = 0; i < iNumChannels; i++ ) + { + if ( vecHostAddresses[i].InetAddr == QHostAddress ( static_cast ( 0 ) ) ) + { + continue; + } + + if ( QString ( vecHostAddresses[i].toString ( CHostAddress::SM_IP_PORT ) ) == QString ( strAddress.toString() ) ) + { + // Send chat to channel + + boolStatus = pServer->CreateAndSendPreEscapedChatText ( i, QString ( chatMessage.toString() ) ); + + if ( boolStatus ) + { + response["result"] = "ok"; + return; + } + else + break; + } + } + + response["error"] = CRpcServer::CreateJsonRpcError ( CRpcServer::iErrChannelNotFound, "Could not locate channel from address" ); + } ); + /// @rpc_method jamulusserver/getRecorderStatus /// @brief Returns the recorder state. /// @param {object} params - No parameters (empty object). diff --git a/src/serverrpc.h b/src/serverrpc.h index 3aafc046ad..8bf8054d31 100644 --- a/src/serverrpc.h +++ b/src/serverrpc.h @@ -27,6 +27,7 @@ #include "server.h" #include "rpcserver.h" +#include "channel.h" /* Classes ********************************************************************/ class CServerRpc : public QObject