Philipp Hörist pushed to branch master at gajim / gajim
Commits:
-
46926e71
by Philipp Hörist at 2017-11-07T20:39:36+01:00
-
7f1a839e
by Philipp Hörist at 2017-11-07T20:39:36+01:00
-
d4fdf0d7
by Philipp Hörist at 2017-11-07T20:39:45+01:00
-
d381179d
by Philipp Hörist at 2017-11-07T21:03:01+01:00
5 changed files:
- gajim/common/connection_handlers.py
- gajim/common/connection_handlers_events.py
- gajim/common/logger.py
- gajim/common/message_archiving.py
- gajim/session.py
Changes:
| ... | ... | @@ -725,8 +725,6 @@ class ConnectionHandlersBase: |
| 725 | 725 |
# IDs of sent messages (https://trac.gajim.org/ticket/8222)
|
| 726 | 726 |
self.sent_message_ids = []
|
| 727 | 727 |
|
| 728 |
- self.received_message_hashes = []
|
|
| 729 |
- |
|
| 730 | 728 |
# We decrypt GPG messages one after the other. Keep queue in mem
|
| 731 | 729 |
self.gpg_messages_to_decrypt = []
|
| 732 | 730 |
|
| ... | ... | @@ -946,7 +944,8 @@ class ConnectionHandlersBase: |
| 946 | 944 |
def _on_message_received(self, obj):
|
| 947 | 945 |
if isinstance(obj, MessageReceivedEvent):
|
| 948 | 946 |
app.nec.push_incoming_event(
|
| 949 |
- DecryptedMessageReceivedEvent(None, conn=self, msg_obj=obj))
|
|
| 947 |
+ DecryptedMessageReceivedEvent(
|
|
| 948 |
+ None, conn=self, msg_obj=obj, stanza_id=obj.unique_id))
|
|
| 950 | 949 |
else:
|
| 951 | 950 |
app.nec.push_incoming_event(
|
| 952 | 951 |
MamDecryptedMessageReceivedEvent(None, **vars(obj)))
|
| ... | ... | @@ -1012,14 +1011,14 @@ class ConnectionHandlersBase: |
| 1012 | 1011 |
return True
|
| 1013 | 1012 |
elif obj.mtype == 'groupchat':
|
| 1014 | 1013 |
app.nec.push_incoming_event(GcMessageReceivedEvent(None,
|
| 1015 |
- conn=self, msg_obj=obj))
|
|
| 1014 |
+ conn=self, msg_obj=obj, stanza_id=obj.unique_id))
|
|
| 1016 | 1015 |
return True
|
| 1017 | 1016 |
|
| 1018 | 1017 |
def _nec_gc_message_received(self, obj):
|
| 1019 | 1018 |
if obj.conn.name != self.name:
|
| 1020 | 1019 |
return
|
| 1021 | 1020 |
if (app.config.should_log(obj.conn.name, obj.jid) and
|
| 1022 |
- obj.msgtxt and obj.nick):
|
|
| 1021 |
+ obj.msgtxt and obj.nick):
|
|
| 1023 | 1022 |
# if not obj.nick, it means message comes from room itself
|
| 1024 | 1023 |
# usually it hold description and can be send at each connection
|
| 1025 | 1024 |
# so don't store it in logs
|
| ... | ... | @@ -1028,7 +1027,8 @@ class ConnectionHandlersBase: |
| 1028 | 1027 |
KindConstant.GC_MSG,
|
| 1029 | 1028 |
message=obj.msgtxt,
|
| 1030 | 1029 |
contact_name=obj.nick,
|
| 1031 |
- additional_data=obj.additional_data)
|
|
| 1030 |
+ additional_data=obj.additional_data,
|
|
| 1031 |
+ stanza_id=obj.unique_id)
|
|
| 1032 | 1032 |
app.logger.set_room_last_message_time(obj.room_jid, obj.timestamp)
|
| 1033 | 1033 |
|
| 1034 | 1034 |
# process and dispatch an error message
|
| ... | ... | @@ -2048,8 +2048,6 @@ ConnectionHandlersBase, ConnectionJingle, ConnectionIBBytestream): |
| 2048 | 2048 |
app.nec.push_incoming_event(SignedInEvent(None, conn=self))
|
| 2049 | 2049 |
self.send_awaiting_pep()
|
| 2050 | 2050 |
self.continue_connect_info = None
|
| 2051 |
- # hashes of already received messages
|
|
| 2052 |
- self.received_message_hashes = []
|
|
| 2053 | 2051 |
|
| 2054 | 2052 |
def _SearchCB(self, con, iq_obj):
|
| 2055 | 2053 |
log.debug('SearchCB')
|
| ... | ... | @@ -139,6 +139,28 @@ class HelperEvent: |
| 139 | 139 |
if oob_desc is not None:
|
| 140 | 140 |
self.additional_data['gajim']['oob_desc'] = oob_desc
|
| 141 | 141 |
|
| 142 |
+ def get_stanza_id(self, stanza, query=False):
|
|
| 143 |
+ if query:
|
|
| 144 |
+ # On a MAM query the stanza-id is maybe not set, so
|
|
| 145 |
+ # get the id of the stanza
|
|
| 146 |
+ return stanza.getAttr('id')
|
|
| 147 |
+ stanza_id, by = stanza.getStanzaIDAttrs()
|
|
| 148 |
+ if by is None:
|
|
| 149 |
+ # We can not verify who set this stanza-id, ignore it.
|
|
| 150 |
+ return
|
|
| 151 |
+ elif not self.conn.get_own_jid().bareMatch(by):
|
|
| 152 |
+ # by attribute does not match the server, ignore it.
|
|
| 153 |
+ return
|
|
| 154 |
+ return stanza_id
|
|
| 155 |
+ |
|
| 156 |
+ @staticmethod
|
|
| 157 |
+ def get_forwarded_message(stanza):
|
|
| 158 |
+ forwarded = stanza.getTag('forwarded',
|
|
| 159 |
+ namespace=nbxmpp.NS_FORWARD,
|
|
| 160 |
+ protocol=True)
|
|
| 161 |
+ if forwarded is not None:
|
|
| 162 |
+ return forwarded.getTag('message', protocol=True)
|
|
| 163 |
+ |
|
| 142 | 164 |
class HttpAuthReceivedEvent(nec.NetworkIncomingEvent):
|
| 143 | 165 |
name = 'http-auth-received'
|
| 144 | 166 |
base_network_events = []
|
| ... | ... | @@ -1026,6 +1048,7 @@ class MamMessageReceivedEvent(nec.NetworkIncomingEvent, HelperEvent): |
| 1026 | 1048 |
:stanza: Complete stanza Node
|
| 1027 | 1049 |
:forwarded: Forwarded Node
|
| 1028 | 1050 |
:result: Result Node
|
| 1051 |
+ :unique_id: The unique stable id
|
|
| 1029 | 1052 |
'''
|
| 1030 | 1053 |
self._set_base_event_vars_as_attributes(base_event)
|
| 1031 | 1054 |
self.additional_data = {}
|
| ... | ... | @@ -1046,6 +1069,13 @@ class MamMessageReceivedEvent(nec.NetworkIncomingEvent, HelperEvent): |
| 1046 | 1069 |
log.info('Received groupchat message from user archive')
|
| 1047 | 1070 |
return False
|
| 1048 | 1071 |
|
| 1072 |
+ # use stanza-id as unique-id
|
|
| 1073 |
+ self.unique_id, origin_id = self.get_unique_id()
|
|
| 1074 |
+ |
|
| 1075 |
+ # Check for duplicates
|
|
| 1076 |
+ if app.logger.find_stanza_id(self.unique_id, origin_id):
|
|
| 1077 |
+ return
|
|
| 1078 |
+ |
|
| 1049 | 1079 |
self.msgtxt = self.msg_.getTagData('body')
|
| 1050 | 1080 |
self.query_id = self.result.getAttr('queryid')
|
| 1051 | 1081 |
|
| ... | ... | @@ -1057,24 +1087,12 @@ class MamMessageReceivedEvent(nec.NetworkIncomingEvent, HelperEvent): |
| 1057 | 1087 |
to = own_jid
|
| 1058 | 1088 |
|
| 1059 | 1089 |
if frm.bareMatch(own_jid):
|
| 1060 |
- self.stanza_id = self.msg_.getOriginID()
|
|
| 1061 |
- if self.stanza_id is None:
|
|
| 1062 |
- self.stanza_id = self.msg_.getID()
|
|
| 1063 |
- |
|
| 1064 | 1090 |
self.with_ = to
|
| 1065 | 1091 |
self.kind = KindConstant.CHAT_MSG_SENT
|
| 1066 | 1092 |
else:
|
| 1067 |
- if self.result.getNamespace() == nbxmpp.NS_MAM_2:
|
|
| 1068 |
- self.stanza_id = self.result.getID()
|
|
| 1069 |
- else:
|
|
| 1070 |
- self.stanza_id = self.msg_.getID()
|
|
| 1071 |
- |
|
| 1072 | 1093 |
self.with_ = frm
|
| 1073 | 1094 |
self.kind = KindConstant.CHAT_MSG_RECV
|
| 1074 | 1095 |
|
| 1075 |
- if self.stanza_id is None:
|
|
| 1076 |
- log.debug('Could not retrieve stanza-id')
|
|
| 1077 |
- |
|
| 1078 | 1096 |
delay = self.forwarded.getTagAttr(
|
| 1079 | 1097 |
'delay', 'stamp', namespace=nbxmpp.NS_DELAY2)
|
| 1080 | 1098 |
if delay is None:
|
| ... | ... | @@ -1097,9 +1115,31 @@ class MamMessageReceivedEvent(nec.NetworkIncomingEvent, HelperEvent): |
| 1097 | 1115 |
log.warning('Received MAM message with '
|
| 1098 | 1116 |
'invalid user timestamp: %s', user_delay)
|
| 1099 | 1117 |
|
| 1100 |
- log.debug('Received mam-message: stanza id: %s', self.stanza_id)
|
|
| 1118 |
+ log.debug('Received mam-message: unique id: %s', self.unique_id)
|
|
| 1101 | 1119 |
return True
|
| 1102 | 1120 |
|
| 1121 |
+ def get_unique_id(self):
|
|
| 1122 |
+ if self.conn.get_own_jid().bareMatch(self.msg_.getFrom()):
|
|
| 1123 |
+ # On our own Messages we have to check for both
|
|
| 1124 |
+ # stanza-id and origin-id, because other resources
|
|
| 1125 |
+ # maybe not support origin-id
|
|
| 1126 |
+ stanza_id = None
|
|
| 1127 |
+ if self.result.getNamespace() == nbxmpp.NS_MAM_2:
|
|
| 1128 |
+ # Only mam:2 ensures valid stanza-id
|
|
| 1129 |
+ stanza_id = self.get_stanza_id(self.result, query=True)
|
|
| 1130 |
+ |
|
| 1131 |
+ # try always to get origin-id because its a message
|
|
| 1132 |
+ # we sent.
|
|
| 1133 |
+ origin_id = self.msg_.getOriginID()
|
|
| 1134 |
+ |
|
| 1135 |
+ return stanza_id, origin_id
|
|
| 1136 |
+ |
|
| 1137 |
+ # A message we received
|
|
| 1138 |
+ elif self.result.getNamespace() == nbxmpp.NS_MAM_2:
|
|
| 1139 |
+ # Only mam:2 ensures valid stanza-id
|
|
| 1140 |
+ return self.get_stanza_id(self.result, query=True), None
|
|
| 1141 |
+ return None, None
|
|
| 1142 |
+ |
|
| 1103 | 1143 |
class MamDecryptedMessageReceivedEvent(nec.NetworkIncomingEvent, HelperEvent):
|
| 1104 | 1144 |
name = 'mam-decrypted-message-received'
|
| 1105 | 1145 |
base_network_events = []
|
| ... | ... | @@ -1177,6 +1217,14 @@ class MessageReceivedEvent(nec.NetworkIncomingEvent, HelperEvent): |
| 1177 | 1217 |
self.stanza.getFrom())
|
| 1178 | 1218 |
return
|
| 1179 | 1219 |
|
| 1220 |
+ # Check for duplicates
|
|
| 1221 |
+ self.unique_id = self.get_unique_id()
|
|
| 1222 |
+ # Check groupchat messages for duplicates,
|
|
| 1223 |
+ # We do this because of MUC History messages
|
|
| 1224 |
+ if self.stanza.getType() == 'groupchat':
|
|
| 1225 |
+ if app.logger.find_stanza_id(self.unique_id):
|
|
| 1226 |
+ return
|
|
| 1227 |
+ |
|
| 1180 | 1228 |
address_tag = self.stanza.getTag('addresses',
|
| 1181 | 1229 |
namespace=nbxmpp.NS_ADDRESS)
|
| 1182 | 1230 |
# Be sure it comes from one of our resource, else ignore address element
|
| ... | ... | @@ -1248,7 +1296,8 @@ class MessageReceivedEvent(nec.NetworkIncomingEvent, HelperEvent): |
| 1248 | 1296 |
conn=self.conn,
|
| 1249 | 1297 |
stanza=self.stanza,
|
| 1250 | 1298 |
forwarded=forwarded,
|
| 1251 |
- result=result))
|
|
| 1299 |
+ result=result,
|
|
| 1300 |
+ stanza_id=self.unique_id))
|
|
| 1252 | 1301 |
return
|
| 1253 | 1302 |
|
| 1254 | 1303 |
# Mediated invitation?
|
| ... | ... | @@ -1320,6 +1369,38 @@ class MessageReceivedEvent(nec.NetworkIncomingEvent, HelperEvent): |
| 1320 | 1369 |
|
| 1321 | 1370 |
return True
|
| 1322 | 1371 |
|
| 1372 |
+ def get_unique_id(self):
|
|
| 1373 |
+ if self.stanza.getType() == 'groupchat':
|
|
| 1374 |
+ # TODO: Disco the MUC check if 'urn:xmpp:mam:2' is announced
|
|
| 1375 |
+ return self.get_stanza_id(self.stanza)
|
|
| 1376 |
+ |
|
| 1377 |
+ elif self.stanza.getType() != 'chat':
|
|
| 1378 |
+ return
|
|
| 1379 |
+ |
|
| 1380 |
+ # Messages we receive live
|
|
| 1381 |
+ if self.conn.archiving_namespace != nbxmpp.NS_MAM_2:
|
|
| 1382 |
+ # Only mam:2 ensures valid stanza-id
|
|
| 1383 |
+ return
|
|
| 1384 |
+ |
|
| 1385 |
+ # Sent Carbon
|
|
| 1386 |
+ sent_carbon = self.stanza.getTag('sent',
|
|
| 1387 |
+ namespace=nbxmpp.NS_CARBONS,
|
|
| 1388 |
+ protocol=True)
|
|
| 1389 |
+ if sent_carbon is not None:
|
|
| 1390 |
+ message = self.get_forwarded_message(sent_carbon)
|
|
| 1391 |
+ return self.get_stanza_id(message)
|
|
| 1392 |
+ |
|
| 1393 |
+ # Received Carbon
|
|
| 1394 |
+ received_carbon = self.stanza.getTag('received',
|
|
| 1395 |
+ namespace=nbxmpp.NS_CARBONS,
|
|
| 1396 |
+ protocol=True)
|
|
| 1397 |
+ if received_carbon is not None:
|
|
| 1398 |
+ message = self.get_forwarded_message(received_carbon)
|
|
| 1399 |
+ return self.get_stanza_id(message)
|
|
| 1400 |
+ |
|
| 1401 |
+ # Normal Message
|
|
| 1402 |
+ return self.get_stanza_id(self.stanza)
|
|
| 1403 |
+ |
|
| 1323 | 1404 |
class ZeroconfMessageReceivedEvent(MessageReceivedEvent):
|
| 1324 | 1405 |
name = 'message-received'
|
| 1325 | 1406 |
base_network_events = []
|
| ... | ... | @@ -1416,6 +1497,7 @@ class DecryptedMessageReceivedEvent(nec.NetworkIncomingEvent, HelperEvent): |
| 1416 | 1497 |
self.stanza = self.msg_obj.stanza
|
| 1417 | 1498 |
self.additional_data = self.msg_obj.additional_data
|
| 1418 | 1499 |
self.id_ = self.msg_obj.id_
|
| 1500 |
+ self.unique_id = self.msg_obj.unique_id
|
|
| 1419 | 1501 |
self.jid = self.msg_obj.jid
|
| 1420 | 1502 |
self.fjid = self.msg_obj.fjid
|
| 1421 | 1503 |
self.resource = self.msg_obj.resource
|
| ... | ... | @@ -1472,22 +1554,6 @@ class DecryptedMessageReceivedEvent(nec.NetworkIncomingEvent, HelperEvent): |
| 1472 | 1554 |
if replace:
|
| 1473 | 1555 |
self.correct_id = replace.getAttr('id')
|
| 1474 | 1556 |
|
| 1475 |
- # ignore message duplicates
|
|
| 1476 |
- if self.msgtxt and self.id_ and self.jid:
|
|
| 1477 |
- self.msghash = hashlib.sha256(("%s|%s|%s" % (
|
|
| 1478 |
- hashlib.sha256(self.msgtxt.encode('utf-8')).hexdigest(),
|
|
| 1479 |
- hashlib.sha256(self.id_.encode('utf-8')).hexdigest(),
|
|
| 1480 |
- hashlib.sha256(self.jid.encode('utf-8')).hexdigest())).encode(
|
|
| 1481 |
- 'utf-8')).digest()
|
|
| 1482 |
- if self.msghash in self.conn.received_message_hashes:
|
|
| 1483 |
- log.info("Ignoring duplicated message from '%s' with id '%s'" % (self.jid, self.id_))
|
|
| 1484 |
- return False
|
|
| 1485 |
- else:
|
|
| 1486 |
- log.debug("subhashes: msgtxt, id_, jid = ('%s', '%s', '%s')" % (hashlib.sha256(self.msgtxt.encode('utf-8')).hexdigest(), hashlib.sha256(self.id_.encode('utf-8')).hexdigest(), hashlib.sha256(self.jid.encode('utf-8')).hexdigest()))
|
|
| 1487 |
- self.conn.received_message_hashes.append(self.msghash)
|
|
| 1488 |
- # only record the last 20000 hashes (should be about 1MB [32 bytes per hash]
|
|
| 1489 |
- # and about 24 hours if you receive a message every 5 seconds)
|
|
| 1490 |
- self.conn.received_message_hashes = self.conn.received_message_hashes[-20000:]
|
|
| 1491 | 1557 |
return True
|
| 1492 | 1558 |
|
| 1493 | 1559 |
class ChatstateReceivedEvent(nec.NetworkIncomingEvent):
|
| ... | ... | @@ -1513,6 +1579,7 @@ class GcMessageReceivedEvent(nec.NetworkIncomingEvent): |
| 1513 | 1579 |
else:
|
| 1514 | 1580 |
self.additional_data = self.msg_obj.additional_data
|
| 1515 | 1581 |
self.id_ = self.msg_obj.stanza.getID()
|
| 1582 |
+ self.unique_id = self.msg_obj.unique_id
|
|
| 1516 | 1583 |
self.fjid = self.msg_obj.fjid
|
| 1517 | 1584 |
self.msgtxt = self.msg_obj.msgtxt
|
| 1518 | 1585 |
self.jid = self.msg_obj.jid
|
| ... | ... | @@ -1064,9 +1064,9 @@ class Logger: |
| 1064 | 1064 |
:param msg: The message text
|
| 1065 | 1065 |
"""
|
| 1066 | 1066 |
|
| 1067 |
- # Add 5 minutes around the timestamp
|
|
| 1068 |
- start_time = timestamp - 300
|
|
| 1069 |
- end_time = timestamp + 300
|
|
| 1067 |
+ # Add 10 seconds around the timestamp
|
|
| 1068 |
+ start_time = timestamp - 10
|
|
| 1069 |
+ end_time = timestamp + 10
|
|
| 1070 | 1070 |
|
| 1071 | 1071 |
log.debug('start: %s, end: %s, jid: %s, message: %s',
|
| 1072 | 1072 |
start_time, end_time, jid, msg)
|
| ... | ... | @@ -1084,6 +1084,33 @@ class Logger: |
| 1084 | 1084 |
return True
|
| 1085 | 1085 |
return False
|
| 1086 | 1086 |
|
| 1087 |
+ def find_stanza_id(self, stanza_id, origin_id=None):
|
|
| 1088 |
+ """
|
|
| 1089 |
+ Checks if a stanza-id is already in the `logs` table
|
|
| 1090 |
+ |
|
| 1091 |
+ :param stanza_id: The stanza-id
|
|
| 1092 |
+ |
|
| 1093 |
+ return True if the stanza-id was found
|
|
| 1094 |
+ """
|
|
| 1095 |
+ ids = []
|
|
| 1096 |
+ if stanza_id is not None:
|
|
| 1097 |
+ ids.append(stanza_id)
|
|
| 1098 |
+ if origin_id is not None:
|
|
| 1099 |
+ ids.append(origin_id)
|
|
| 1100 |
+ |
|
| 1101 |
+ sql = '''
|
|
| 1102 |
+ SELECT stanza_id FROM logs
|
|
| 1103 |
+ WHERE stanza_id IN ({values}) LIMIT 1
|
|
| 1104 |
+ '''.format(values=', '.join('?' * len(ids)))
|
|
| 1105 |
+ |
|
| 1106 |
+ result = self.con.execute(sql, tuple(ids)).fetchone()
|
|
| 1107 |
+ |
|
| 1108 |
+ if result is not None:
|
|
| 1109 |
+ log.info('Found duplicated message, stanza-id: %s, origin-id: %s',
|
|
| 1110 |
+ stanza_id, origin_id)
|
|
| 1111 |
+ return True
|
|
| 1112 |
+ return False
|
|
| 1113 |
+ |
|
| 1087 | 1114 |
def insert_jid(self, jid, kind=None, type_=JIDConstant.NORMAL_TYPE):
|
| 1088 | 1115 |
"""
|
| 1089 | 1116 |
Insert a new jid into the `jids` table.
|
| ... | ... | @@ -1129,6 +1156,9 @@ class Logger: |
| 1129 | 1156 |
|
| 1130 | 1157 |
lastrowid = self.con.execute(sql, (jid_id, time_, kind) + tuple(kwargs.values())).lastrowid
|
| 1131 | 1158 |
|
| 1159 |
+ log.info('Insert into DB: jid: %s, time: %s, kind: %s, stanza_id: %s',
|
|
| 1160 |
+ jid, time_, kind, kwargs.get('stanza_id', None))
|
|
| 1161 |
+ |
|
| 1132 | 1162 |
if unread and kind == KindConstant.CHAT_MSG_RECV:
|
| 1133 | 1163 |
sql = '''INSERT INTO unread_messages (message_id, jid_id)
|
| 1134 | 1164 |
VALUES (?, (SELECT jid_id FROM jids WHERE jid = ?))'''
|
| ... | ... | @@ -127,14 +127,18 @@ class ConnectionArchive313: |
| 127 | 127 |
def _nec_mam_decrypted_message_received(self, obj):
|
| 128 | 128 |
if obj.conn.name != self.name:
|
| 129 | 129 |
return
|
| 130 |
+ # if self.archiving_namespace != nbxmpp.NS_MAM_2:
|
|
| 131 |
+ # Fallback duplicate search without stanza-id
|
|
| 130 | 132 |
duplicate = app.logger.search_for_duplicate(
|
| 131 | 133 |
obj.with_, obj.timestamp, obj.msgtxt)
|
| 132 |
- if not duplicate:
|
|
| 133 |
- app.logger.insert_into_logs(
|
|
| 134 |
- obj.with_, obj.timestamp, obj.kind,
|
|
| 135 |
- unread=False,
|
|
| 136 |
- message=obj.msgtxt,
|
|
| 137 |
- additional_data=obj.additional_data)
|
|
| 134 |
+ if duplicate:
|
|
| 135 |
+ return
|
|
| 136 |
+ app.logger.insert_into_logs(
|
|
| 137 |
+ obj.with_, obj.timestamp, obj.kind,
|
|
| 138 |
+ unread=False,
|
|
| 139 |
+ message=obj.msgtxt,
|
|
| 140 |
+ additional_data=obj.additional_data,
|
|
| 141 |
+ stanza_id=obj.unique_id)
|
|
| 138 | 142 |
|
| 139 | 143 |
def get_query_id(self):
|
| 140 | 144 |
self.mam_query_id = self.connection.getAnID()
|
| ... | ... | @@ -120,7 +120,8 @@ class ChatControlSession(stanza_session.EncryptedStanzaSession): |
| 120 | 120 |
jid, obj.timestamp, log_type,
|
| 121 | 121 |
message=msg_to_log,
|
| 122 | 122 |
subject=obj.subject,
|
| 123 |
- additional_data=obj.additional_data)
|
|
| 123 |
+ additional_data=obj.additional_data,
|
|
| 124 |
+ stanza_id=obj.unique_id)
|
|
| 124 | 125 |
|
| 125 | 126 |
# Handle chat states
|
| 126 | 127 |
if contact and (not obj.forwarded or not obj.sent):
|