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
This commit is contained in:
Philipp Hörist 2018-07-09 00:21:24 +02:00 committed by Philipp Hörist
parent 72ee9af79c
commit ebbe06d587
26 changed files with 1403 additions and 1462 deletions

View File

@ -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):

View File

@ -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',

View File

@ -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:

View File

@ -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

View File

@ -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 = {

View File

@ -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':

View File

@ -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)

View File

@ -77,7 +77,10 @@ class HelperEvent:
del self.conn.groupchat_jids[self.id_]
else:
self.fjid = helpers.get_full_jid_from_iq(self.stanza)
self.jid, self.resource = app.get_room_and_nick_from_fjid(self.fjid)
if self.fjid is None:
self.jid = None
else:
self.jid, self.resource = app.get_room_and_nick_from_fjid(self.fjid)
def get_id(self):
self.id_ = self.stanza.getID()
@ -630,240 +633,6 @@ class BeforeChangeShowEvent(nec.NetworkIncomingEvent):
name = 'before-change-show'
base_network_events = []
class MamMessageReceivedEvent(nec.NetworkIncomingEvent, HelperEvent):
name = 'mam-message-received'
base_network_events = ['raw-mam-message-received']
def __init__(self, name, base_event):
'''
Pre-Generated attributes on self:
:conn: Connection instance
:stanza: Complete stanza Node
:forwarded: Forwarded Node
:result: Result Node
'''
self._set_base_event_vars_as_attributes(base_event)
self.additional_data = {}
self.encrypted = False
self.groupchat = False
self.nick = None
self.self_message = None
self.muc_pm = None
def generate(self):
account = self.conn.name
archive_jid = self.stanza.getFrom()
own_jid = self.conn.get_own_jid()
if archive_jid and not archive_jid.bareMatch(own_jid):
# MAM Message not from our Archive
return False
self.msg_ = self.forwarded.getTag('message', protocol=True)
if self.msg_.getType() == 'groupchat':
return False
# use stanza-id as unique-id
self.unique_id, origin_id = self.get_unique_id()
self.message_id = self.msg_.getID()
# Check for duplicates
if app.logger.find_stanza_id(account,
own_jid.getStripped(),
self.unique_id, origin_id):
return
self.msgtxt = self.msg_.getTagData('body')
frm = self.msg_.getFrom()
# Some servers dont set the 'to' attribute when
# we send a message to ourself
to = self.msg_.getTo()
if to is None:
to = own_jid
if frm.bareMatch(own_jid):
self.with_ = to
self.kind = KindConstant.CHAT_MSG_SENT
else:
self.with_ = frm
self.kind = KindConstant.CHAT_MSG_RECV
delay = self.forwarded.getTagAttr(
'delay', 'stamp', namespace=nbxmpp.NS_DELAY2)
if delay is None:
log.error('Received MAM message without timestamp')
log.error(self.stanza)
return
self.timestamp = helpers.parse_datetime(
delay, check_utc=True, epoch=True)
if self.timestamp is None:
log.error('Received MAM message with invalid timestamp: %s', delay)
log.error(self.stanza)
return
# Save timestamp added by the user
user_delay = self.msg_.getTagAttr(
'delay', 'stamp', namespace=nbxmpp.NS_DELAY2)
if user_delay is not None:
self.user_timestamp = helpers.parse_datetime(
user_delay, check_utc=True, epoch=True)
if self.user_timestamp is None:
log.warning('Received MAM message with '
'invalid user timestamp: %s', user_delay)
log.warning(self.stanza)
log.debug('Received mam-message: unique id: %s', self.unique_id)
return True
def get_unique_id(self):
stanza_id = self.get_stanza_id(self.result, query=True)
if self._is_self_message(self.msg_) or self._is_muc_pm(self.msg_):
origin_id = self.msg_.getOriginID()
return stanza_id, origin_id
if self.conn.get_own_jid().bareMatch(self.msg_.getFrom()):
# message we sent
origin_id = self.msg_.getOriginID()
return stanza_id, origin_id
# A message we received
return stanza_id, None
class MamGcMessageReceivedEvent(nec.NetworkIncomingEvent, HelperEvent):
name = 'mam-gc-message-received'
base_network_events = ['raw-mam-message-received']
def __init__(self, name, base_event):
'''
Pre-Generated attributes on self:
:conn: Connection instance
:stanza: Complete stanza Node
:forwarded: Forwarded Node
:result: Result Node
:muc_pm: True, if this is a MUC PM
propagated to MamDecryptedMessageReceivedEvent
'''
self._set_base_event_vars_as_attributes(base_event)
self.additional_data = {}
self.encrypted = False
self.groupchat = True
self.kind = KindConstant.GC_MSG
def generate(self):
account = self.conn.name
self.msg_ = self.forwarded.getTag('message', protocol=True)
if self.msg_.getType() != 'groupchat':
return False
try:
self.room_jid = self.stanza.getFrom().getStripped()
except AttributeError:
log.warning('Received GC MAM message '
'without from attribute\n%s', self.stanza)
return False
self.unique_id = self.get_stanza_id(self.result, query=True)
self.message_id = self.msg_.getID()
# Check for duplicates
if app.logger.find_stanza_id(account,
self.room_jid,
self.unique_id,
groupchat=True):
return
self.msgtxt = self.msg_.getTagData('body')
self.with_ = self.msg_.getFrom().getStripped()
self.nick = self.msg_.getFrom().getResource()
# Get the real jid if we have it
self.real_jid = None
muc_user = self.msg_.getTag('x', namespace=nbxmpp.NS_MUC_USER)
if muc_user is not None:
self.real_jid = muc_user.getTagAttr('item', 'jid')
delay = self.forwarded.getTagAttr(
'delay', 'stamp', namespace=nbxmpp.NS_DELAY2)
if delay is None:
log.error('Received MAM message without timestamp')
log.error(self.stanza)
return
self.timestamp = helpers.parse_datetime(
delay, check_utc=True, epoch=True)
if self.timestamp is None:
log.error('Received MAM message with invalid timestamp: %s', delay)
log.error(self.stanza)
return
# Save timestamp added by the user
user_delay = self.msg_.getTagAttr(
'delay', 'stamp', namespace=nbxmpp.NS_DELAY2)
if user_delay is not None:
self.user_timestamp = helpers.parse_datetime(
user_delay, check_utc=True, epoch=True)
if self.user_timestamp is None:
log.warning('Received MAM message with '
'invalid user timestamp: %s', user_delay)
log.warning(self.stanza)
log.debug('Received mam-gc-message: unique id: %s', self.unique_id)
return True
class MamDecryptedMessageReceivedEvent(nec.NetworkIncomingEvent, HelperEvent):
name = 'mam-decrypted-message-received'
base_network_events = []
def generate(self):
self.correct_id = None
if not self.msgtxt:
# For example Chatstates, Receipts, Chatmarkers
log.debug('Received MAM message without text')
return
replace = self.msg_.getTag('replace', namespace=nbxmpp.NS_CORRECT)
if replace is not None:
self.correct_id = replace.getAttr('id')
self.get_oob_data(self.msg_)
if self.groupchat:
return True
if not self.muc_pm:
# muc_pm = False, means only there was no muc#user namespace
# This could still be a muc pm, we check the database if we
# know this jid. If not we disco it.
self.muc_pm = app.logger.jid_is_room_jid(self.with_.getStripped())
if self.muc_pm is None:
# Check if this event is triggered after a disco, so we dont
# run into an endless loop
if hasattr(self, 'disco'):
log.error('JID not known even after sucessful disco')
log.error(self.with_.getStripped())
return
# we don't know this JID, we need to disco it.
server = self.with_.getDomain()
if server not in self.conn.mam_awaiting_disco_result:
self.conn.mam_awaiting_disco_result[server] = [self]
self.conn.discoverInfo(server)
else:
self.conn.mam_awaiting_disco_result[server].append(self)
return
if self.muc_pm:
self.with_ = str(self.with_)
else:
self.with_ = self.with_.getStripped()
return True
class MessageReceivedEvent(nec.NetworkIncomingEvent, HelperEvent):
name = 'message-received'
base_network_events = ['raw-message-received']
@ -968,30 +737,6 @@ class MessageReceivedEvent(nec.NetworkIncomingEvent, HelperEvent):
return
self.forwarded = True
result = self.stanza.getTag('result', protocol=True)
if result and result.getNamespace() in (nbxmpp.NS_MAM_1,
nbxmpp.NS_MAM_2):
if result.getAttr('queryid') not in self.conn.mam_query_ids:
log.warning('Invalid MAM Message: unknown query id')
log.debug(self.stanza)
return
forwarded = result.getTag('forwarded',
namespace=nbxmpp.NS_FORWARD,
protocol=True)
if not forwarded:
log.warning('Invalid MAM Message: no forwarded child')
return
app.nec.push_incoming_event(
NetworkEvent('raw-mam-message-received',
conn=self.conn,
stanza=self.stanza,
forwarded=forwarded,
result=result))
return
# Mediated invitation?
muc_user = self.stanza.getTag('x', namespace=nbxmpp.NS_MUC_USER)
if muc_user:
@ -1085,7 +830,7 @@ class MessageReceivedEvent(nec.NetworkIncomingEvent, HelperEvent):
return
# Messages we receive live
if self.conn.archiving_namespace != nbxmpp.NS_MAM_2:
if self.conn.get_module('MAM').archiving_namespace != nbxmpp.NS_MAM_2:
# Only mam:2 ensures valid stanza-id
return
@ -1498,77 +1243,6 @@ class JingleErrorReceivedEvent(nec.NetworkIncomingEvent):
self.sid = self.jingle_session.sid
return True
class ArchivingReceivedEvent(nec.NetworkIncomingEvent):
name = 'archiving-received'
base_network_events = []
def generate(self):
self.type_ = self.stanza.getType()
if self.type_ not in ('result', 'set', 'error'):
return
return True
class ArchivingErrorReceivedEvent(nec.NetworkIncomingEvent):
name = 'archiving-error-received'
base_network_events = ['archiving-received']
def generate(self):
self.conn = self.base_event.conn
self.stanza = self.base_event.stanza
self.type_ = self.base_event.type_
if self.type_ == 'error':
self.error_msg = self.stanza.getErrorMsg()
return True
class ArchivingCountReceived(nec.NetworkIncomingEvent):
name = 'archiving-count-received'
base_network_events = []
def generate(self):
return True
class ArchivingIntervalFinished(nec.NetworkIncomingEvent):
name = 'archiving-interval-finished'
base_network_events = []
def generate(self):
return True
class ArchivingQueryID(nec.NetworkIncomingEvent):
name = 'archiving-query-id'
base_network_events = []
def generate(self):
return True
class Archiving313PreferencesChangedReceivedEvent(nec.NetworkIncomingEvent):
name = 'archiving-313-preferences-changed-received'
base_network_events = ['archiving-received']
def generate(self):
self.conn = self.base_event.conn
self.stanza = self.base_event.stanza
self.type_ = self.base_event.type_
self.items = []
self.default = None
self.id = self.stanza.getID()
self.answer = None
prefs = self.stanza.getTag('prefs')
if self.type_ != 'result' or not prefs:
return
self.default = prefs.getAttr('default')
for item in prefs.getTag('always').getTags('jid'):
self.items.append((item.getData(), 'Always'))
for item in prefs.getTag('never').getTags('jid'):
self.items.append((item.getData(), 'Never'))
return True
class AccountCreatedEvent(nec.NetworkIncomingEvent):
name = 'account-created'
base_network_events = []

