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