diff --git a/README.md b/README.md
index b81c99c..c318109 100644
--- a/README.md
+++ b/README.md
@@ -20,6 +20,7 @@ As work to reverse the protocols and provide a self-hosted central server is sti
| Model | Protocol Used | Bumper Version |
|--|--|--|
| Deebot 900/901 | MQTT | master |
+| Deebot Ozmo 930 | XMPP | master |
| Deebot M81 Pro | XMPP | v0.1.0 |
For more information about the protocols and how it works, see the [How does it work?](#how-does-it-work) section at the end. If you test against another model and it works, please report it so it can be added to the list.
diff --git a/bumper/__init__.py b/bumper/__init__.py
index 77e54be..3ae2598 100644
--- a/bumper/__init__.py
+++ b/bumper/__init__.py
@@ -89,7 +89,7 @@ class VacBotDevice(object):
resource="",
name="",
nick="",
- company="eco-ng",
+ company="",
):
self.vac_bot_device_class = vac_bot_device_class
self.company = company
@@ -132,13 +132,14 @@ def check_authcode(uid, authcode):
return False
-def add_bot(sn, did, devclass, resource):
+def add_bot(sn, did, devclass, resource, company):
newbot = VacBotDevice()
newbot.did = did
newbot.name = sn
newbot.vac_bot_device_class = devclass
newbot.resource = resource
+ newbot.company = company
bots = bumper_bots_var.get()
existingbot = False
diff --git a/bumper/confserver.py b/bumper/confserver.py
index 70c37d8..6cb58e4 100644
--- a/bumper/confserver.py
+++ b/bumper/confserver.py
@@ -581,9 +581,28 @@ class ConfServer:
self.bumper_bots.set(bots)
body = {"result": "ok", "todo": "result"}
+
+ elif todo == "AddOneDevice":
+ bots = self.bumper_bots.get()
+ for bot in bots:
+ if postbody["did"] == bot.did:
+ bot.nick = postbody["nick"]
+ self.bumper_bots.set(bots)
+ body = {"result": "ok", "todo": "result"}
+
+ elif todo == "DeleteOneDevice":
+ bots = self.bumper_bots.get()
+ for bot in bots:
+ if postbody["did"] == bot.did:
+ #bots.remove(bot)
+ #self.bumper_bots.set(bots)
+ body = {"result": "ok", "todo": "result"}
+
confserverlog.debug(
"\r\n POST: {} \r\n Response: {}".format(postbody, body)
)
+
+
return web.json_response(body)
except Exception as e:
@@ -606,18 +625,24 @@ class ConfServer:
if todo == "FindBest":
service = postbody["service"]
if service == "EcoMsgNew":
- body = {
- "result": "ok",
- "ip": socket.gethostbyname(socket.gethostname()),
- "port": 5223,
- }
+
+ srvip = socket.gethostbyname(socket.gethostname())
+ msgserver = {"ip":srvip,"port":5223,"result":"ok"}
+ msgserver = json.dumps(msgserver)
+ msgserver = msgserver.replace(" ","") #bot seems to be very picky about having no spaces, only way was with text
+
+ confserverlog.debug(
+ "\r\n POST: {} \r\n Response: {}".format(postbody, msgserver)
+ )
+ return web.json_response(text=msgserver)
+
elif service == "EcoUpdate":
body = {"result": "ok", "ip": "47.88.66.164", "port": 8005}
confserverlog.debug(
"\r\n POST: {} \r\n Response: {}".format(postbody, body)
)
- return web.json_response(body)
+ return web.json_response(body)
except Exception as e:
confserverlog.exception("{}".format(e))
@@ -627,22 +652,32 @@ class ConfServer:
json_body = json.loads(await request.text())
randomid = "".join(random.sample(string.ascii_letters, 6))
bots = self.bumper_bots.get()
- for bot in bots:
- if bot.did == json_body["toId"] and bot.mqtt_connection == True:
- retcmd = await self.helperbot.send_command(json_body, randomid)
- body = retcmd
- confserverlog.debug(
- "\r\n POST: {} \r\n Response: {}".format(json_body, body)
+ if "toId" in json_body: #Its a command
+ for bot in bots:
+ if bot.company == 'eco-ng':
+ if bot.did == json_body["toId"] and bot.mqtt_connection == True:
+ retcmd = await self.helperbot.send_command(json_body, randomid)
+ body = retcmd
+ confserverlog.debug(
+ "\r\n POST: {} \r\n Response: {}".format(json_body, body)
+ )
+ return web.json_response(body)
+
+ #No response, send error back
+ confserverlog.error(
+ "No bots with DID: {} connected to MQTT".format(
+ json_body["toId"]
)
- return web.json_response(body)
- else:
- confserverlog.error(
- "No bots with DID: {} connected to MQTT".format(
- json_body["toId"]
- )
- )
- body = {"id": randomid, "errno": bumper.ERR_COMMON, "ret": "fail"}
- return web.json_response(body)
+ )
+ body = {"id": randomid, "errno": bumper.ERR_COMMON, "ret": "fail"}
+ return web.json_response(body)
+ else:
+ if "td" in json_body: #Seen when doing initial wifi config
+ if json_body["td"] == "PollSCResult":
+ body = {
+ "ret": "ok"
+ }
+ return web.json_response(body)
except Exception as e:
confserverlog.exception("{}".format(e))
diff --git a/bumper/mqttserver.py b/bumper/mqttserver.py
index 35285b2..6dc3f10 100644
--- a/bumper/mqttserver.py
+++ b/bumper/mqttserver.py
@@ -333,7 +333,7 @@ class BumperMQTTServer_Plugin:
):
tmpbotdetail = str(didsplit[1]).split("/")
bumper.add_bot(
- username, didsplit[0], tmpbotdetail[0], tmpbotdetail[1]
+ username, didsplit[0], tmpbotdetail[0], tmpbotdetail[1], "eco-ng"
)
mqttserverlog.debug(
"new bot authenticated SN: {} DID: {}".format(
diff --git a/bumper/xmppserver.py b/bumper/xmppserver.py
index f20081f..78cf78b 100644
--- a/bumper/xmppserver.py
+++ b/bumper/xmppserver.py
@@ -11,8 +11,7 @@ xmppserverlog = logging.getLogger("xmppserver")
class XMPPServer:
- server_id = "bumper"
- bot_id = "bumpy"
+ server_id = "ecouser.net"
client_id = None
clients = []
exit_flag = False
@@ -193,6 +192,8 @@ class Client(threading.Thread):
self.connection = connection
self.address = client_address[0]
self.clientresource = ""
+ self.devclass = ""
+ self.bumper_jid = ""
self.uid = ""
self.log_sent_message = False # Set to true to log sends
self.log_incoming_data = True # Set to true to log sends
@@ -216,7 +217,7 @@ class Client(threading.Thread):
except BrokenPipeError as e:
xmppserverlog.error("{}".format(e))
- # self._set_state('DISCONNECT')
+ self._set_state('DISCONNECT')
except ConnectionResetError as e:
xmppserverlog.error("{}".format(e))
@@ -280,7 +281,7 @@ class Client(threading.Thread):
def _handle_ctl(self, xml, data):
try:
- if data.decode("utf-8").find("roster") > -1:
+ if "roster" in data:
# Return not-implemented for roster
self.send(
''.format(
@@ -291,23 +292,18 @@ class Client(threading.Thread):
if xml.get("type") == "set":
if (
- data.decode("utf-8").find("com:sf") > -1
+ "com:sf" in data
and xml.get("to") == "rl.ecorobot.net"
): # Android bind? Not sure what this does yet.
self.send(
''.format(
xml.get("id"),
self.uid,
- XMPPServer.bot_id,
+ XMPPServer.server_id,
self.clientresource,
)
)
- else:
- xmppserverlog.debug(
- "Unknown set type: {}".format(data.decode("utf-8"))
- )
-
if xml[0][0]:
ctl = xml[0][0]
if ctl.get("admin") and self.type == self.BOT:
@@ -319,108 +315,190 @@ class Client(threading.Thread):
# forward
for client in XMPPServer.clients:
- if client.address != self.address and client.state == client.READY:
+ if client.bumper_jid != self.bumper_jid and client.state == client.READY:
+ ctl_to = xml.get("to")
+ xml.attrib["from"] = "{}".format(self.bumper_jid)
+ rxmlstring = ET.tostring(xml).decode("utf-8")
+ #clean up string to remove namespaces added by ET
+ rxmlstring = rxmlstring.replace("xmlns:ns0=", "xmlns=")
+ rxmlstring = rxmlstring.replace("ns0:", "")
+ rxmlstring = rxmlstring.replace('iq xmlns="com:ctl"', "iq")
+ rxmlstring = rxmlstring.replace(' -1:
- data = (
- data[:id_index]
- + 'from="'
- + XMPPServer.client_id
- + '" '
- + data[id_index:]
- )
- data = data.encode()
- client.send(data.decode("utf-8"))
+ if client.uid.lower() in ctl_to.lower():
+ xmppserverlog.info("Sending ctl to bot: {}".format(rxmlstring))
+ client.send(rxmlstring)
+
except Exception as e:
xmppserverlog.exception("{}".format(e))
def _handle_ping(self, xml, data):
try:
- if xml.get("to").find("@") == -1:
+ if xml.get("to").find("@") == -1: #No to address
# Ping to server - respond
- self.send(
- ''.format(
+ pingresp = ''.format(
xml.get("id"), xml.get("to")
)
- )
+ #xmppserverlog.debug("Server Ping resp: {}".format(pingresp))
+ self.send(pingresp)
else:
+ pingto = xml.get("to")
+ pingfrom = self.bumper_jid
+
+ xml.attrib["from"] = pingfrom
+ pingstring = ET.tostring(xml).decode("utf-8")
+ #clean up string to remove namespaces added by ET
+ pingstring = pingstring.replace("xmlns:ns0=", "xmlns=")
+ pingstring = pingstring.replace("ns0:", "")
+ pingstring = pingstring.replace('iq xmlns="com:ctl"', "iq")
+ pingstring = pingstring.replace(''.format(
+ uuid.uuid4(), adminuser, self.bumper_jid, newuser)
+ xmppserverlog.debug("Add User: {}".format(adduser))
+ self.send(adduser)
+
+ #Add user ACs - Manage users, settings, and clean (full access)
+ adduseracs = ''.format(
+ uuid.uuid4(), adminuser, self.bumper_jid, newuser)
+ xmppserverlog.debug("Add User ACs: {}".format(adduseracs))
+ self.send(adduseracs)
+
+ #GetUserInfo - Just to confirm it set correctly
+ self.send(
+ ''.format(
+ uuid.uuid4(), adminuser, self.bumper_jid)
+ )
+
+ else:
+ rxmlstring = ET.tostring(xml).decode("utf-8")
+ #clean up string to remove namespaces added by ET
+ rxmlstring = rxmlstring.replace("xmlns:ns0=", "xmlns=")
+ rxmlstring = rxmlstring.replace("ns0:", "")
+ rxmlstring = rxmlstring.replace('iq xmlns="com:ctl"', "iq")
+ rxmlstring = rxmlstring.replace(' -1:
- # ack jabbr:client
- # no STARTTLS
- self.send(
- ''.format(
- XMPPServer.server_id
+ if xml == None:
+ # Client first connecting, send our features
+ if data.decode("utf-8").find("jabber:client") > -1:
+ sc = data.decode("utf-8").find("to=")
+ ec = data.decode("utf-8").find(".ecorobot.net")
+ if ec > -1:
+ self.devclass = data.decode("utf-8")[sc+4:ec]
+ # ack jabbr:client
+ # no STARTTLS
+ self.send(
+ ''.format(
+ XMPPServer.server_id
+ )
)
- )
- # with STARTTLS
- # self.send(''.format(XMPPServer.server_id))
- time.sleep(0.25)
- # send authentication support for iq-auth (fallback) and SASL
- self.send(
- 'PLAIN'
- )
- # self.send('')
+ # with STARTTLS
+ # self.send(''.format(XMPPServer.server_id))
+ time.sleep(0.25)
+ # send authentication support for iq-auth (fallback) and SASL
+ self.send(
+ 'PLAIN'
+ )
+ # self.send('')
- elif data.decode("utf-8").find("jabber:iq:auth") > -1: # Handle iq-auth
- self._handle_iq_auth(data)
-
- elif (
- data.decode("utf-8").find("urn:ietf:params:xml:ns:xmpp-sasl") > -1
- ): # Handle SASL auth
- self._handle_sasl_auth(data)
+ else:
+ self.send("")
+
+ else:
+ if "jabber:iq:auth" in xml.tag: # Handle iq-auth
+ self._handle_iq_auth(xml)
+ elif "urn:ietf:params:xml:ns:xmpp-sasl" in xml.tag: #Handle SASL Auth
+ self._handle_sasl_auth(xml)
+ else:
+ xmppserverlog.error("Couldn't handle: {}".format(xml))
elif self.state == self.INIT:
- # Client getting session after authentication
- if data.decode("utf-8").find("jabber:client") > -1:
- # ack jabbr:client
- self.send(
- ''.format(
- XMPPServer.server_id
+ if xml == None:
+ # Client getting session after authentication
+ if data.decode("utf-8").find("jabber:client") > -1:
+ # ack jabbr:client
+ self.send(
+ ''.format(
+ XMPPServer.server_id
+ )
+ )
+ time.sleep(0.25)
+ # session
+ self.send(
+ ''
)
- )
- time.sleep(0.25)
- # session
- self.send(
- ''
- )
- else: # Handle init bind
- xml = ET.fromstring(data.decode("utf-8"))
+ else: # Handle init bind
if len(xml):
- child = self._tag_strip_uri(xml[0].tag)
+ child = self._tag_strip_uri(xml[0].tag)
else:
child = None
if xml.tag == "iq":
if child == "bind":
- self._handle_bind(xml)
+ self._handle_bind(xml)
+ else:
+ xmppserverlog.error("Couldn't handle: {}".format(xml))
+
+
except Exception as e:
xmppserverlog.exception("{}".format(e))
@@ -504,7 +582,7 @@ class Client(threading.Thread):
except ET.ParseError as e:
if "no element found" in e.msg:
xmppserverlog.debug(
- "xml parse error - {} - {} - this is common with ecovac protocol".format(
+ "xml parse error - {} - {}".format(
data.decode("utf-8"), e
)
)
@@ -520,20 +598,27 @@ class Client(threading.Thread):
except Exception as e:
xmppserverlog.exception("{}".format(e))
- def _handle_sasl_auth(self, data):
- try:
- xml = ET.fromstring(data.decode("utf-8"))
- saslauth = base64.b64decode(xml.text).decode("utf-8").split("/")
+ def _handle_sasl_auth(self, xml):
+ try:
+
+ saslauth = base64.b64decode(xml.text).decode("utf-8").split("/")
username = saslauth[0]
username = saslauth[0].split("\x00")[1]
self.uid = username
- resource = saslauth[1]
- self.clientresource = resource
- authcode = saslauth[2]
+ if len(saslauth) > 1:
+ resource = saslauth[1]
+ self.clientresource = resource
+ elif len(saslauth[0].split("\x00")) > 2:
+ resource = saslauth[0].split("\x00")[2]
+ self.clientresource = resource
+
+ if len(saslauth) > 2:
+ authcode = saslauth[2]
if not self.uid.startswith("fuid"):
# Need sample data to see details here
- bumper.add_bot("", self.uid, "", resource)
+ bumper.add_bot(self.uid, self.uid, self.devclass, "atom","eco-legacy")
+ self.type = self.BOT
xmppserverlog.info("bot authenticated {}".format(self.uid))
# Send response
self.send(
@@ -551,6 +636,7 @@ class Client(threading.Thread):
auth = True
if auth:
+ self.type = self.CONTROLLER
bumper.add_client(self.uid, "bumper", self.clientresource)
xmppserverlog.debug("client authenticated {}".format(self.uid))
@@ -568,22 +654,6 @@ class Client(threading.Thread):
''
) # Fail
- except ET.ParseError as e:
- if "no element found" in e.msg:
- xmppserverlog.debug(
- "xml parse error - {} - {} - this is common with ecovac protocol".format(
- data.decode("utf-8"), e
- )
- )
- elif "not well-formed (invalid token)" in e.msg:
- xmppserverlog.debug(
- "xml parse error - {} - {}".format(data.decode("utf-8"), e)
- )
- else:
- xmppserverlog.debug(
- "xml parse error - {} - {}".format(data.decode("utf-8"), e)
- )
-
except Exception as e:
xmppserverlog.exception("{}".format(e))
@@ -606,21 +676,31 @@ class Client(threading.Thread):
clientbindxml = xml.getchildren()
clientresourcexml = clientbindxml[0].getchildren()
- if len(clientresourcexml) > 0:
+ if self.devclass: #its a bot
+ self.name = "XMPP_Client_{}_{}".format(self.uid,self.devclass)
+ self.bumper_jid = "{}@{}.ecorobot.net/atom".format(self.uid, self.devclass)
+ xmppserverlog.debug("new bot {}".format(self.uid))
+ res = '{}'.format(
+ xml.get("id"), self.bumper_jid
+ )
+ elif len(clientresourcexml) > 0:
self.clientresource = clientresourcexml[0].text
self.name = "XMPP_Client_{}".format(self.clientresource)
+ self.bumper_jid = "{}@{}/{}".format(self.uid, XMPPServer.server_id, self.clientresource)
xmppserverlog.debug(
"new client {} using resource {}".format(
- self.address, self.clientresource
+ self.uid, self.clientresource
)
)
- res = '{}@{}/{}'.format(
- xml.get("id"), self.uid, XMPPServer.bot_id, self.clientresource
+ res = '{}'.format(
+ xml.get("id"), self.bumper_jid
)
- else:
- xmppserverlog.debug("new client {}".format(self.address))
- res = '{}@{}'.format(
- xml.get("id"), self.uid, XMPPServer.bot_id
+ else:
+ self.name = "XMPP_Client_{}_{}".format(self.uid,self.address)
+ self.bumper_jid = "{}@{}".format(self.uid, XMPPServer.server_id)
+ xmppserverlog.debug("new client {}".format(self.uid))
+ res = '{}'.format(
+ xml.get("id"), self.bumper_jid
)
self._set_state("BIND")
@@ -640,93 +720,148 @@ class Client(threading.Thread):
def _handle_presence(self, xml):
try:
+
if len(xml) and xml[0].tag == "status":
- # bot announcing arrival
- self.type = self.BOT
xmppserverlog.debug(
- "{} type set to BOT (based on presence tag)".format(self.address)
+ "bot presence {} ".format(ET.tostring(xml, encoding="utf-8"))
)
- # send a command from an unknown user - the response will contain the correct admin username
-
+ #Most likely a bot, possibly hello world in text
+
+ #Send dummy return
self.send(
- ''.format(
- uuid.uuid4(), "unknown@ecouser.net", XMPPServer.bot_id
+ ' dummy '.format(
+ self.bumper_jid
)
)
- else:
- self.type = self.CONTROLLER
+ #If it is a BOT, send extras
+ if self.type == self.BOT:
+ #get device info
+ self.send(
+ ''.format(
+ self.bumper_jid, XMPPServer.server_id
+ )
+ )
+
+ else:
xmppserverlog.debug(
- "{} type set to CONTROLLER (based on presence tag)".format(
- self.address
+ "client presence - {} ".format(ET.tostring(xml, encoding="utf-8"))
+ )
+
+ if xml.get("type") == "available":
+ xmppserverlog.debug(
+ "client presence available - {} ".format(ET.tostring(xml, encoding="utf-8"))
)
- )
- self.send(
- ' dummy '.format(
- self.uid, XMPPServer.bot_id, self.clientresource
+ #Send dummy return
+ self.send(
+ ' dummy '.format(
+ self.bumper_jid
+ )
)
- )
+ elif xml.get("type") == "unavailable":
+ xmppserverlog.debug(
+ "client presence unavailable (DISCONNECT) - {} ".format(ET.tostring(xml, encoding="utf-8"))
+ )
+
+ self._set_state("DISCONNECT")
+ else:
+ #Sometimes the android app sends these
+ xmppserverlog.debug(
+ "client presence (UNKNOWN) - {} ".format(ET.tostring(xml, encoding="utf-8"))
+ )
+ #Send dummy return
+ self.send(
+ ' dummy '.format(
+ self.bumper_jid
+ )
+ )
+
except Exception as e:
xmppserverlog.exception("{}".format(e))
- def _parse_data(self, data):
- if self.log_incoming_data:
- xmppserverlog.debug(
- "from {} - {}".format(self.address, data.decode("utf-8"))
- )
+ def _parse_data(self, data):
- try:
- xml = ET.fromstring(data.decode("utf-8"))
- self._handle_xml(xml, data)
+
+ if data.decode("utf-8").startswith("]+\?>)", r"",data.decode("utf-8")) + ""
+
+ else:
+ newdata = "{}".format(data.decode("utf-8")) #Add artificial root
+
+ try:
+ root = ET.fromstring(newdata)
+ for item in root.iter():
+ if item.tag != "root":
+ if item.tag == "iq":
+ if self.log_incoming_data:
+ xmppserverlog.debug(
+ "from {} - {}".format(self.address, str(ET.tostring(item, encoding="utf-8").decode("utf-8")).replace("ns0:",""))
+ )
+ self._handle_iq(item, newdata)
+ item.clear()
+ elif "auth" in item.tag:
+ if "urn:ietf:params:xml:ns:xmpp-sasl" in item.tag: #SASL Auth
+ self._handle_sasl_auth(item)
+ item.clear()
+
+ elif "presence" in item.tag:
+ self._handle_presence(item)
+ item.clear()
+
+ else:
+ if self.log_incoming_data:
+ xmppserverlog.debug(
+ "Unparsed Item - {}".format(str(ET.tostring(item, encoding="utf-8").decode("utf-8")).replace("ns0:",""))
+ )
+ print("e")
+
except ET.ParseError as e:
if (
"no element found" in e.msg
): # Element not closed or not all bytes received
# Happens wth connect stream often
- if " - client is signalling end of session/disconnect
- if not "" in data.decode("utf-8"):
+ if not "" in newdata:
xmppserverlog.error(
- "xml parse error - {} - {}".format(data.decode("utf-8"), e)
+ "xml parse error - {} - {}".format(newdata, e)
)
else:
self.send("") # Close stream
-
- elif (
- "junk after document element" in e.msg
- ): # More than one xml doc in data
- # try to split it
- data0 = data.decode("utf-8")
- data1 = data0[e.position[1] :]
- data0 = data0[: e.position[1]]
- # xmppserverlog.debug('xml parse error - {} - {} - split0: {} - split1: {}'.format(data.decode('utf-8'), e, data0, data1))
- self._parse_data(data0.encode("utf-8"))
- self._parse_data(data1.encode("utf-8"))
-
+
else:
- xmppserverlog.debug(
- "xml parse error - {} - {}".format(data.decode("utf-8"), e)
- )
+ if "" in newdata:
+ xmppserverlog.error(
+ "xml parse error - {} - {}".format(newdata, e)
+ )
+ else:
+ self.send("") # Close stream
+ self._set_state("DISCONNECT")
+
except Exception as e:
xmppserverlog.exception("{}".format(e))
- def _handle_xml(self, xml, data):
+ def _handle_iq(self, xml, data):
try:
- if self.state == self.CONNECT or self.state == self.INIT:
- self._handle_connect(data)
-
if len(xml):
child = self._tag_strip_uri(xml[0].tag)
else:
@@ -737,14 +872,23 @@ class Client(threading.Thread):
self._handle_bind(xml)
elif child == "session":
self._handle_session(xml)
- elif child == "query":
- self._handle_ctl(xml, data)
elif child == "ping":
self._handle_ping(xml, data)
+ elif child == "query":
+ if self.type == self.BOT:
+ self._handle_result(xml, data)
+ else:
+ self._handle_ctl(xml, data)
elif xml.get("type") == "result":
- self._handle_result(data)
- elif xml.tag == "presence":
- self._handle_presence(xml)
+ if self.type == self.BOT:
+ self._handle_result(xml, data)
+ else:
+ self._handle_result(xml, data)
+ elif xml.get("type") == "set":
+ if self.type == self.BOT:
+ self._handle_result(xml, data)
+ else:
+ self._handle_result(xml, data)
except Exception as e:
xmppserverlog.exception("{}".format(e))
@@ -752,15 +896,14 @@ class Client(threading.Thread):
def run(self):
# xmppserverlog.info('client connected - {}'.format(self.address))
self._set_state("CONNECT")
-
while not self.state == self.DISCONNECT and not self.connection._closed:
data = b""
-
- time.sleep(0.2)
+ time.sleep(0.1)
if not self.connection._closed:
try:
- data = self.connection.recv(4096)
-
+ data = self.connection.recv(4096)
+ if data != b"":
+ self._parse_data(data)
except ConnectionResetError as e:
xmppserverlog.error("{}".format(e))
except OSError as e:
@@ -768,6 +911,5 @@ class Client(threading.Thread):
except Exception as e:
xmppserverlog.exception("{}".format(e))
- if data != b"":
- self._parse_data(data)
-
+
+