View File

@ -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(',')

View File

@ -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):

View File

@ -1,372 +0,0 @@
# -*- coding:utf-8 -*-
## src/common/message_archiving.py
##
## Copyright (C) 2009 Anaël Verrier <elghinn AT free.fr>
##
## 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/>.
##
import logging
from datetime import datetime, timedelta
import nbxmpp
from gajim.common import app
from gajim.common import ged
from gajim.common import helpers
from gajim.common.const import ArchiveState, JIDConstant
from gajim.common.caps_cache import muc_caps_cache
import gajim.common.connection_handlers_events as ev
log = logging.getLogger('gajim.c.message_archiving')
class ConnectionArchive313:
def __init__(self):
self.archiving_313_supported = False
self.mam_awaiting_disco_result = {}
self.iq_answer = []
self.mam_query_ids = []
app.nec.register_incoming_event(ev.MamMessageReceivedEvent)
app.nec.register_incoming_event(ev.MamGcMessageReceivedEvent)
app.ged.register_event_handler('agent-info-error-received', ged.CORE,
self._nec_agent_info_error)
app.ged.register_event_handler('agent-info-received', ged.CORE,
self._nec_agent_info)
app.ged.register_event_handler('mam-decrypted-message-received',
ged.CORE, self._nec_mam_decrypted_message_received)
app.ged.register_event_handler(
'archiving-313-preferences-changed-received', ged.CORE,
self._nec_archiving_313_preferences_changed_received)
def cleanup(self):
app.ged.remove_event_handler('agent-info-error-received', ged.CORE,
self._nec_agent_info_error)
app.ged.remove_event_handler('agent-info-received', ged.CORE,
self._nec_agent_info)
app.ged.remove_event_handler('mam-decrypted-message-received',
ged.CORE, self._nec_mam_decrypted_message_received)
app.ged.remove_event_handler(
'archiving-313-preferences-changed-received', ged.CORE,
self._nec_archiving_313_preferences_changed_received)
def _nec_archiving_313_preferences_changed_received(self, obj):
if obj.id in self.iq_answer:
obj.answer = True
def _nec_agent_info_error(self, obj):
if obj.jid in self.mam_awaiting_disco_result:
log.warn('Unable to discover %s, ignoring those logs', obj.jid)
del self.mam_awaiting_disco_result[obj.jid]
def _nec_agent_info(self, obj):
if obj.jid not in self.mam_awaiting_disco_result:
return
for identity in obj.identities:
if identity['category'] != 'conference':
continue
# it's a groupchat
for msg_obj in self.mam_awaiting_disco_result[obj.jid]:
app.logger.insert_jid(msg_obj.with_.getStripped(),
type_=JIDConstant.ROOM_TYPE)
app.nec.push_incoming_event(
ev.MamDecryptedMessageReceivedEvent(
None, disco=True, **vars(msg_obj)))
del self.mam_awaiting_disco_result[obj.jid]
return
# it's not a groupchat
for msg_obj in self.mam_awaiting_disco_result[obj.jid]:
app.logger.insert_jid(msg_obj.with_.getStripped())
app.nec.push_incoming_event(
ev.MamDecryptedMessageReceivedEvent(
None, disco=True, **vars(msg_obj)))
del self.mam_awaiting_disco_result[obj.jid]
@staticmethod
def parse_iq(stanza):
if not nbxmpp.isResultNode(stanza):
log.error('Error on MAM query: %s', stanza.getError())
raise InvalidMamIQ
fin = stanza.getTag('fin')
if fin is None:
log.error('Malformed MAM query result received: %s', stanza)
raise InvalidMamIQ
set_ = fin.getTag('set', namespace=nbxmpp.NS_RSM)
if set_ is None:
log.error(
'Malformed MAM query result received (no "set" Node): %s',
stanza)
raise InvalidMamIQ
return fin, set_
def parse_from_jid(self, stanza):
jid = stanza.getFrom()
if jid is None:
# No from means, iq from our own archive
jid = self.get_own_jid().getStripped()
else:
jid = jid.getStripped()
return jid
def _result_finished(self, conn, stanza, query_id, start_date, groupchat):
try:
fin, set_ = self.parse_iq(stanza)
except InvalidMamIQ:
return
last = set_.getTagData('last')
if last is None:
log.info('End of MAM query, no items retrieved')
return
jid = self.parse_from_jid(stanza)
complete = fin.getAttr('complete')
app.logger.set_archive_timestamp(jid, last_mam_id=last)
if complete != 'true':
self.mam_query_ids.remove(query_id)
query_id = self.get_query_id()
query = self.get_archive_query(query_id, jid=jid, after=last)
self._send_archive_query(query, query_id, groupchat=groupchat)
else:
self.mam_query_ids.remove(query_id)
if start_date is not None:
app.logger.set_archive_timestamp(
jid,
last_mam_id=last,
oldest_mam_timestamp=start_date.timestamp())
log.info('End of MAM query, last mam id: %s', last)
def _intervall_result_finished(self, conn, stanza, query_id,
start_date, end_date, event_id):
try:
fin, set_ = self.parse_iq(stanza)
except InvalidMamIQ:
return
self.mam_query_ids.remove(query_id)
jid = self.parse_from_jid(stanza)
if start_date:
timestamp = start_date.timestamp()
else:
timestamp = ArchiveState.ALL
last = set_.getTagData('last')
if last is None:
app.nec.push_incoming_event(ev.ArchivingIntervalFinished(
None, event_id=event_id))
app.logger.set_archive_timestamp(
jid, oldest_mam_timestamp=timestamp)
log.info('End of MAM query, no items retrieved')
return
complete = fin.getAttr('complete')
if complete != 'true':
self.request_archive_interval(event_id, start_date, end_date, last)
else:
log.info('query finished')
app.logger.set_archive_timestamp(
jid, oldest_mam_timestamp=timestamp)
app.nec.push_incoming_event(ev.ArchivingIntervalFinished(
None, event_id=event_id, stanza=stanza))
def _received_count(self, conn, stanza, query_id, event_id):
try:
_, set_ = self.parse_iq(stanza)
except InvalidMamIQ:
return
self.mam_query_ids.remove(query_id)
count = set_.getTagData('count')
log.info('message count received: %s', count)
app.nec.push_incoming_event(ev.ArchivingCountReceived(
None, event_id=event_id, count=count))
def _nec_mam_decrypted_message_received(self, obj):
if obj.conn.name != self.name:
return
namespace = self.archiving_namespace
blacklisted = False
if obj.groupchat:
namespace = muc_caps_cache.get_mam_namespace(obj.room_jid)
blacklisted = obj.room_jid in helpers.get_mam_blacklist()
if namespace != nbxmpp.NS_MAM_2 or blacklisted:
# Fallback duplicate search without stanza-id
duplicate = app.logger.search_for_duplicate(
self.name, obj.with_, obj.timestamp, obj.msgtxt)
if duplicate:
# dont propagate the event further
return True
app.logger.insert_into_logs(self.name,
obj.with_,
obj.timestamp,
obj.kind,
unread=False,
message=obj.msgtxt,
contact_name=obj.nick,
additional_data=obj.additional_data,
stanza_id=obj.unique_id)
def get_query_id(self):
query_id = self.connection.getAnID()
self.mam_query_ids.append(query_id)
return query_id
def request_archive_on_signin(self):
own_jid = self.get_own_jid().getStripped()
archive = app.logger.get_archive_timestamp(own_jid)
# Migration of last_mam_id from config to DB
if archive is not None:
mam_id = archive.last_mam_id
else:
mam_id = app.config.get_per('accounts', self.name, 'last_mam_id')
start_date = None
query_id = self.get_query_id()
if mam_id:
log.info('MAM query after: %s', mam_id)
query = self.get_archive_query(query_id, after=mam_id)
else:
# First Start, we request the last week
start_date = datetime.utcnow() - timedelta(days=7)
log.info('First start: query archive start: %s', start_date)
query = self.get_archive_query(query_id, start=start_date)
self._send_archive_query(query, query_id, start_date)
def request_archive_on_muc_join(self, jid):
archive = app.logger.get_archive_timestamp(
jid, type_=JIDConstant.ROOM_TYPE)
query_id = self.get_query_id()
start_date = None
if archive is not None:
log.info('Query Groupchat MAM Archive %s after %s:',
jid, archive.last_mam_id)
query = self.get_archive_query(
query_id, jid=jid, after=archive.last_mam_id)
else:
# First Start, we dont request history
# Depending on what a MUC saves, there could be thousands
# of Messages even in just one day.
start_date = datetime.utcnow() - timedelta(days=1)
log.info('First join: query archive %s from: %s', jid, start_date)
query = self.get_archive_query(query_id, jid=jid, start=start_date)
self._send_archive_query(query, query_id, start_date, groupchat=True)
def request_archive_count(self, event_id, start_date, end_date):
query_id = self.get_query_id()
query = self.get_archive_query(
query_id, start=start_date, end=end_date, max_=0)
self.connection.SendAndCallForResponse(
query, self._received_count, {'query_id': query_id,
'event_id': event_id})
def request_archive_interval(self, event_id, start_date,
end_date, after=None):
query_id = self.get_query_id()
query = self.get_archive_query(query_id, start=start_date,
end=end_date, after=after, max_=30)
app.nec.push_incoming_event(ev.ArchivingQueryID(
None, event_id=event_id, query_id=query_id))
self.connection.SendAndCallForResponse(
query, self._intervall_result_finished, {'query_id': query_id,
'start_date': start_date,
'end_date': end_date,
'event_id': event_id})
def _send_archive_query(self, query, query_id, start_date=None,
groupchat=False):
self.connection.SendAndCallForResponse(
query, self._result_finished, {'query_id': query_id,
'start_date': start_date,
'groupchat': groupchat})
def get_archive_query(self, query_id, jid=None, start=None, end=None, with_=None,
after=None, max_=30):
# Muc archive query?
namespace = muc_caps_cache.get_mam_namespace(jid)
if namespace is None:
# Query to our own archive
namespace = self.archiving_namespace
iq = nbxmpp.Iq('set', to=jid)
query = iq.addChild('query', namespace=namespace)
form = query.addChild(node=nbxmpp.DataForm(typ='submit'))
field = nbxmpp.DataField(typ='hidden',
name='FORM_TYPE',
value=namespace)
form.addChild(node=field)
if start:
field = nbxmpp.DataField(typ='text-single',
name='start',
value=start.strftime('%Y-%m-%dT%H:%M:%SZ'))
form.addChild(node=field)
if end:
field = nbxmpp.DataField(typ='text-single',
name='end',
value=end.strftime('%Y-%m-%dT%H:%M:%SZ'))
form.addChild(node=field)
if with_:
field = nbxmpp.DataField(typ='jid-single', name='with', value=with_)
form.addChild(node=field)
set_ = query.setTag('set', namespace=nbxmpp.NS_RSM)
set_.setTagData('max', max_)
if after:
set_.setTagData('after', after)
query.setAttr('queryid', query_id)
return iq
def request_archive_preferences(self):
if not app.account_is_connected(self.name):
return
iq = nbxmpp.Iq(typ='get')
id_ = self.connection.getAnID()
iq.setID(id_)
iq.addChild(name='prefs', namespace=self.archiving_namespace)
self.connection.send(iq)
def set_archive_preferences(self, items, default):
if not app.account_is_connected(self.name):
return
iq = nbxmpp.Iq(typ='set')
id_ = self.connection.getAnID()
self.iq_answer.append(id_)
iq.setID(id_)
prefs = iq.addChild(name='prefs', namespace=self.archiving_namespace, attrs={'default': default})
always = prefs.addChild(name='always')
never = prefs.addChild(name='never')
for item in items:
jid, preference = item
if preference == 'always':
always.addChild(name='jid').setData(jid)
else:
never.addChild(name='jid').setData(jid)
self.connection.send(iq)
def _ArchiveCB(self, con, iq_obj):
app.nec.push_incoming_event(ev.ArchivingReceivedEvent(None, conn=self,
stanza=iq_obj))
raise nbxmpp.NodeProcessed
class InvalidMamIQ(Exception):
pass

