diff --git a/test/unitTest/syncHistory/syncHistory.cpp b/test/unitTest/syncHistory/syncHistory.cpp
index 3f13dc284380867b72494f314b1ad9db4d04aac3..1b2298fd513392ee8a418eceadc52be22dfc48b5 100644
--- a/test/unitTest/syncHistory/syncHistory.cpp
+++ b/test/unitTest/syncHistory/syncHistory.cpp
@@ -356,7 +356,7 @@ SyncHistoryTest::testReceivesInviteThenAddDevice()
 
     // Start conversation for Alice
     auto convId = bobAccount->startConversation();
-    bobAccount->addConversationMember(convId, uri);
+    CPPUNIT_ASSERT(bobAccount->addConversationMember(convId, uri));
 
     // Check that alice receives the request
     std::mutex mtx;
diff --git a/tools/dringctrl/controller.py b/tools/dringctrl/controller.py
index 23ea853562f70b4abffdb082af1212fd59222d6f..027d24e2ccbf33fe38006d05ca14069e6db68934 100644
--- a/tools/dringctrl/controller.py
+++ b/tools/dringctrl/controller.py
@@ -127,6 +127,9 @@ class DRingCtrl(Thread):
             proxy_callmgr.connect_to_signal('conferenceCreated', self.onConferenceCreated)
             proxy_confmgr.connect_to_signal('accountsChanged', self.onAccountsChanged)
             proxy_confmgr.connect_to_signal('dataTransferEvent', self.onDataTransferEvent)
+            proxy_confmgr.connect_to_signal('conversationReady', self.onConversationReady)
+            proxy_confmgr.connect_to_signal('conversationRequestReceived', self.onConversationRequestReceived)
+            proxy_confmgr.connect_to_signal('messageReceived', self.onMessageReceived)
 
         except dbus.DBusException as e:
             raise DRingCtrlDBusError("Unable to connect to dring DBus signals")
@@ -305,6 +308,17 @@ class DRingCtrl(Thread):
     def onDataTransferEvent(self, transferId, code):
         pass
 
+    def onConversationReady(self, account, conversationId):
+        print(f'New conversation ready for {account} with id {conversationId}')
+
+    def onConversationRequestReceived(self, account, conversationId, metadatas):
+        print(f'New conversation request for {account} with id {conversationId}')
+
+    def onMessageReceived(self, account, conversationId, message):
+        print(f'New message for {account} in conversation {conversationId} with id {message["id"]}')
+        for key in message:
+            print(f'\t {key}: {message[key]}')
+
     #
     # Account management
     #
@@ -553,7 +567,7 @@ class DRingCtrl(Thread):
         if not self.account:
             self.setFirstRegisteredAccount()
 
-        if self.account is not "IP2IP" and not self.isAccountRegistered():
+        if self.account != "IP2IP" and not self.isAccountRegistered():
             raise DRingCtrlAccountError("Can't place a call without a registered account")
 
         # Send the request to the CallManager
@@ -696,6 +710,30 @@ class DRingCtrl(Thread):
     def sendTextMessage(self, account, to, message):
         return self.configurationmanager.sendTextMessage(account, to, { 'text/plain': message })
 
+    def startConversation(self, account):
+        return self.configurationmanager.startConversation(account)
+
+    def listConversations(self, account):
+        return self.configurationmanager.getConversations(account)
+
+    def listConversationsRequests(self, account):
+        return self.configurationmanager.getConversationRequests(account)
+
+    def listConversationsMembers(self, account, conversationId):
+        return self.configurationmanager.getConversationMembers(account, conversationId)
+
+    def addConversationMember(self, account, conversationId, member):
+        return self.configurationmanager.addConversationMember(account, conversationId, member)
+
+    def acceptConversationRequest(self, account, conversationId):
+        return self.configurationmanager.acceptConversationRequest(account, conversationId)
+
+    def declineConversationRequest(self, account, conversationId):
+        return self.configurationmanager.declineConversationRequest(account, conversationId)
+
+    def sendMessage(self, account, conversationId, message, parent=''):
+        return self.configurationmanager.sendMessage(account, conversationId, message, parent)
+
     def run(self):
         """Processing method for this thread"""
 
diff --git a/tools/dringctrl/swarm.py b/tools/dringctrl/swarm.py
new file mode 100644
index 0000000000000000000000000000000000000000..6e1a3af1adcc229fbbbb2a26a091848e5aa42df9
--- /dev/null
+++ b/tools/dringctrl/swarm.py
@@ -0,0 +1,94 @@
+#!/usr/bin/env python
+#
+#  Copyright (C) 2020 Savoir-faire Linux Inc.
+#
+# Author: Sébastien Blin <sebastien.blin@savoirfairelinux.com>
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+
+from controller import DRingCtrl
+
+import argparse
+import sys
+import signal
+import os.path
+import threading
+
+parser = argparse.ArgumentParser()
+parser.add_argument('--account', help='Account to use', metavar='<account>', type=str)
+
+args = parser.parse_args()
+
+ctrl = DRingCtrl(sys.argv[0], False)
+if not args.account:
+    for account in ctrl.getAllEnabledAccounts():
+        details = ctrl.getAccountDetails(account)
+        if details['Account.type'] == 'RING':
+            args.account = account
+            break
+    if not args.account:
+        raise ValueError("no valid account")
+
+def run_controller():
+    ctrl.run()
+
+if __name__ == "__main__":
+    ctrlThread = threading.Thread(target=run_controller, args=(), daemon=True)
+    ctrlThread.start()
+    while True:
+        print("""Swarm options:
+0. Create conversation
+1. List conversations
+2. List conversations members
+3. Add Member
+4. List requests
+5. Accept request
+6. Decline request
+7. Load messages
+8. Send message
+        """)
+        opt = int(input("> "))
+        if opt == 0:
+            ctrl.startConversation(args.account)
+        elif opt == 1:
+            print(f'Conversations for account {args.account}:')
+            for conversation in ctrl.listConversations(args.account):
+                print(f'\t{conversation}')
+        elif opt == 2:
+            conversationId = input('Conversation: ')
+            print(f'Members for conversation {conversationId}:')
+            for member in ctrl.listConversationsMembers(args.account, conversationId):
+                print(f'{member["uri"]}')
+        elif opt == 3:
+            conversationId = input('Conversation: ')
+            contactUri = input('New member: ')
+            ctrl.addConversationMember(args.account, conversationId, contactUri)
+        elif opt == 4:
+            print(f'Conversations request for account {args.account}:')
+            for request in ctrl.listConversationsRequests(args.account):
+                print(f'{request["id"]}')
+        elif opt == 5:
+            conversationId = input('Conversation: ')
+            ctrl.acceptConversationRequest(args.account, conversationId)
+        elif opt == 6:
+            conversationId = input('Conversation: ')
+            ctrl.declineConversationRequest(args.account, conversationId)
+        elif opt == 8:
+            conversationId = input('Conversation: ')
+            message = input('Message: ')
+            ctrl.sendMessage(args.account, conversationId, message)
+        else:
+            print('Not implemented yet')
+    ctrlThread.join()