Commit ebbe06d5 authored by Philipp Hörist's avatar Philipp Hörist Committed by Philipp Hörist

Refactor MAM into own module

- Rework the MAM Preference dialog
- Move MAM Preference dialog into a new gtk module
- Refactor all MAM code into own module
- Refactor the MAM code itself so we can easier test it in the future
- Add a misc module for smaller XEPs and move EME, Last Message Correction
Delay, OOB into it
- Add dedicated module for XEP-0082 Time Profiles
parent 72ee9af7
......@@ -29,6 +29,7 @@ from gajim import history_window
from gajim import disco
from gajim.history_sync import HistorySyncAssistant
from gajim.server_info import ServerInfoDialog
from gajim.gtk.mam_preferences import MamPreferences
# General Actions
......@@ -181,14 +182,13 @@ def on_import_contacts(action, param):
# Advanced Actions
def on_archiving_preferences(action, param):
def on_mam_preferences(action, param):
account = param.get_string()
if 'archiving_preferences' in interface.instances[account]:
interface.instances[account]['archiving_preferences'].window.\
present()
window = app.get_app_window(MamPreferences, account)
if window is None:
MamPreferences(account)
else:
interface.instances[account]['archiving_preferences'] = \
dialogs.Archiving313PreferencesWindow(account)
window.present()
def on_history_sync(action, param):
......
......@@ -356,7 +356,7 @@ class GajimApplication(Gtk.Application):
('-profile', app_actions.on_profile, 'feature', 's'),
('-xml-console', app_actions.on_xml_console, 'always', 's'),
('-server-info', app_actions.on_server_info, 'online', 's'),
('-archive', app_actions.on_archiving_preferences, 'feature', 's'),
('-archive', app_actions.on_mam_preferences, 'feature', 's'),
('-sync-history', app_actions.on_history_sync, 'online', 's'),
('-privacylists', app_actions.on_privacy_lists, 'feature', 's'),
('-send-server-message',
......
......@@ -809,8 +809,13 @@ class ChatControl(ChatControlBase):
def _nec_mam_decrypted_message_received(self, obj):
if obj.conn.name != self.account:
return
if obj.with_ != self.contact.jid:
return
if obj.muc_pm:
if not obj.with_ == self.contact.get_full_jid():
return
else:
if not obj.with_.bareMatch(self.contact.jid):
return
kind = '' # incoming
if obj.kind == KindConstant.CHAT_MSG_SENT:
......
......@@ -595,11 +595,17 @@ def prefers_app_menu():
return False
return app.prefers_app_menu()
def get_app_window(cls):
def get_app_window(cls, account=None):
for win in app.get_windows():
if isinstance(cls, str):
if type(win).__name__ == cls:
if account is not None:
if account != win.account:
continue
return win
elif isinstance(win, cls):
if account is not None:
if account != win.account:
continue
return win
return None
......@@ -305,7 +305,6 @@ class Config:
'use_keyring': [opt_bool, True, _('If true, Gajim will use the Systems Keyring to store account passwords.')],
'pgp_encoding': [ opt_str, '', _('Sets the encoding used by python-gnupg'), True],
'remote_commands': [opt_bool, False, _('If true, Gajim will execute XEP-0146 Commands.')],
'mam_blacklist': [opt_str, '', _('All non-compliant MAM Groupchats')],
}, {})
__options_per_key = {
......
......@@ -121,9 +121,6 @@ class CommonConnection:
self.privacy_rules_supported = False
self.vcard_supported = False
self.private_storage_supported = False
self.archiving_namespace = None
self.archiving_supported = False
self.archiving_313_supported = False
self.roster_supported = True
self.blocking_supported = False
self.addressing_supported = False
......@@ -1611,12 +1608,11 @@ class Connection(CommonConnection, ConnectionHandlers):
if obj.fjid == our_jid:
if nbxmpp.NS_MAM_2 in obj.features:
self.archiving_namespace = nbxmpp.NS_MAM_2
self.get_module('MAM').archiving_namespace = nbxmpp.NS_MAM_2
elif nbxmpp.NS_MAM_1 in obj.features:
self.archiving_namespace = nbxmpp.NS_MAM_1
if self.archiving_namespace:
self.archiving_supported = True
self.archiving_313_supported = True
self.get_module('MAM').archiving_namespace = nbxmpp.NS_MAM_1
if self.get_module('MAM').archiving_namespace:
self.get_module('MAM').available = True
get_action(self.name + '-archive').set_enabled(True)
for identity in obj.identities:
if identity['category'] == 'pubsub':
......
......@@ -45,8 +45,8 @@ from gajim.common.caps_cache import muc_caps_cache
from gajim.common.protocol.caps import ConnectionCaps
from gajim.common.protocol.bytestream import ConnectionSocks5Bytestream
from gajim.common.protocol.bytestream import ConnectionIBBytestream
from gajim.common.message_archiving import ConnectionArchive313
from gajim.common.connection_handlers_events import *
from gajim.common.modules.misc import parse_eme
from gajim.common import ged
from gajim.common.nec import NetworkEvent
......@@ -295,7 +295,9 @@ class ConnectionHandlersBase:
# XEPs that are based on Message
self._message_namespaces = set([nbxmpp.NS_HTTP_AUTH,
nbxmpp.NS_PUBSUB_EVENT,
nbxmpp.NS_ROSTERX])
nbxmpp.NS_ROSTERX,
nbxmpp.NS_MAM_1,
nbxmpp.NS_MAM_2])
app.ged.register_event_handler('iq-error-received', ged.CORE,
self._nec_iq_error_received)
......@@ -303,10 +305,6 @@ class ConnectionHandlersBase:
self._nec_presence_received)
app.ged.register_event_handler('message-received', ged.CORE,
self._nec_message_received)
app.ged.register_event_handler('mam-message-received', ged.CORE,
self._nec_message_received)
app.ged.register_event_handler('mam-gc-message-received', ged.CORE,
self._nec_message_received)
app.ged.register_event_handler('decrypted-message-received', ged.CORE,
self._nec_decrypted_message_received)
app.ged.register_event_handler('gc-message-received', ged.CORE,
......@@ -319,10 +317,6 @@ class ConnectionHandlersBase:
self._nec_presence_received)
app.ged.remove_event_handler('message-received', ged.CORE,
self._nec_message_received)
app.ged.remove_event_handler('mam-message-received', ged.CORE,
self._nec_message_received)
app.ged.remove_event_handler('mam-gc-message-received', ged.CORE,
self._nec_message_received)
app.ged.remove_event_handler('decrypted-message-received', ged.CORE,
self._nec_decrypted_message_received)
app.ged.remove_event_handler('gc-message-received', ged.CORE,
......@@ -460,37 +454,15 @@ class ConnectionHandlersBase:
app.plugin_manager.extension_point(
'decrypt', self, obj, self._on_message_received)
if not obj.encrypted:
# XEP-0380
enc_tag = obj.stanza.getTag('encryption', namespace=nbxmpp.NS_EME)
if enc_tag:
ns = enc_tag.getAttr('namespace')
if ns:
if ns == 'urn:xmpp:otr:0':
obj.msgtxt = _('This message was encrypted with OTR '
'and could not be decrypted.')
elif ns == 'jabber:x:encrypted':
obj.msgtxt = _('This message was encrypted with Legacy '
'OpenPGP and could not be decrypted. You can install '
'the PGP plugin to handle those messages.')
elif ns == 'urn:xmpp:openpgp:0':
obj.msgtxt = _('This message was encrypted with '
'OpenPGP for XMPP and could not be decrypted.')
else:
enc_name = enc_tag.getAttr('name')
if not enc_name:
enc_name = ns
obj.msgtxt = _('This message was encrypted with %s '
'and could not be decrypted.') % enc_name
eme = parse_eme(obj.stanza)
if eme is not None:
obj.msgtxt = eme
self._on_message_received(obj)
def _on_message_received(self, obj):
if isinstance(obj, MessageReceivedEvent):
app.nec.push_incoming_event(
DecryptedMessageReceivedEvent(
None, conn=self, msg_obj=obj, stanza_id=obj.unique_id))
else:
app.nec.push_incoming_event(
MamDecryptedMessageReceivedEvent(None, **vars(obj)))
app.nec.push_incoming_event(
DecryptedMessageReceivedEvent(
None, conn=self, msg_obj=obj, stanza_id=obj.unique_id))
def _nec_decrypted_message_received(self, obj):
if obj.conn.name != self.name:
......@@ -564,7 +536,7 @@ class ConnectionHandlersBase:
def _check_for_mam_compliance(self, room_jid, stanza_id):
namespace = muc_caps_cache.get_mam_namespace(room_jid)
if stanza_id is None and namespace == nbxmpp.NS_MAM_2:
helpers.add_to_mam_blacklist(room_jid)
log.warning('%s announces mam:2 without stanza-id')
def _nec_gc_message_received(self, obj):
if obj.conn.name != self.name:
......@@ -743,11 +715,10 @@ class ConnectionHandlersBase:
return sess
class ConnectionHandlers(ConnectionArchive313,
ConnectionSocks5Bytestream, ConnectionDisco, ConnectionCaps,
ConnectionHandlersBase, ConnectionJingle, ConnectionIBBytestream):
class ConnectionHandlers(ConnectionSocks5Bytestream, ConnectionDisco,
ConnectionCaps, ConnectionHandlersBase,
ConnectionJingle, ConnectionIBBytestream):
def __init__(self):
ConnectionArchive313.__init__(self)
ConnectionSocks5Bytestream.__init__(self)
ConnectionIBBytestream.__init__(self)
......@@ -772,9 +743,6 @@ ConnectionHandlersBase, ConnectionJingle, ConnectionIBBytestream):
app.nec.register_incoming_event(StreamConflictReceivedEvent)
app.nec.register_incoming_event(MessageReceivedEvent)
app.nec.register_incoming_event(ArchivingErrorReceivedEvent)
app.nec.register_incoming_event(
Archiving313PreferencesChangedReceivedEvent)
app.nec.register_incoming_event(NotificationEvent)
app.ged.register_event_handler('roster-set-received',
......@@ -799,7 +767,6 @@ ConnectionHandlersBase, ConnectionJingle, ConnectionIBBytestream):
def cleanup(self):
ConnectionHandlersBase.cleanup(self)
ConnectionCaps.cleanup(self)
ConnectionArchive313.cleanup(self)
app.ged.remove_event_handler('roster-set-received',
ged.CORE, self._nec_roster_set_received)
app.ged.remove_event_handler('roster-received', ged.CORE,
......@@ -1343,8 +1310,6 @@ ConnectionHandlersBase, ConnectionJingle, ConnectionIBBytestream):
con.RegisterHandler('iq', self._DiscoverItemsGetCB, 'get',
nbxmpp.NS_DISCO_ITEMS)
con.RegisterHandler('iq', self._ArchiveCB, ns=nbxmpp.NS_MAM_1)
con.RegisterHandler('iq', self._ArchiveCB, ns=nbxmpp.NS_MAM_2)
con.RegisterHandler('iq', self._JingleCB, 'result')
con.RegisterHandler('iq', self._JingleCB, 'error')
con.RegisterHandler('iq', self._JingleCB, 'set', nbxmpp.NS_JINGLE)
......
This diff is collapsed.
......@@ -43,7 +43,7 @@ import shlex
from gajim.common import caps_cache
import socket
import time
from datetime import datetime, timedelta, timezone, tzinfo
from datetime import datetime, timedelta
from distutils.version import LooseVersion as V
from encodings.punycode import punycode_encode
......@@ -89,77 +89,6 @@ log = logging.getLogger('gajim.c.helpers')
special_groups = (_('Transports'), _('Not in Roster'), _('Observers'), _('Groupchats'))
# Patterns for DateTime parsing XEP-0082
PATTERN_DATETIME = re.compile(
r'([0-9]{4}-[0-9]{2}-[0-9]{2})'
r'T'
r'([0-9]{2}:[0-9]{2}:[0-9]{2})'
r'(\.[0-9]{0,6})?'
r'(?:[0-9]+)?'
r'(?:(Z)|(?:([-+][0-9]{2}):([0-9]{2})))$'
)
PATTERN_DELAY = re.compile(
r'([0-9]{4}-[0-9]{2}-[0-9]{2})'
r'T'
r'([0-9]{2}:[0-9]{2}:[0-9]{2})'
r'(\.[0-9]{0,6})?'
r'(?:[0-9]+)?'
r'(?:(Z)|(?:([-+][0]{2}):([0]{2})))$'
)
ZERO = timedelta(0)
HOUR = timedelta(hours=1)
SECOND = timedelta(seconds=1)
STDOFFSET = timedelta(seconds=-time.timezone)
if time.daylight:
DSTOFFSET = timedelta(seconds=-time.altzone)
else:
DSTOFFSET = STDOFFSET
DSTDIFF = DSTOFFSET - STDOFFSET
class LocalTimezone(tzinfo):
'''
A class capturing the platform's idea of local time.
May result in wrong values on historical times in
timezones where UTC offset and/or the DST rules had
changed in the past.
'''
def fromutc(self, dt):
assert dt.tzinfo is self
stamp = (dt - datetime(1970, 1, 1, tzinfo=self)) // SECOND
args = time.localtime(stamp)[:6]
dst_diff = DSTDIFF // SECOND
# Detect fold
fold = (args == time.localtime(stamp - dst_diff))
return datetime(*args, microsecond=dt.microsecond,
tzinfo=self, fold=fold)
def utcoffset(self, dt):
if self._isdst(dt):
return DSTOFFSET
else:
return STDOFFSET
def dst(self, dt):
if self._isdst(dt):
return DSTDIFF
else:
return ZERO
def tzname(self, dt):
return 'local'
def _isdst(self, dt):
tt = (dt.year, dt.month, dt.day,
dt.hour, dt.minute, dt.second,
dt.weekday(), 0, 0)
stamp = time.mktime(tt)
tt = time.localtime(stamp)
return tt.tm_isdst > 0
class InvalidFormat(Exception):
pass
......@@ -673,56 +602,6 @@ def datetime_tuple(timestamp):
tim = tim.timetuple()
return tim
def parse_datetime(timestring, check_utc=False, convert='utc', epoch=False):
'''
Parse a XEP-0082 DateTime Profile String
https://xmpp.org/extensions/xep-0082.html
:param timestring: a XEP-0082 DateTime profile formated string
:param check_utc: if True, returns None if timestring is not
a timestring expressing UTC
:param convert: convert the given timestring to utc or local time
:param epoch: if True, returns the time in epoch
Examples:
'2017-11-05T01:41:20Z'
'2017-11-05T01:41:20.123Z'
'2017-11-05T01:41:20.123+05:00'
return a datetime or epoch
'''
if convert not in (None, 'utc', 'local'):
raise TypeError('"%s" is not a valid value for convert')
if check_utc:
match = PATTERN_DELAY.match(timestring)
else:
match = PATTERN_DATETIME.match(timestring)
if match:
timestring = ''.join(match.groups(''))
strformat = '%Y-%m-%d%H:%M:%S%z'
if match.group(3):
# Fractional second addendum to Time
strformat = '%Y-%m-%d%H:%M:%S.%f%z'
if match.group(4):
# UTC string denoted by addition of the character 'Z'
timestring = timestring[:-1] + '+0000'
try:
date_time = datetime.strptime(timestring, strformat)
except ValueError:
pass
else:
if not check_utc and convert == 'utc':
date_time = date_time.astimezone(timezone.utc)
if convert == 'local':
date_time = date_time.astimezone(LocalTimezone())
if epoch:
return date_time.timestamp()
return date_time
return None
from gajim.common import app
if app.is_installed('PYCURL'):
......@@ -1003,6 +882,9 @@ def get_full_jid_from_iq(iq_obj):
"""
Return the full jid (with resource) from an iq
"""
jid = iq_obj.getFrom()
if jid is None:
return None
return parse_jid(str(iq_obj.getFrom()))
def get_jid_from_iq(iq_obj):
......@@ -1626,21 +1508,3 @@ def get_emoticon_theme_path(theme):
emoticons_user_path = os.path.join(configpaths.get('MY_EMOTS'), theme)
if os.path.exists(emoticons_user_path):
return emoticons_user_path
def add_to_mam_blacklist(jid):
config_value = app.config.get('mam_blacklist')
if not config_value:
config_value = [jid]
else:
if jid in config_value:
return
config_value = config_value.split(',')
config_value.append(jid)
log.warning('Found not-compliant MUC. %s added to MAM Blacklist', jid)
app.config.set('mam_blacklist', ','.join(config_value))
def get_mam_blacklist():
config_value = app.config.get('mam_blacklist')
if not config_value:
return []
return config_value.split(',')
......@@ -374,14 +374,10 @@ class Logger:
"""
Return True if it's a room jid, False if it's not, None if we don't know
"""
row = self._con.execute(
'SELECT type FROM jids WHERE jid=?', (jid,)).fetchone()
if row is None:
return None
else:
if row.type == JIDConstant.ROOM_TYPE:
return True
return False
jid_ = self._jid_ids.get(jid)
if jid_ is None:
return
return jid_.type == JIDConstant.ROOM_TYPE
@staticmethod
def _get_family_jids(account, jid):
......
This diff is collapsed.
# This file is part of Gajim.
#
# Gajim 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; version 3 only.
#
# Gajim 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 Gajim. If not, see <http://www.gnu.org/licenses/>.
# XEP-0082: XMPP Date and Time Profiles
import re
import time
from datetime import datetime, timedelta, timezone, tzinfo
PATTERN_DATETIME = re.compile(
r'([0-9]{4}-[0-9]{2}-[0-9]{2})'
r'T'
r'([0-9]{2}:[0-9]{2}:[0-9]{2})'
r'(\.[0-9]{0,6})?'
r'(?:[0-9]+)?'
r'(?:(Z)|(?:([-+][0-9]{2}):([0-9]{2})))$'
)
PATTERN_DELAY = re.compile(
r'([0-9]{4}-[0-9]{2}-[0-9]{2})'
r'T'
r'([0-9]{2}:[0-9]{2}:[0-9]{2})'
r'(\.[0-9]{0,6})?'
r'(?:[0-9]+)?'
r'(?:(Z)|(?:([-+][0]{2}):([0]{2})))$'
)
ZERO = timedelta(0)
HOUR = timedelta(hours=1)
SECOND = timedelta(seconds=1)
STDOFFSET = timedelta(seconds=-time.timezone)
if time.daylight:
DSTOFFSET = timedelta(seconds=-time.altzone)
else:
DSTOFFSET = STDOFFSET
DSTDIFF = DSTOFFSET - STDOFFSET
class LocalTimezone(tzinfo):
'''
A class capturing the platform's idea of local time.
May result in wrong values on historical times in
timezones where UTC offset and/or the DST rules had
changed in the past.
'''
def fromutc(self, dt):
assert dt.tzinfo is self
stamp = (dt - datetime(1970, 1, 1, tzinfo=self)) // SECOND
args = time.localtime(stamp)[:6]
dst_diff = DSTDIFF // SECOND
# Detect fold
fold = (args == time.localtime(stamp - dst_diff))
return datetime(*args, microsecond=dt.microsecond,
tzinfo=self, fold=fold)
def utcoffset(self, dt):
if self._isdst(dt):
return DSTOFFSET
else:
return STDOFFSET
def dst(self, dt):
if self._isdst(dt):
return DSTDIFF
else:
return ZERO
def tzname(self, dt):
return 'local'
def _isdst(self, dt):
tt = (dt.year, dt.month, dt.day,
dt.hour, dt.minute, dt.second,
dt.weekday(), 0, 0)
stamp = time.mktime(tt)
tt = time.localtime(stamp)
return tt.tm_isdst > 0
def parse_datetime(timestring, check_utc=False,
convert='utc', epoch=False):
'''
Parse a XEP-0082 DateTime Profile String
:param timestring: a XEP-0082 DateTime profile formated string
:param check_utc: if True, returns None if timestring is not
a timestring expressing UTC
:param convert: convert the given timestring to utc or local time
:param epoch: if True, returns the time in epoch
Examples:
'2017-11-05T01:41:20Z'
'2017-11-05T01:41:20.123Z'
'2017-11-05T01:41:20.123+05:00'
return a datetime or epoch
'''
if convert not in (None, 'utc', 'local'):
raise TypeError('"%s" is not a valid value for convert')
if check_utc:
match = PATTERN_DELAY.match(timestring)
else:
match = PATTERN_DATETIME.match(timestring)
if match:
timestring = ''.join(match.groups(''))
strformat = '%Y-%m-%d%H:%M:%S%z'
if match.group(3):
# Fractional second addendum to Time
strformat = '%Y-%m-%d%H:%M:%S.%f%z'
if match.group(4):
# UTC string denoted by addition of the character 'Z'
timestring = timestring[:-1] + '+0000'
try:
date_time = datetime.strptime(timestring, strformat)
except ValueError:
pass
else:
if not check_utc and convert == 'utc':
date_time = date_time.astimezone(timezone.utc)
if convert == 'local':
date_time = date_time.astimezone(LocalTimezone())
if epoch:
return date_time.timestamp()
return date_time
return None
This diff is collapsed.
# This file is part of Gajim.
#
# Gajim 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; version 3 only.
#
# Gajim 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 Gajim. If not, see <http://www.gnu.org/licenses/>.
# All XEPs that dont need their own module
import logging
import nbxmpp
from gajim.common.modules.date_and_time import parse_datetime
log = logging.getLogger('gajim.c.m.misc')
# XEP-0380: Explicit Message Encryption
_eme_namespaces = {
'urn:xmpp:otr:0':
_('This message was encrypted with OTR '
'and could not be decrypted.'),
'jabber:x:encrypted':
_('This message was encrypted with Legacy '
'OpenPGP and could not be decrypted. You can install '
'the PGP plugin to handle those messages.'),
'urn:xmpp:openpgp:0':
_('This message was encrypted with '
'OpenPGP for XMPP and could not be decrypted.'),
'fallback':
_('This message was encrypted with %s '
'and could not be decrypted.')
}
def parse_eme(stanza):
enc_tag = stanza.getTag('encryption', namespace=nbxmpp.NS_EME)
if enc_tag is None:
return
ns = enc_tag.getAttr('namespace')
if ns is None:
log.warning('No namespace on EME message')
return
if ns in _eme_namespaces:
log.info('Found not decrypted message: %s', ns)
return _eme_namespaces.get(ns)
enc_name = enc_tag.getAttr('name')
log.info('Found not decrypted message: %s', enc_name or ns)
return _eme_namespaces.get('fallback') % enc_name or ns
# XEP-0203: Delayed Delivery
def parse_delay(stanza, epoch=True, convert='utc'):
timestamp = None
delay = stanza.getTagAttr(
'delay', 'stamp', namespace=nbxmpp.NS_DELAY2)
if delay is not None:
timestamp = parse_datetime(delay, check_utc=True,
epoch=epoch, convert=convert)
if timestamp is None:
log.warning('Invalid timestamp received: %s', delay)
log.warning(stanza)
return timestamp
# XEP-0066: Out of Band Data
def parse_oob(stanza, dict_=None, key='Gajim'):
oob_node = stanza.getTag('x', namespace=nbxmpp.NS_X_OOB)
if oob_node is None:
return
result = {}
url = oob_node.getTagData('url')
if url is not None:
result['oob_url'] = url
desc = oob_node.getTagData('desc')
if desc is not None:
result['oob_desc'] = desc
if dict_ is None:
return result
if key in dict_:
dict_[key] += result
else:
dict_[key] = result
return dict_
# XEP-0308: Last Message Correction
def parse_correction(stanza):
replace = stanza.getTag('replace', namespace=nbxmpp.NS_CORRECT)
if replace is not None:
id_ = replace.getAttr('id')
if id_ is not None:
return id_
log.warning('No id attr found: %s' % stanza)
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.18.3 -->
<interface>
<requires lib="gtk+" version="3.12"/>
<object class="GtkListStore" id="dialog_pref_liststore">
<columns>
<!-- column-name gchararray1 -->
<column type="gchararray"/>
</columns>
<data>
<row>
<col id="0" translatable="yes">Always</col>