View File

@ -0,0 +1,144 @@
# 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

626
gajim/common/modules/mam.py Normal file
View File

@ -0,0 +1,626 @@
# 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-0313: Message Archive Management
import logging
from datetime import datetime, timedelta
import nbxmpp
from gajim.common import app
from gajim.common.nec import NetworkIncomingEvent
from gajim.common.const import ArchiveState, JIDConstant, KindConstant
from gajim.common.caps_cache import muc_caps_cache
from gajim.common.modules.misc import parse_delay
from gajim.common.modules.misc import parse_oob
from gajim.common.modules.misc import parse_correction
from gajim.common.modules.misc import parse_eme
log = logging.getLogger('gajim.c.m.archiving')
class MAM:
def __init__(self, con):
self._con = con
self._account = con.name
self.handlers = [
('message', self._mam_message_received, nbxmpp.NS_MAM_1),
('message', self._mam_message_received, nbxmpp.NS_MAM_2)
]
self.available = False
self.archiving_namespace = None
self._mam_query_ids = {}
def _from_valid_archive(self, stanza, message, groupchat):
if groupchat:
expected_archive = message.getFrom()
else:
expected_archive = self._con.get_own_jid()
archive_jid = stanza.getFrom()
if archive_jid is None:
if groupchat:
return
# Message from our own archive
return self._con.get_own_jid()
else:
if archive_jid.bareMatch(expected_archive):
return archive_jid
@staticmethod
def _is_self_message(message, groupchat):
if groupchat:
return False
frm = message.getFrom()
to = message.getTo()
return frm.bareMatch(to)
@staticmethod
def _is_muc_pm(message, groupchat, with_):
if groupchat:
return False
muc_user = message.getTag('x', namespace=nbxmpp.NS_MUC_USER)
if muc_user is not None:
return muc_user.getChildren() == []
else:
# muc#user namespace was added in MUC 1.28 so we need a fallback
# Check if we know the jid, otherwise disco it
if app.logger.jid_is_room_jid(with_.getStripped()):
return True
return False
def _get_unique_id(self, result, message, groupchat, self_message, muc_pm):
stanza_id = result.getAttr('id')
if groupchat:
return stanza_id, None
origin_id = message.getOriginID()
if self_message:
return None, origin_id
if muc_pm:
return stanza_id, origin_id
if self._con.get_own_jid().bareMatch(message.getFrom()):
# message we sent
return stanza_id, origin_id
# A message we received
return stanza_id, None
def _mam_message_received(self, conn, stanza):
app.nec.push_incoming_event(
NetworkIncomingEvent('raw-mam-message-received',
conn=self._con,
stanza=stanza))
result = stanza.getTag('result', protocol=True)
queryid = result.getAttr('queryid')
forwarded = result.getTag('forwarded',
namespace=nbxmpp.NS_FORWARD,
protocol=True)
message = forwarded.getTag('message', protocol=True)
groupchat = message.getType() == 'groupchat'
archive_jid = self._from_valid_archive(stanza, message, groupchat)
if archive_jid is None:
log.warning('Message from invalid archive %s', stanza)
raise nbxmpp.NodeProcessed
log.info('Received message from archive: %s', archive_jid)
if not self._is_valid_request(archive_jid, queryid):
log.warning('Invalid MAM Message: unknown query id')
log.debug(stanza)
raise nbxmpp.NodeProcessed
# Timestamp parsing
timestamp = parse_delay(forwarded)
if timestamp is None:
raise nbxmpp.NodeProcessed
user_timestamp = parse_delay(message)
# Fix for self messaging
if not groupchat:
to = message.getTo()
if to is None:
# Some servers dont set the 'to' attribute when
# we send a message to ourself
message.setTo(self._con.get_own_jid())
event_attrs = {}
if groupchat:
event_attrs.update(self._parse_gc_attrs(message))
else:
event_attrs.update(self._parse_chat_attrs(message))
self_message = self._is_self_message(message, groupchat)
muc_pm = self._is_muc_pm(message, groupchat, event_attrs['with_'])
stanza_id, origin_id = self._get_unique_id(
result, message, groupchat, self_message, muc_pm)
message_id = message.getID()
# Check for duplicates
namespace = self.archiving_namespace
if groupchat:
namespace = muc_caps_cache.get_mam_namespace(
archive_jid.getStripped())
if namespace == nbxmpp.NS_MAM_2:
# Search only with stanza-id for duplicates on mam:2
if app.logger.find_stanza_id(self._account,
archive_jid.getStripped(),
stanza_id,
origin_id,
groupchat=groupchat):
log.info('Found duplicate with stanza-id')
raise nbxmpp.NodeProcessed
msgtxt = message.getTagData('body')
event_attrs.update(
{'conn': self._con,
'additional_data': {},
'encrypted': False,
'timestamp': timestamp,
'user_timestamp': user_timestamp,
'self_message': self_message,
'groupchat': groupchat,
'muc_pm': muc_pm,
'stanza_id': stanza_id,
'origin_id': origin_id,
'message_id': message_id,
'correct_id': None,
'archive_jid': archive_jid,
'msgtxt': msgtxt,
'message': message,
'namespace': namespace,
})
if groupchat:
event = MamGcMessageReceivedEvent(None, **event_attrs)
else:
event = MamMessageReceivedEvent(None, **event_attrs)
app.plugin_manager.extension_point(
'decrypt', self._con, event, self._decryption_finished)
if not event.encrypted:
eme = parse_eme(event.message)
if eme is not None:
event.msgtxt = eme
self._decryption_finished(event)
raise nbxmpp.NodeProcessed
def _parse_gc_attrs(self, message):
with_ = message.getFrom()
nick = message.getFrom().getResource()
# Get the real jid if we have it
real_jid = None
muc_user = message.getTag('x', namespace=nbxmpp.NS_MUC_USER)
if muc_user is not None:
real_jid = muc_user.getTagAttr('item', 'jid')
if real_jid is not None:
real_jid = nbxmpp.JID(real_jid)
return {'with_': with_,
'nick': nick,
'real_jid': real_jid,
'kind': KindConstant.GC_MSG}
def _parse_chat_attrs(self, message):
frm = message.getFrom()
to = message.getTo()
if frm.bareMatch(self._con.get_own_jid()):
with_ = to
kind = KindConstant.CHAT_MSG_SENT
else:
with_ = frm
kind = KindConstant.CHAT_MSG_RECV
return {'with_': with_,
'nick': None,
'kind': kind}
def _decryption_finished(self, event):
if not event.msgtxt:
# For example Chatstates, Receipts, Chatmarkers
log.debug(event.message.getProperties())
return
log.debug(event.msgtxt)
event.correct_id = parse_correction(event.message)
parse_oob(event.message, event.additional_data)
with_ = event.with_.getStripped()
if event.muc_pm:
# we store the message with the full JID
with_ = str(event.with_)
stanza_id = event.stanza_id
if event.self_message:
# Self messages can only be deduped with origin-id
if event.origin_id is None:
log.warning('Self message without origin-id found')
return
stanza_id = event.origin_id
if event.namespace == nbxmpp.NS_MAM_1:
if app.logger.search_for_duplicate(
self._account, with_, event.timestamp, event.msgtxt):
log.info('Found duplicate with fallback for mam:1')
return
app.logger.insert_into_logs(self._account,
with_,
event.timestamp,
event.kind,
unread=False,
message=event.msgtxt,
contact_name=event.nick,
additional_data=event.additional_data,
stanza_id=stanza_id)
app.nec.push_incoming_event(
MamDecryptedMessageReceived(None, **vars(event)))
def _is_valid_request(self, jid, query_id):
if query_id is None:
return False
valid_id = self._mam_query_ids.get(jid.getStripped(), None)
return valid_id == query_id
def _get_query_id(self, jid):
query_id = self._con.connection.getAnID()
self._mam_query_ids[jid] = query_id
return query_id
@staticmethod
def _parse_iq(stanza):
if not nbxmpp.isResultNode(stanza):
log.error('Error on MAM query: %s', stanza.getError())
raise InvalidMamIQ
fin = stanza.getTag('fin')
if fin is None:
log.error('Malformed MAM query result received: %s', stanza)
raise InvalidMamIQ
set_ = fin.getTag('set', namespace=nbxmpp.NS_RSM)
if set_ is None:
log.error(
'Malformed MAM query result received (no "set" Node): %s',
stanza)
raise InvalidMamIQ
return fin, set_
def _get_from_jid(self, stanza):
jid = stanza.getFrom()
if jid is None:
# No from means, iq from our own archive
jid = self._con.get_own_jid().getStripped()
else:
jid = jid.getStripped()
return jid
def request_archive_count(self, start_date, end_date):
jid = self._con.get_own_jid().getStripped()
log.info('Request archive count from: %s', jid)
query_id = self._get_query_id(jid)
query = self._get_archive_query(
query_id, start=start_date, end=end_date, max_=0)
self._con.connection.SendAndCallForResponse(
query, self._received_count, {'query_id': query_id})
return query_id
def _received_count(self, conn, stanza, query_id):
try:
_, set_ = self._parse_iq(stanza)
except InvalidMamIQ:
return
jid = self._get_from_jid(stanza)
self._mam_query_ids.pop(jid)
count = set_.getTagData('count')
log.info('Received archive count: %s', count)
app.nec.push_incoming_event(ArchivingCountReceived(
None, query_id=query_id, count=count))
def request_archive_on_signin(self):
own_jid = self._con.get_own_jid().getStripped()
if own_jid in self._mam_query_ids:
log.warning('MAM request for %s already running', own_jid)
return
archive = app.logger.get_archive_timestamp(own_jid)
# Migration of last_mam_id from config to DB
if archive is not None:
mam_id = archive.last_mam_id
else:
mam_id = app.config.get_per(
'accounts', self._account, 'last_mam_id')
if mam_id:
app.config.del_per('accounts', self._account, 'last_mam_id')
start_date = None
query_id = self._get_query_id(own_jid)
if mam_id:
log.info('MAM query after: %s', mam_id)
query = self._get_archive_query(query_id, after=mam_id)
else:
# First Start, we request the last week
start_date = datetime.utcnow() - timedelta(days=7)
log.info('First start: query archive start: %s', start_date)
query = self._get_archive_query(query_id, start=start_date)
self._send_archive_query(query, query_id, start_date)
def request_archive_on_muc_join(self, jid):
archive = app.logger.get_archive_timestamp(
jid, type_=JIDConstant.ROOM_TYPE)
query_id = self._get_query_id(jid)
start_date = None
if archive is not None:
log.info('Request from archive %s after %s:',
jid, archive.last_mam_id)
query = self._get_archive_query(
query_id, jid=jid, after=archive.last_mam_id)
else:
# First Start, we dont request history
# Depending on what a MUC saves, there could be thousands
# of Messages even in just one day.
start_date = datetime.utcnow() - timedelta(days=1)
log.info('First join: query archive %s from: %s', jid, start_date)
query = self._get_archive_query(query_id, jid=jid, start=start_date)
self._send_archive_query(query, query_id, start_date, groupchat=True)
def _send_archive_query(self, query, query_id, start_date=None,
groupchat=False):
self._con.connection.SendAndCallForResponse(
query, self._result_finished, {'query_id': query_id,
'start_date': start_date,
'groupchat': groupchat})
def _result_finished(self, conn, stanza, query_id, start_date, groupchat):
try:
fin, set_ = self._parse_iq(stanza)
except InvalidMamIQ:
return
jid = self._get_from_jid(stanza)
last = set_.getTagData('last')
if last is None:
log.info('End of MAM query, no items retrieved')
self._mam_query_ids.pop(jid)
return
complete = fin.getAttr('complete')
app.logger.set_archive_timestamp(jid, last_mam_id=last)
if complete != 'true':
self._mam_query_ids.pop(jid)
query_id = self._get_query_id(jid)
query = self._get_archive_query(query_id, jid=jid, after=last)
self._send_archive_query(query, query_id, groupchat=groupchat)
else:
self._mam_query_ids.pop(jid)
if start_date is not None:
app.logger.set_archive_timestamp(
jid,
last_mam_id=last,
oldest_mam_timestamp=start_date.timestamp())
log.info('End of MAM query, last mam id: %s', last)
def request_archive_interval(self, start_date, end_date, after=None,
query_id=None):
jid = self._con.get_own_jid().getStripped()
if after is None:
log.info('Request intervall from %s to %s from %s',
start_date, end_date, jid)
else:
log.info('Query page after %s from %s',
after, jid)
if query_id is None:
query_id = self._get_query_id(jid)
self._mam_query_ids[jid] = query_id
query = self._get_archive_query(query_id, start=start_date,
end=end_date, after=after, max_=30)
self._con.connection.SendAndCallForResponse(
query, self._intervall_result, {'query_id': query_id,
'start_date': start_date,
'end_date': end_date})
return query_id
def _intervall_result(self, conn, stanza, query_id,
start_date, end_date):
try:
fin, set_ = self._parse_iq(stanza)
except InvalidMamIQ:
return
jid = self._get_from_jid(stanza)
self._mam_query_ids.pop(jid)
if start_date:
timestamp = start_date.timestamp()
else:
timestamp = ArchiveState.ALL
last = set_.getTagData('last')
if last is None:
app.nec.push_incoming_event(ArchivingIntervalFinished(
None, query_id=query_id))
app.logger.set_archive_timestamp(
jid, oldest_mam_timestamp=timestamp)
log.info('End of MAM request, no items retrieved')
return
complete = fin.getAttr('complete')
if complete != 'true':
self.request_archive_interval(start_date, end_date, last, query_id)
else:
log.info('Request finished')
app.logger.set_archive_timestamp(
jid, oldest_mam_timestamp=timestamp)
app.nec.push_incoming_event(ArchivingIntervalFinished(
None, query_id=query_id))
def _get_archive_query(self, query_id, jid=None, start=None, end=None,
with_=None, after=None, max_=30):
# Muc archive query?
namespace = muc_caps_cache.get_mam_namespace(jid)
if namespace is None:
# Query to our own archive
namespace = self.archiving_namespace
iq = nbxmpp.Iq('set', to=jid)
query = iq.addChild('query', namespace=namespace)
form = query.addChild(node=nbxmpp.DataForm(typ='submit'))
field = nbxmpp.DataField(typ='hidden',
name='FORM_TYPE',
value=namespace)
form.addChild(node=field)
if start:
field = nbxmpp.DataField(typ='text-single',
name='start',
value=start.strftime('%Y-%m-%dT%H:%M:%SZ'))
form.addChild(node=field)
if end:
field = nbxmpp.DataField(typ='text-single',
name='end',
value=end.strftime('%Y-%m-%dT%H:%M:%SZ'))
form.addChild(node=field)
if with_:
field = nbxmpp.DataField(typ='jid-single', name='with', value=with_)
form.addChild(node=field)
set_ = query.setTag('set', namespace=nbxmpp.NS_RSM)
set_.setTagData('max', max_)
if after:
set_.setTagData('after', after)
query.setAttr('queryid', query_id)
return iq
def request_mam_preferences(self):
log.info('Request MAM preferences')
iq = nbxmpp.Iq('get', self.archiving_namespace)
iq.setQuery('prefs')
self._con.connection.SendAndCallForResponse(
iq, self._preferences_received)
def _preferences_received(self, stanza):
if not nbxmpp.isResultNode(stanza):
log.info('Error: %s', stanza.getError())
app.nec.push_incoming_event(MAMPreferenceError(
None, conn=self._con, error=stanza.getError()))
return
log.info('Received MAM preferences')
prefs = stanza.getTag('prefs', namespace=self.archiving_namespace)
if prefs is None:
log.error('Malformed stanza (no prefs node): %s', stanza)
return
rules = []
default = prefs.getAttr('default')
for item in prefs.getTag('always').getTags('jid'):
rules.append((item.getData(), 'Always'))
for item in prefs.getTag('never').getTags('jid'):
rules.append((item.getData(), 'Never'))
app.nec.push_incoming_event(MAMPreferenceReceived(
None, conn=self._con, rules=rules, default=default))
def set_mam_preferences(self, rules, default):
iq = nbxmpp.Iq(typ='set')
prefs = iq.addChild(name='prefs',
namespace=self.archiving_namespace,
attrs={'default': default})
always = prefs.addChild(name='always')
never = prefs.addChild(name='never')
for item in rules:
jid, archive = item
if archive:
always.addChild(name='jid').setData(jid)
else:
never.addChild(name='jid').setData(jid)
self._con.connection.SendAndCallForResponse(
iq, self._preferences_saved)
def _preferences_saved(self, stanza):
if not nbxmpp.isResultNode(stanza):
log.info('Error: %s', stanza.getError())
app.nec.push_incoming_event(MAMPreferenceError(
None, conn=self._con, error=stanza.getError()))
else:
log.info('Preferences saved')
app.nec.push_incoming_event(
MAMPreferenceSaved(None, conn=self._con))
class MamMessageReceivedEvent(NetworkIncomingEvent):
name = 'mam-message-received'
class MamGcMessageReceivedEvent(NetworkIncomingEvent):
name = 'mam-message-received'
class MamDecryptedMessageReceived(NetworkIncomingEvent):
name = 'mam-decrypted-message-received'
class MAMPreferenceError(NetworkIncomingEvent):
name = 'mam-prefs-error'
class MAMPreferenceReceived(NetworkIncomingEvent):
name = 'mam-prefs-received'
class MAMPreferenceSaved(NetworkIncomingEvent):
name = 'mam-prefs-saved'
class ArchivingCountReceived(NetworkIncomingEvent):
name = 'archiving-count-received'
class ArchivingIntervalFinished(NetworkIncomingEvent):
name = 'archiving-interval-finished'
class ArchivingErrorReceived(NetworkIncomingEvent):
name = 'archiving-error-received'
class InvalidMamIQ(Exception):
pass
def get_instance(*args, **kwargs):
return MAM(*args, **kwargs), 'MAM'

View File

@ -0,0 +1,113 @@
# 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)

View File

@ -1,143 +0,0 @@
<?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>
</row>
<row>
<col id="0" translatable="yes">Never</col>
</row>
</data>
</object>
<object class="GtkDialog" id="item_dialog">
<property name="can_focus">False</property>
<property name="border_width">12</property>
<property name="resizable">False</property>
<property name="destroy_with_parent">True</property>
<property name="type_hint">dialog</property>
<signal name="destroy" handler="on_item_archiving_preferences_window_destroy" swapped="no"/>
<child internal-child="vbox">
<object class="GtkBox" id="dialog-vbox">
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<property name="spacing">20</property>
<child internal-child="action_area">
<object class="GtkButtonBox" id="dialog-action_area">
<property name="can_focus">False</property>
<property name="layout_style">end</property>
<child>
<object class="GtkButton" id="cancel_button">
<property name="label">gtk-close</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="use_stock">True</property>
<property name="always_show_image">True</property>
<signal name="clicked" handler="on_cancel_button_clicked" swapped="no"/>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkButton" id="ok_button">
<property name="label">gtk-ok</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="use_stock">True</property>
<property name="always_show_image">True</property>
<signal name="clicked" handler="on_ok_button_clicked" swapped="no"/>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkGrid" id="dialog_grid">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="row_spacing">5</property>
<property name="column_spacing">5</property>
<child>
<object class="GtkLabel" id="jid_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Jabber ID:</property>
<property name="xalign">0</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">0</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="pref_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Preference:</property>
<property name="xalign">0</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">1</property>
</packing>
</child>
<child>
<object class="GtkEntry" id="jid_entry">
<property name="width_request">194</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">0</property>
</packing>
</child>
<child>
<object class="GtkComboBox" id="pref_cb">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">start</property>
<property name="model">dialog_pref_liststore</property>
<child>
<object class="GtkCellRendererText" id="cellrenderertext2"/>
<attributes>
<attribute name="text">0</attribute>
</attributes>
</child>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>
</child>
</object>
</child>
</object>
</interface>

View File

@ -1,214 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.20.0 -->
<interface>
<requires lib="gtk+" version="3.12"/>
<object class="GtkImage" id="add_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="stock">gtk-add</property>
</object>
<object class="GtkListStore" id="archive_items_liststore">
<columns>
<!-- column-name jid -->
<column type="gchararray"/>
<!-- column-name archive_pref -->
<column type="gchararray"/>
</columns>
</object>
<object class="GtkListStore" id="default_pref_liststore">
<columns>
<!-- column-name gchararray1 -->
<column type="gchararray"/>
</columns>
<data>
<row>
<col id="0" translatable="yes">Always</col>
</row>
<row>
<col id="0" translatable="yes">Roster</col>
</row>
<row>
<col id="0" translatable="yes">Never</col>
</row>
</data>
</object>
<object class="GtkImage" id="remove_image">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="stock">gtk-remove</property>
</object>
<object class="GtkWindow" id="archiving_313_pref">
<property name="can_focus">False</property>
<property name="border_width">12</property>
<property name="window_position">center</property>
<property name="default_width">450</property>
<signal name="destroy" handler="on_archiving_preferences_window_destroy" swapped="no"/>
<signal name="key-press-event" handler="on_key_press_event" swapped="no"/>
<child>
<object class="GtkGrid" id="pref_grid">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="row_spacing">5</property>
<property name="column_spacing">10</property>
<child>
<object class="GtkScrolledWindow" id="scrolledwindow1">
<property name="height_request">150</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="hexpand">True</property>
<property name="vexpand">True</property>
<property name="shadow_type">in</property>
<child>
<object class="GtkTreeView" id="archive_view">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="model">archive_items_liststore</property>
<child internal-child="selection">
<object class="GtkTreeSelection" id="treeview-selection2"/>
</child>
<child>
<object class="GtkTreeViewColumn" id="treeviewcolumn1">
<property name="title" translatable="yes">Jabber ID</property>
<property name="clickable">True</property>
<property name="sort_indicator">True</property>
<property name="sort_column_id">0</property>
<child>
<object class="GtkCellRendererText" id="cellrenderertext3"/>
<attributes>
<attribute name="text">0</attribute>
</attributes>
</child>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="treeviewcolumn2">
<property name="title" translatable="yes">Preference</property>
<property name="clickable">True</property>
<property name="sort_indicator">True</property>
<property name="sort_column_id">1</property>
<child>
<object class="GtkCellRendererText" id="cellrenderertext4"/>
<attributes>
<attribute name="text">1</attribute>
</attributes>
</child>
</object>
</child>
</object>
</child>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">1</property>
<property name="width">2</property>
</packing>
</child>
<child>
<object class="GtkButtonBox" id="buttonbox1">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">start</property>
<property name="spacing">5</property>
<property name="layout_style">start</property>
<child>
<object class="GtkButton" id="add_button">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="image">add_image</property>
<signal name="clicked" handler="on_add_item_button_clicked" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
<property name="non_homogeneous">True</property>
</packing>
</child>
<child>
<object class="GtkButton" id="remove_button">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="image">remove_image</property>
<signal name="clicked" handler="on_remove_item_button_clicked" swapped="no"/>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
<property name="non_homogeneous">True</property>
</packing>
</child>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">2</property>
</packing>
</child>
<child>
<object class="GtkGrid" id="grid1">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">start</property>
<property name="column_spacing">5</property>
<child>
<object class="GtkLabel" id="default_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Default:</property>
<property name="xalign">0</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">0</property>
</packing>
</child>
<child>
<object class="GtkComboBox" id="default_cb">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">start</property>
<property name="hexpand">False</property>
<property name="model">default_pref_liststore</property>
<child>
<object class="GtkCellRendererText" id="cellrenderertext1"/>
<attributes>
<attribute name="text">0</attribute>
</attributes>
</child>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">0</property>
</packing>
</child>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">0</property>
</packing>
</child>
<child>
<object class="GtkButton" id="save_button">
<property name="label">gtk-save</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="halign">end</property>
<property name="use_stock">True</property>
<property name="always_show_image">True</property>
<signal name="clicked" handler="on_save_button_clicked" swapped="no"/>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">2</property>
</packing>
</child>
<child>
<placeholder/>
</child>
</object>
</child>
</object>
</interface>

View File

@ -0,0 +1,236 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.22.1 -->
<interface>
<requires lib="gtk+" version="3.12"/>
<object class="GtkListStore" id="default_store">
<columns>
<!-- column-name text -->
<column type="gchararray"/>
<!-- column-name value -->
<column type="gchararray"/>
</columns>
<data>
<row>
<col id="0" translatable="yes">Always</col>
<col id="1">always</col>
</row>
<row>
<col id="0" translatable="yes">Roster</col>
<col id="1">roster</col>
</row>
<row>
<col id="0" translatable="yes">Never</col>
<col id="1">never</col>
</row>
</data>
</object>
<object class="GtkListStore" id="preferences_store">
<columns>
<!-- column-name jid -->
<column type="gchararray"/>
<!-- column-name gboolean1 -->
<column type="gboolean"/>
</columns>
</object>
<object class="GtkGrid" id="preferences_grid">
<property name="width_request">400</property>
<property name="height_request">300</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="margin_left">18</property>
<property name="margin_right">18</property>
<property name="margin_top">18</property>
<property name="margin_bottom">18</property>
<property name="row_spacing">5</property>
<property name="column_spacing">10</property>
<child>
<object class="GtkButtonBox">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">start</property>
<property name="spacing">5</property>
<property name="layout_style">start</property>
<child>
<object class="GtkButton" id="add_button">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<signal name="clicked" handler="_on_add" swapped="no"/>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">list-add-symbolic</property>
</object>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
<property name="non_homogeneous">True</property>
</packing>
</child>
<child>
<object class="GtkButton" id="remove_button">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<signal name="clicked" handler="_on_remove" swapped="no"/>
<child>
<object class="GtkImage">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="icon_name">list-remove-symbolic</property>
</object>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
<property name="non_homogeneous">True</property>
</packing>
</child>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">2</property>
</packing>
</child>
<child>
<object class="GtkGrid">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">start</property>
<property name="column_spacing">5</property>
<child>
<object class="GtkLabel" id="default_label">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="label" translatable="yes">Default:</property>
<property name="xalign">0</property>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">0</property>
</packing>
</child>
<child>
<object class="GtkComboBox" id="default_cb">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="halign">start</property>
<property name="hexpand">False</property>
<property name="model">default_store</property>
<property name="active">0</property>
<property name="id_column">1</property>
<child>
<object class="GtkCellRendererText" id="cellrenderertext1"/>
<attributes>
<attribute name="text">0</attribute>
</attributes>
</child>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">0</property>
</packing>
</child>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">0</property>
</packing>
</child>
<child>
<object class="GtkButton" id="save_button">
<property name="label">Save</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="halign">end</property>
<property name="always_show_image">True</property>
<signal name="clicked" handler="_on_save" swapped="no"/>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">2</property>
</packing>
</child>
<child>
<object class="GtkOverlay" id="overlay">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="GtkScrolledWindow">
<property name="height_request">150</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="hexpand">True</property>
<property name="vexpand">True</property>
<property name="shadow_type">in</property>
<child>
<object class="GtkTreeView" id="pref_view">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="model">preferences_store</property>
<property name="search_column">0</property>
<child internal-child="selection">
<object class="GtkTreeSelection" id="treeview-selection2"/>
</child>
<child>
<object class="GtkTreeViewColumn" id="treeviewcolumn1">
<property name="title" translatable="yes">Jabber ID</property>
<property name="expand">True</property>
<property name="clickable">True</property>
<property name="sort_indicator">True</property>
<property name="sort_column_id">0</property>
<child>
<object class="GtkCellRendererText" id="cellrenderertext3">
<property name="editable">True</property>
<property name="placeholder_text">user@example.org</property>
<signal name="edited" handler="_jid_edited" swapped="no"/>
</object>
<attributes>
<attribute name="text">0</attribute>
</attributes>
</child>
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="treeviewcolumn2">
<property name="title" translatable="yes">Archive</property>
<property name="clickable">True</property>
<property name="alignment">0.5</property>
<property name="sort_indicator">True</property>
<property name="sort_column_id">1</property>
<child>
<object class="GtkCellRendererToggle">
<signal name="toggled" handler="_pref_toggled" swapped="no"/>
</object>
<attributes>
<attribute name="active">1</attribute>
</attributes>
</child>
</object>
</child>
</object>
</child>
</object>
<packing>
<property name="index">-1</property>
</packing>
</child>
</object>
<packing>
<property name="left_attach">0</property>
<property name="top_attach">1</property>
<property name="width">2</property>
</packing>
</child>
<child>
<placeholder/>
</child>
</object>
</interface>

View File

@ -3871,170 +3871,6 @@ class RosterItemExchangeWindow:
self.window.destroy()
class Archiving313PreferencesWindow:
default_dict = {'always': 0, 'roster': 1, 'never': 2}
default_dict_cb = {0: 'always', 1: 'roster', 2: 'never'}
def __init__(self, account):
self.account = account
self.idle_id = None
# Connect to glade
self.xml = gtkgui_helpers.get_gtk_builder(
'archiving_313_preferences_window.ui')
self.window = self.xml.get_object('archiving_313_pref')
# Add Widgets
for widget in ('archive_items_liststore', 'default_cb'):
setattr(self, widget, self.xml.get_object(widget))
self.window.set_title(_('Archiving Preferences for %s') % self.account)
app.ged.register_event_handler(
'archiving-313-preferences-changed-received', ged.GUI1,
self._nec_archiving_313_changed_received)
app.ged.register_event_handler(
'archiving-error-received', ged.GUI1, self._nec_archiving_error)
self.default_cb.set_active(0)
self.set_widget_state(False)
self.window.show_all()
self.xml.connect_signals(self)
self.idle_id = GLib.timeout_add_seconds(3, self._nec_archiving_error)
app.connections[self.account].request_archive_preferences()
def on_key_press_event(self, widget, event):
if event.keyval == Gdk.KEY_Escape:
self.window.destroy()
def set_widget_state(self, state):
for widget in ('default_cb', 'save_button', 'add_button',
'remove_button'):
self.xml.get_object(widget).set_sensitive(state)
def _nec_archiving_313_changed_received(self, obj):
if obj.conn.name != self.account:
return
try:
GLib.source_remove(self.idle_id)
except Exception as e:
log.debug(e)
self.set_widget_state(True)
if obj.answer:
def on_ok(dialog):
self.window.destroy()
dialog = HigDialog(
self.window, Gtk.MessageType.INFO, Gtk.ButtonsType.OK,
_('Success!'), _('Your Archiving Preferences have been saved!'),
on_response_ok=on_ok, on_response_cancel=on_ok)
dialog.popup()
self.default_cb.set_active(self.default_dict[obj.default])
self.archive_items_liststore.clear()
for items in obj.items:
self.archive_items_liststore.append(items)
def _nec_archiving_error(self, obj=None):
if obj and obj.conn.name != self.account:
return
try:
GLib.source_remove(self.idle_id)
except Exception as e:
log.debug(e)
if not obj:
msg = _('No response from the Server')
else:
msg = _('Error received: {}').format(self.error_msg)
dialog = HigDialog(
self.window, Gtk.MessageType.INFO, Gtk.ButtonsType.OK,
_('Error!'), msg)
dialog.popup()
self.set_widget_state(True)
return
def on_add_item_button_clicked(self, widget):
key_name = 'item_archiving_preferences'
if key_name in app.interface.instances[self.account]:
app.interface.instances[self.account][key_name].window.present()
else:
app.interface.instances[self.account][key_name] = \
ItemArchiving313PreferencesWindow(
self.account, self, self.window)
def on_remove_item_button_clicked(self, widget):
archive_view = self.xml.get_object('archive_view')
mod, path = archive_view.get_selection().get_selected_rows()
if path:
iter_ = mod.get_iter(path)
self.archive_items_liststore.remove(iter_)
def on_save_button_clicked(self, widget):
self.set_widget_state(False)
items = []
default = self.default_dict_cb[self.default_cb.get_active()]
for item in self.archive_items_liststore:
items.append((item[0].lower(), item[1].lower()))
self.idle_id = GLib.timeout_add_seconds(3, self._nec_archiving_error)
app.connections[self.account]. \
set_archive_preferences(items, default)
def on_close_button_clicked(self, widget):
self.window.destroy()
def on_archiving_preferences_window_destroy(self, widget):
app.ged.remove_event_handler(
'archiving-313-preferences-changed-received', ged.GUI1,
self._nec_archiving_313_changed_received)
app.ged.remove_event_handler(
'archiving-error-received', ged.GUI1, self._nec_archiving_error)
if 'archiving_preferences' in app.interface.instances[self.account]:
del app.interface.instances[self.account]['archiving_preferences']
class ItemArchiving313PreferencesWindow:
def __init__(self, account, archive, transient):
self.account = account
self.archive = archive
self.xml = gtkgui_helpers.get_gtk_builder(
'archiving_313_preferences_item.ui')
self.window = self.xml.get_object('item_dialog')
self.window.set_transient_for(transient)
# Add Widgets
for widget in ('jid_entry', 'pref_cb'):
setattr(self, widget, self.xml.get_object(widget))
self.window.set_title(_('Add JID'))
self.pref_cb.set_active(0)
self.window.show_all()
self.xml.connect_signals(self)
def on_ok_button_clicked(self, widget):
if self.pref_cb.get_active() == 0:
pref = 'Always'
else:
pref = 'Never'
text = self.jid_entry.get_text()
if not text:
self.window.destroy()
return
else:
self.archive.archive_items_liststore.append((text, pref))
self.window.destroy()
def on_cancel_button_clicked(self, widget):
self.window.destroy()
def on_item_archiving_preferences_window_destroy(self, widget):
key_name = 'item_archiving_preferences'
if key_name in app.interface.instances[self.account]:
del app.interface.instances[self.account][key_name]
class PrivacyListWindow:
"""
Window that is used for creating NEW or EDITING already there privacy lists

View File

@ -1168,9 +1168,11 @@ class GroupchatControl(ChatControlBase):
self._update_banner_state_image()
def _nec_mam_decrypted_message_received(self, obj):
if obj.conn.name != self.account:
return
if not obj.groupchat:
return
if obj.room_jid != self.room_jid:
if obj.archive_jid != self.room_jid:
return
self.print_conversation(
obj.msgtxt, contact=obj.nick,
@ -1588,7 +1590,8 @@ class GroupchatControl(ChatControlBase):
if muc_caps_cache.has_mam(self.room_jid):
# Request MAM
app.connections[self.account].request_archive_on_muc_join(
con = app.connections[self.account]
con.get_module('MAM').request_archive_on_muc_join(
self.room_jid)
app.gc_connected[self.account][self.room_jid] = True
@ -2256,6 +2259,8 @@ class GroupchatControl(ChatControlBase):
self._nec_signed_in)
app.ged.remove_event_handler('decrypted-message-received', ged.GUI2,
self._nec_decrypted_message_received)
app.ged.remove_event_handler('mam-decrypted-message-received',
ged.GUI1, self._nec_mam_decrypted_message_received)
app.ged.remove_event_handler('gc-stanza-message-outgoing', ged.OUT_POSTCORE,
self._message_sent)

0
gajim/gtk/__init__.py Normal file
View File

View File

@ -0,0 +1,158 @@
# 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/>.
import logging
from gi.repository import Gtk
from gi.repository import Gdk
from gajim.common import app
from gajim.common import ged
from gajim.gtk.util import get_builder
from gajim.dialogs import HigDialog
log = logging.getLogger('gajim.gtk.mam_preferences')
class MamPreferences(Gtk.ApplicationWindow):
def __init__(self, account):
Gtk.ApplicationWindow.__init__(self)
self.set_application(app.app)
self.set_position(Gtk.WindowPosition.CENTER)
self.set_show_menubar(False)
self.set_title(_('Archiving Preferences for %s') % account)
self.connect('destroy', self._on_destroy)
self.connect('key-press-event', self._on_key_press)
self.account = account
self._con = app.connections[account]
self._builder = get_builder('mam_preferences.ui')
self.add(self._builder.get_object('preferences_grid'))
self._default = self._builder.get_object('default_cb')
self._pref_store = self._builder.get_object('preferences_store')
self._overlay = self._builder.get_object('overlay')
self._spinner = Gtk.Spinner()
self._overlay.add_overlay(self._spinner)
app.ged.register_event_handler('mam-prefs-received', ged.GUI1,
self._mam_prefs_received)
app.ged.register_event_handler('mam-prefs-saved', ged.GUI1,
self._mam_prefs_saved)
app.ged.register_event_handler('mam-prefs-error', ged.GUI1,
self._mam_prefs_error)
self._set_grid_state(False)
self._builder.connect_signals(self)
self.show_all()
self._activate_spinner()
self._con.get_module('MAM').request_mam_preferences()
def _mam_prefs_received(self, obj):
if obj.conn.name != self.account:
return
self._disable_spinner()
self._set_grid_state(True)
self._default.set_active_id(obj.default)
self._pref_store.clear()
for item in obj.rules:
self._pref_store.append(item)
def _mam_prefs_saved(self, obj):
if obj.conn.name != self.account:
return
self._disable_spinner()
def on_ok(dialog):
self.destroy()
dialog = HigDialog(
self, Gtk.MessageType.INFO, Gtk.ButtonsType.OK,
_('Success!'), _('Your Archiving Preferences have been saved!'),
on_response_ok=on_ok, on_response_cancel=on_ok)
dialog.popup()
def _mam_prefs_error(self, obj=None):
if obj and obj.conn.name != self.account:
return
self._disable_spinner()
if not obj:
msg = _('No response from the Server')
else:
msg = _('Error received: {}').format(obj.error_msg)
dialog = HigDialog(
self, Gtk.MessageType.INFO, Gtk.ButtonsType.OK,
_('Error!'), msg)
dialog.popup()
self._set_grid_state(True)
def _set_grid_state(self, state):
self._builder.get_object('preferences_grid').set_sensitive(state)
def _jid_edited(self, renderer, path, new_text):
iter_ = self._pref_store.get_iter(path)
self._pref_store.set_value(iter_, 0, new_text)
def _pref_toggled(self, renderer, path):
iter_ = self._pref_store.get_iter(path)
current_value = self._pref_store[iter_][1]
self._pref_store.set_value(iter_, 1, not current_value)
def _on_add(self, button):
self._pref_store.append(['', False])
def _on_remove(self, button):
pref_view = self._builder.get_object('pref_view')
mod, paths = pref_view.get_selection().get_selected_rows()
for path in paths:
iter_ = mod.get_iter(path)
self._pref_store.remove(iter_)
def _on_save(self, button):
self._activate_spinner()
self._set_grid_state(False)
items = []
default = self._default.get_active_id()
for item in self._pref_store:
items.append((item[0].lower(), item[1]))
self._con.get_module('MAM').set_mam_preferences(items, default)
def _activate_spinner(self):
self._spinner.show()
self._spinner.start()
def _disable_spinner(self):
self._spinner.hide()
self._spinner.stop()
def _on_key_press(self, widget, event):
if event.keyval == Gdk.KEY_Escape:
self.destroy()
def _on_destroy(self, widget):
app.ged.remove_event_handler('mam-prefs-received', ged.GUI1,
self._mam_prefs_received)
app.ged.remove_event_handler('mam-prefs-saved', ged.GUI1,
self._mam_prefs_saved)
app.ged.remove_event_handler('mam-prefs-error', ged.GUI1,
self._mam_prefs_error)

54
gajim/gtk/util.py Normal file
View File

@ -0,0 +1,54 @@
# 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/>.
import os
import sys
from gi.repository import Gtk
import xml.etree.ElementTree as ET
from gajim.common import i18n
from gajim.common import configpaths
def get_builder(file_name, widget=None):
file_path = os.path.join(configpaths.get('GUI'), file_name)
builder = _translate(file_path, widget)
builder.set_translation_domain(i18n.DOMAIN)
return builder
def _translate(gui_file, widget):
"""
This is a workaround for non working translation on Windows
"""
if sys.platform == "win32":
tree = ET.parse(gui_file)
for node in tree.iter():
if 'translatable' in node.attrib:
node.text = _(node.text)
xml_text = ET.tostring(tree.getroot(),
encoding='unicode',
method='xml')
if widget is not None:
builder = Gtk.Builder()
builder.add_objects_from_string(xml_text, [widget])
return builder
return Gtk.Builder.new_from_string(xml_text, -1)
else:
if widget is not None:
builder = Gtk.Builder()
builder.add_objects_from_file(gui_file, [widget])
return builder
return Gtk.Builder.new_from_file(gui_file)

View File

@ -1113,9 +1113,9 @@ class Interface:
# Else disable autoaway
app.sleeper_state[account] = 'off'
if obj.conn.archiving_313_supported and app.config.get_per('accounts',
if obj.conn.get_module('MAM').available and app.config.get_per('accounts',
account, 'sync_logs_with_server'):
obj.conn.request_archive_on_signin()
obj.conn.get_module('MAM').request_archive_on_signin()
invisible_show = app.SHOW_LIST.index('invisible')
# We cannot join rooms if we are invisible

View File

@ -54,7 +54,6 @@ class HistorySyncAssistant(Gtk.Assistant):
self.end = None
self.next = None
self.hide_buttons()
self.event_id = id(self)
own_jid = self.con.get_own_jid().getStripped()
@ -88,9 +87,6 @@ class HistorySyncAssistant(Gtk.Assistant):
app.ged.register_event_handler('archiving-count-received',
ged.GUI1,
self._received_count)
app.ged.register_event_handler('archiving-query-id',
ged.GUI1,
self._new_query_id)
app.ged.register_event_handler('archiving-interval-finished',
ged.GUI1,
self._received_finished)
@ -145,28 +141,27 @@ class HistorySyncAssistant(Gtk.Assistant):
log.info('start: %s', self.start)
log.info('end: %s', self.end)
self.con.request_archive_count(self.event_id, self.start, self.end)
self.query_id = self.con.get_module('MAM').request_archive_count(
self.start, self.end)
def _received_count(self, event):
if event.event_id != self.event_id:
if event.query_id != self.query_id:
return
if event.count is not None:
self.download_history.count = int(event.count)
self.con.request_archive_interval(self.event_id, self.start, self.end)
self.query_id = self.con.get_module('MAM').request_archive_interval(
self.start, self.end)
def _received_finished(self, event):
if event.event_id != self.event_id:
if event.query_id != self.query_id:
return
self.query_id = None
log.info('query finished')
GLib.idle_add(self.download_history.finished)
self.set_current_page(Pages.SUMMARY)
self.summary.finished()
def _new_query_id(self, event):
if event.event_id != self.event_id:
return
self.query_id = event.query_id
def _nec_mam_message_received(self, obj):
if obj.conn.name != self.account:
return
@ -193,9 +188,6 @@ class HistorySyncAssistant(Gtk.Assistant):
app.ged.remove_event_handler('archiving-count-received',
ged.GUI1,
self._received_count)
app.ged.remove_event_handler('archiving-query-id',
ged.GUI1,
self._new_query_id)
app.ged.remove_event_handler('archiving-interval-finished',
ged.GUI1,
self._received_finished)

View File

@ -5404,7 +5404,7 @@ class RosterWindow:
self.on_privacy_lists_menuitem_activate, account)
else:
privacy_lists_menuitem.set_sensitive(False)
if app.connections[account].archiving_313_supported:
if app.connections[account].get_module('MAM').available:
archiving_preferences_menuitem.connect(
'activate',
self.on_archiving_preferences_menuitem_activate, account)

View File

@ -174,7 +174,8 @@ class ServerInfoDialog(Gtk.Dialog):
Feature('XEP-0280: Message Carbons',
con.carbons_available, nbxmpp.NS_CARBONS, carbons_enabled),
Feature('XEP-0313: Message Archive Management',
con.archiving_namespace, con.archiving_namespace,
con.get_module('MAM').archiving_namespace,
con.get_module('MAM').archiving_namespace,
mam_enabled),
Feature('XEP-0363: HTTP File Upload',
con.get_module('HTTPUpload').available,