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:
parent
72ee9af79c
commit
ebbe06d587
|
@ -29,6 +29,7 @@ from gajim import history_window
|
||||||
from gajim import disco
|
from gajim import disco
|
||||||
from gajim.history_sync import HistorySyncAssistant
|
from gajim.history_sync import HistorySyncAssistant
|
||||||
from gajim.server_info import ServerInfoDialog
|
from gajim.server_info import ServerInfoDialog
|
||||||
|
from gajim.gtk.mam_preferences import MamPreferences
|
||||||
|
|
||||||
|
|
||||||
# General Actions
|
# General Actions
|
||||||
|
@ -181,14 +182,13 @@ def on_import_contacts(action, param):
|
||||||
# Advanced Actions
|
# Advanced Actions
|
||||||
|
|
||||||
|
|
||||||
def on_archiving_preferences(action, param):
|
def on_mam_preferences(action, param):
|
||||||
account = param.get_string()
|
account = param.get_string()
|
||||||
if 'archiving_preferences' in interface.instances[account]:
|
window = app.get_app_window(MamPreferences, account)
|
||||||
interface.instances[account]['archiving_preferences'].window.\
|
if window is None:
|
||||||
present()
|
MamPreferences(account)
|
||||||
else:
|
else:
|
||||||
interface.instances[account]['archiving_preferences'] = \
|
window.present()
|
||||||
dialogs.Archiving313PreferencesWindow(account)
|
|
||||||
|
|
||||||
|
|
||||||
def on_history_sync(action, param):
|
def on_history_sync(action, param):
|
||||||
|
|
|
@ -356,7 +356,7 @@ class GajimApplication(Gtk.Application):
|
||||||
('-profile', app_actions.on_profile, 'feature', 's'),
|
('-profile', app_actions.on_profile, 'feature', 's'),
|
||||||
('-xml-console', app_actions.on_xml_console, 'always', 's'),
|
('-xml-console', app_actions.on_xml_console, 'always', 's'),
|
||||||
('-server-info', app_actions.on_server_info, 'online', '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'),
|
('-sync-history', app_actions.on_history_sync, 'online', 's'),
|
||||||
('-privacylists', app_actions.on_privacy_lists, 'feature', 's'),
|
('-privacylists', app_actions.on_privacy_lists, 'feature', 's'),
|
||||||
('-send-server-message',
|
('-send-server-message',
|
||||||
|
|
|
@ -809,7 +809,12 @@ class ChatControl(ChatControlBase):
|
||||||
def _nec_mam_decrypted_message_received(self, obj):
|
def _nec_mam_decrypted_message_received(self, obj):
|
||||||
if obj.conn.name != self.account:
|
if obj.conn.name != self.account:
|
||||||
return
|
return
|
||||||
if obj.with_ != self.contact.jid:
|
|
||||||
|
if obj.muc_pm:
|
||||||
|
if not obj.with_ == self.contact.get_full_jid():
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
if not obj.with_.bareMatch(self.contact.jid):
|
||||||
return
|
return
|
||||||
|
|
||||||
kind = '' # incoming
|
kind = '' # incoming
|
||||||
|
|
|
@ -595,11 +595,17 @@ def prefers_app_menu():
|
||||||
return False
|
return False
|
||||||
return app.prefers_app_menu()
|
return app.prefers_app_menu()
|
||||||
|
|
||||||
def get_app_window(cls):
|
def get_app_window(cls, account=None):
|
||||||
for win in app.get_windows():
|
for win in app.get_windows():
|
||||||
if isinstance(cls, str):
|
if isinstance(cls, str):
|
||||||
if type(win).__name__ == cls:
|
if type(win).__name__ == cls:
|
||||||
|
if account is not None:
|
||||||
|
if account != win.account:
|
||||||
|
continue
|
||||||
return win
|
return win
|
||||||
elif isinstance(win, cls):
|
elif isinstance(win, cls):
|
||||||
|
if account is not None:
|
||||||
|
if account != win.account:
|
||||||
|
continue
|
||||||
return win
|
return win
|
||||||
return None
|
return None
|
||||||
|
|
|
@ -305,7 +305,6 @@ class Config:
|
||||||
'use_keyring': [opt_bool, True, _('If true, Gajim will use the Systems Keyring to store account passwords.')],
|
'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],
|
'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.')],
|
'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 = {
|
__options_per_key = {
|
||||||
|
|
|
@ -121,9 +121,6 @@ class CommonConnection:
|
||||||
self.privacy_rules_supported = False
|
self.privacy_rules_supported = False
|
||||||
self.vcard_supported = False
|
self.vcard_supported = False
|
||||||
self.private_storage_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.roster_supported = True
|
||||||
self.blocking_supported = False
|
self.blocking_supported = False
|
||||||
self.addressing_supported = False
|
self.addressing_supported = False
|
||||||
|
@ -1611,12 +1608,11 @@ class Connection(CommonConnection, ConnectionHandlers):
|
||||||
|
|
||||||
if obj.fjid == our_jid:
|
if obj.fjid == our_jid:
|
||||||
if nbxmpp.NS_MAM_2 in obj.features:
|
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:
|
elif nbxmpp.NS_MAM_1 in obj.features:
|
||||||
self.archiving_namespace = nbxmpp.NS_MAM_1
|
self.get_module('MAM').archiving_namespace = nbxmpp.NS_MAM_1
|
||||||
if self.archiving_namespace:
|
if self.get_module('MAM').archiving_namespace:
|
||||||
self.archiving_supported = True
|
self.get_module('MAM').available = True
|
||||||
self.archiving_313_supported = True
|
|
||||||
get_action(self.name + '-archive').set_enabled(True)
|
get_action(self.name + '-archive').set_enabled(True)
|
||||||
for identity in obj.identities:
|
for identity in obj.identities:
|
||||||
if identity['category'] == 'pubsub':
|
if identity['category'] == 'pubsub':
|
||||||
|
|
|
@ -45,8 +45,8 @@ from gajim.common.caps_cache import muc_caps_cache
|
||||||
from gajim.common.protocol.caps import ConnectionCaps
|
from gajim.common.protocol.caps import ConnectionCaps
|
||||||
from gajim.common.protocol.bytestream import ConnectionSocks5Bytestream
|
from gajim.common.protocol.bytestream import ConnectionSocks5Bytestream
|
||||||
from gajim.common.protocol.bytestream import ConnectionIBBytestream
|
from gajim.common.protocol.bytestream import ConnectionIBBytestream
|
||||||
from gajim.common.message_archiving import ConnectionArchive313
|
|
||||||
from gajim.common.connection_handlers_events import *
|
from gajim.common.connection_handlers_events import *
|
||||||
|
from gajim.common.modules.misc import parse_eme
|
||||||
|
|
||||||
from gajim.common import ged
|
from gajim.common import ged
|
||||||
from gajim.common.nec import NetworkEvent
|
from gajim.common.nec import NetworkEvent
|
||||||
|
@ -295,7 +295,9 @@ class ConnectionHandlersBase:
|
||||||
# XEPs that are based on Message
|
# XEPs that are based on Message
|
||||||
self._message_namespaces = set([nbxmpp.NS_HTTP_AUTH,
|
self._message_namespaces = set([nbxmpp.NS_HTTP_AUTH,
|
||||||
nbxmpp.NS_PUBSUB_EVENT,
|
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,
|
app.ged.register_event_handler('iq-error-received', ged.CORE,
|
||||||
self._nec_iq_error_received)
|
self._nec_iq_error_received)
|
||||||
|
@ -303,10 +305,6 @@ class ConnectionHandlersBase:
|
||||||
self._nec_presence_received)
|
self._nec_presence_received)
|
||||||
app.ged.register_event_handler('message-received', ged.CORE,
|
app.ged.register_event_handler('message-received', ged.CORE,
|
||||||
self._nec_message_received)
|
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,
|
app.ged.register_event_handler('decrypted-message-received', ged.CORE,
|
||||||
self._nec_decrypted_message_received)
|
self._nec_decrypted_message_received)
|
||||||
app.ged.register_event_handler('gc-message-received', ged.CORE,
|
app.ged.register_event_handler('gc-message-received', ged.CORE,
|
||||||
|
@ -319,10 +317,6 @@ class ConnectionHandlersBase:
|
||||||
self._nec_presence_received)
|
self._nec_presence_received)
|
||||||
app.ged.remove_event_handler('message-received', ged.CORE,
|
app.ged.remove_event_handler('message-received', ged.CORE,
|
||||||
self._nec_message_received)
|
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,
|
app.ged.remove_event_handler('decrypted-message-received', ged.CORE,
|
||||||
self._nec_decrypted_message_received)
|
self._nec_decrypted_message_received)
|
||||||
app.ged.remove_event_handler('gc-message-received', ged.CORE,
|
app.ged.remove_event_handler('gc-message-received', ged.CORE,
|
||||||
|
@ -460,37 +454,15 @@ class ConnectionHandlersBase:
|
||||||
app.plugin_manager.extension_point(
|
app.plugin_manager.extension_point(
|
||||||
'decrypt', self, obj, self._on_message_received)
|
'decrypt', self, obj, self._on_message_received)
|
||||||
if not obj.encrypted:
|
if not obj.encrypted:
|
||||||
# XEP-0380
|
eme = parse_eme(obj.stanza)
|
||||||
enc_tag = obj.stanza.getTag('encryption', namespace=nbxmpp.NS_EME)
|
if eme is not None:
|
||||||
if enc_tag:
|
obj.msgtxt = eme
|
||||||
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
|
|
||||||
self._on_message_received(obj)
|
self._on_message_received(obj)
|
||||||
|
|
||||||
def _on_message_received(self, obj):
|
def _on_message_received(self, obj):
|
||||||
if isinstance(obj, MessageReceivedEvent):
|
|
||||||
app.nec.push_incoming_event(
|
app.nec.push_incoming_event(
|
||||||
DecryptedMessageReceivedEvent(
|
DecryptedMessageReceivedEvent(
|
||||||
None, conn=self, msg_obj=obj, stanza_id=obj.unique_id))
|
None, conn=self, msg_obj=obj, stanza_id=obj.unique_id))
|
||||||
else:
|
|
||||||
app.nec.push_incoming_event(
|
|
||||||
MamDecryptedMessageReceivedEvent(None, **vars(obj)))
|
|
||||||
|
|
||||||
def _nec_decrypted_message_received(self, obj):
|
def _nec_decrypted_message_received(self, obj):
|
||||||
if obj.conn.name != self.name:
|
if obj.conn.name != self.name:
|
||||||
|
@ -564,7 +536,7 @@ class ConnectionHandlersBase:
|
||||||
def _check_for_mam_compliance(self, room_jid, stanza_id):
|
def _check_for_mam_compliance(self, room_jid, stanza_id):
|
||||||
namespace = muc_caps_cache.get_mam_namespace(room_jid)
|
namespace = muc_caps_cache.get_mam_namespace(room_jid)
|
||||||
if stanza_id is None and namespace == nbxmpp.NS_MAM_2:
|
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):
|
def _nec_gc_message_received(self, obj):
|
||||||
if obj.conn.name != self.name:
|
if obj.conn.name != self.name:
|
||||||
|
@ -743,11 +715,10 @@ class ConnectionHandlersBase:
|
||||||
|
|
||||||
return sess
|
return sess
|
||||||
|
|
||||||
class ConnectionHandlers(ConnectionArchive313,
|
class ConnectionHandlers(ConnectionSocks5Bytestream, ConnectionDisco,
|
||||||
ConnectionSocks5Bytestream, ConnectionDisco, ConnectionCaps,
|
ConnectionCaps, ConnectionHandlersBase,
|
||||||
ConnectionHandlersBase, ConnectionJingle, ConnectionIBBytestream):
|
ConnectionJingle, ConnectionIBBytestream):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
ConnectionArchive313.__init__(self)
|
|
||||||
ConnectionSocks5Bytestream.__init__(self)
|
ConnectionSocks5Bytestream.__init__(self)
|
||||||
ConnectionIBBytestream.__init__(self)
|
ConnectionIBBytestream.__init__(self)
|
||||||
|
|
||||||
|
@ -772,9 +743,6 @@ ConnectionHandlersBase, ConnectionJingle, ConnectionIBBytestream):
|
||||||
|
|
||||||
app.nec.register_incoming_event(StreamConflictReceivedEvent)
|
app.nec.register_incoming_event(StreamConflictReceivedEvent)
|
||||||
app.nec.register_incoming_event(MessageReceivedEvent)
|
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.nec.register_incoming_event(NotificationEvent)
|
||||||
|
|
||||||
app.ged.register_event_handler('roster-set-received',
|
app.ged.register_event_handler('roster-set-received',
|
||||||
|
@ -799,7 +767,6 @@ ConnectionHandlersBase, ConnectionJingle, ConnectionIBBytestream):
|
||||||
def cleanup(self):
|
def cleanup(self):
|
||||||
ConnectionHandlersBase.cleanup(self)
|
ConnectionHandlersBase.cleanup(self)
|
||||||
ConnectionCaps.cleanup(self)
|
ConnectionCaps.cleanup(self)
|
||||||
ConnectionArchive313.cleanup(self)
|
|
||||||
app.ged.remove_event_handler('roster-set-received',
|
app.ged.remove_event_handler('roster-set-received',
|
||||||
ged.CORE, self._nec_roster_set_received)
|
ged.CORE, self._nec_roster_set_received)
|
||||||
app.ged.remove_event_handler('roster-received', ged.CORE,
|
app.ged.remove_event_handler('roster-received', ged.CORE,
|
||||||
|
@ -1343,8 +1310,6 @@ ConnectionHandlersBase, ConnectionJingle, ConnectionIBBytestream):
|
||||||
con.RegisterHandler('iq', self._DiscoverItemsGetCB, 'get',
|
con.RegisterHandler('iq', self._DiscoverItemsGetCB, 'get',
|
||||||
nbxmpp.NS_DISCO_ITEMS)
|
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, 'result')
|
||||||
con.RegisterHandler('iq', self._JingleCB, 'error')
|
con.RegisterHandler('iq', self._JingleCB, 'error')
|
||||||
con.RegisterHandler('iq', self._JingleCB, 'set', nbxmpp.NS_JINGLE)
|
con.RegisterHandler('iq', self._JingleCB, 'set', nbxmpp.NS_JINGLE)
|
||||||
|
|
|
@ -77,6 +77,9 @@ class HelperEvent:
|
||||||
del self.conn.groupchat_jids[self.id_]
|
del self.conn.groupchat_jids[self.id_]
|
||||||
else:
|
else:
|
||||||
self.fjid = helpers.get_full_jid_from_iq(self.stanza)
|
self.fjid = helpers.get_full_jid_from_iq(self.stanza)
|
||||||
|
if self.fjid is None:
|
||||||
|
self.jid = None
|
||||||
|
else:
|
||||||
self.jid, self.resource = app.get_room_and_nick_from_fjid(self.fjid)
|
self.jid, self.resource = app.get_room_and_nick_from_fjid(self.fjid)
|
||||||
|
|
||||||
def get_id(self):
|
def get_id(self):
|
||||||
|
@ -630,240 +633,6 @@ class BeforeChangeShowEvent(nec.NetworkIncomingEvent):
|
||||||
name = 'before-change-show'
|
name = 'before-change-show'
|
||||||
base_network_events = []
|
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):
|
class MessageReceivedEvent(nec.NetworkIncomingEvent, HelperEvent):
|
||||||
name = 'message-received'
|
name = 'message-received'
|
||||||
base_network_events = ['raw-message-received']
|
base_network_events = ['raw-message-received']
|
||||||
|
@ -968,30 +737,6 @@ class MessageReceivedEvent(nec.NetworkIncomingEvent, HelperEvent):
|
||||||
return
|
return
|
||||||
self.forwarded = True
|
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?
|
# Mediated invitation?
|
||||||
muc_user = self.stanza.getTag('x', namespace=nbxmpp.NS_MUC_USER)
|
muc_user = self.stanza.getTag('x', namespace=nbxmpp.NS_MUC_USER)
|
||||||
if muc_user:
|
if muc_user:
|
||||||
|
@ -1085,7 +830,7 @@ class MessageReceivedEvent(nec.NetworkIncomingEvent, HelperEvent):
|
||||||
return
|
return
|
||||||
|
|
||||||
# Messages we receive live
|
# 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
|
# Only mam:2 ensures valid stanza-id
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@ -1498,77 +1243,6 @@ class JingleErrorReceivedEvent(nec.NetworkIncomingEvent):
|
||||||
self.sid = self.jingle_session.sid
|
self.sid = self.jingle_session.sid
|
||||||
return True
|
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):
|
class AccountCreatedEvent(nec.NetworkIncomingEvent):
|
||||||
name = 'account-created'
|
name = 'account-created'
|
||||||
base_network_events = []
|
base_network_events = []
|
||||||
|
|
|
@ -43,7 +43,7 @@ import shlex
|
||||||
from gajim.common import caps_cache
|
from gajim.common import caps_cache
|
||||||
import socket
|
import socket
|
||||||
import time
|
import time
|
||||||
from datetime import datetime, timedelta, timezone, tzinfo
|
from datetime import datetime, timedelta
|
||||||
from distutils.version import LooseVersion as V
|
from distutils.version import LooseVersion as V
|
||||||
|
|
||||||
from encodings.punycode import punycode_encode
|
from encodings.punycode import punycode_encode
|
||||||
|
@ -89,77 +89,6 @@ log = logging.getLogger('gajim.c.helpers')
|
||||||
|
|
||||||
special_groups = (_('Transports'), _('Not in Roster'), _('Observers'), _('Groupchats'))
|
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):
|
class InvalidFormat(Exception):
|
||||||
pass
|
pass
|
||||||
|
@ -673,56 +602,6 @@ def datetime_tuple(timestamp):
|
||||||
tim = tim.timetuple()
|
tim = tim.timetuple()
|
||||||
return tim
|
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
|
from gajim.common import app
|
||||||
if app.is_installed('PYCURL'):
|
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
|
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()))
|
return parse_jid(str(iq_obj.getFrom()))
|
||||||
|
|
||||||
def get_jid_from_iq(iq_obj):
|
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)
|
emoticons_user_path = os.path.join(configpaths.get('MY_EMOTS'), theme)
|
||||||
if os.path.exists(emoticons_user_path):
|
if os.path.exists(emoticons_user_path):
|
||||||
return emoticons_user_path
|
return emoticons_user_path
|
||||||
|
|
||||||
def add_to_mam_blacklist(jid):
|
|
||||||
config_value = app.config.get('mam_blacklist')
|
|
||||||
if not config_value:
|
|
||||||
config_value = [jid]
|
|
||||||
else:
|
|
||||||
if jid in config_value:
|
|
||||||
return
|
|
||||||
config_value = config_value.split(',')
|
|
||||||
config_value.append(jid)
|
|
||||||
log.warning('Found not-compliant MUC. %s added to MAM Blacklist', jid)
|
|
||||||
app.config.set('mam_blacklist', ','.join(config_value))
|
|
||||||
|
|
||||||
def get_mam_blacklist():
|
|
||||||
config_value = app.config.get('mam_blacklist')
|
|
||||||
if not config_value:
|
|
||||||
return []
|
|
||||||
return config_value.split(',')
|
|
||||||
|
|
|
@ -374,14 +374,10 @@ class Logger:
|
||||||
"""
|
"""
|
||||||
Return True if it's a room jid, False if it's not, None if we don't know
|
Return True if it's a room jid, False if it's not, None if we don't know
|
||||||
"""
|
"""
|
||||||
row = self._con.execute(
|
jid_ = self._jid_ids.get(jid)
|
||||||
'SELECT type FROM jids WHERE jid=?', (jid,)).fetchone()
|
if jid_ is None:
|
||||||
if row is None:
|
return
|
||||||
return None
|
return jid_.type == JIDConstant.ROOM_TYPE
|
||||||
else:
|
|
||||||
if row.type == JIDConstant.ROOM_TYPE:
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _get_family_jids(account, jid):
|
def _get_family_jids(account, jid):
|
||||||
|
|
|
@ -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
|
|
|
@ -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
|
|
@ -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'
|
|
@ -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)
|
|
@ -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>
|
|
|
@ -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>
|
|
|
@ -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>
|
164
gajim/dialogs.py
164
gajim/dialogs.py
|
@ -3871,170 +3871,6 @@ class RosterItemExchangeWindow:
|
||||||
self.window.destroy()
|
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:
|
class PrivacyListWindow:
|
||||||
"""
|
"""
|
||||||
Window that is used for creating NEW or EDITING already there privacy lists
|
Window that is used for creating NEW or EDITING already there privacy lists
|
||||||
|
|
|
@ -1168,9 +1168,11 @@ class GroupchatControl(ChatControlBase):
|
||||||
self._update_banner_state_image()
|
self._update_banner_state_image()
|
||||||
|
|
||||||
def _nec_mam_decrypted_message_received(self, obj):
|
def _nec_mam_decrypted_message_received(self, obj):
|
||||||
|
if obj.conn.name != self.account:
|
||||||
|
return
|
||||||
if not obj.groupchat:
|
if not obj.groupchat:
|
||||||
return
|
return
|
||||||
if obj.room_jid != self.room_jid:
|
if obj.archive_jid != self.room_jid:
|
||||||
return
|
return
|
||||||
self.print_conversation(
|
self.print_conversation(
|
||||||
obj.msgtxt, contact=obj.nick,
|
obj.msgtxt, contact=obj.nick,
|
||||||
|
@ -1588,7 +1590,8 @@ class GroupchatControl(ChatControlBase):
|
||||||
|
|
||||||
if muc_caps_cache.has_mam(self.room_jid):
|
if muc_caps_cache.has_mam(self.room_jid):
|
||||||
# Request MAM
|
# 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)
|
self.room_jid)
|
||||||
|
|
||||||
app.gc_connected[self.account][self.room_jid] = True
|
app.gc_connected[self.account][self.room_jid] = True
|
||||||
|
@ -2256,6 +2259,8 @@ class GroupchatControl(ChatControlBase):
|
||||||
self._nec_signed_in)
|
self._nec_signed_in)
|
||||||
app.ged.remove_event_handler('decrypted-message-received', ged.GUI2,
|
app.ged.remove_event_handler('decrypted-message-received', ged.GUI2,
|
||||||
self._nec_decrypted_message_received)
|
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,
|
app.ged.remove_event_handler('gc-stanza-message-outgoing', ged.OUT_POSTCORE,
|
||||||
self._message_sent)
|
self._message_sent)
|
||||||
|
|
||||||
|
|
|
@ -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)
|
|
@ -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)
|
|
@ -1113,9 +1113,9 @@ class Interface:
|
||||||
# Else disable autoaway
|
# Else disable autoaway
|
||||||
app.sleeper_state[account] = 'off'
|
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'):
|
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')
|
invisible_show = app.SHOW_LIST.index('invisible')
|
||||||
# We cannot join rooms if we are invisible
|
# We cannot join rooms if we are invisible
|
||||||
|
|
|
@ -54,7 +54,6 @@ class HistorySyncAssistant(Gtk.Assistant):
|
||||||
self.end = None
|
self.end = None
|
||||||
self.next = None
|
self.next = None
|
||||||
self.hide_buttons()
|
self.hide_buttons()
|
||||||
self.event_id = id(self)
|
|
||||||
|
|
||||||
own_jid = self.con.get_own_jid().getStripped()
|
own_jid = self.con.get_own_jid().getStripped()
|
||||||
|
|
||||||
|
@ -88,9 +87,6 @@ class HistorySyncAssistant(Gtk.Assistant):
|
||||||
app.ged.register_event_handler('archiving-count-received',
|
app.ged.register_event_handler('archiving-count-received',
|
||||||
ged.GUI1,
|
ged.GUI1,
|
||||||
self._received_count)
|
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',
|
app.ged.register_event_handler('archiving-interval-finished',
|
||||||
ged.GUI1,
|
ged.GUI1,
|
||||||
self._received_finished)
|
self._received_finished)
|
||||||
|
@ -145,28 +141,27 @@ class HistorySyncAssistant(Gtk.Assistant):
|
||||||
log.info('start: %s', self.start)
|
log.info('start: %s', self.start)
|
||||||
log.info('end: %s', self.end)
|
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):
|
def _received_count(self, event):
|
||||||
if event.event_id != self.event_id:
|
if event.query_id != self.query_id:
|
||||||
return
|
return
|
||||||
|
|
||||||
if event.count is not None:
|
if event.count is not None:
|
||||||
self.download_history.count = int(event.count)
|
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):
|
def _received_finished(self, event):
|
||||||
if event.event_id != self.event_id:
|
if event.query_id != self.query_id:
|
||||||
return
|
return
|
||||||
|
self.query_id = None
|
||||||
log.info('query finished')
|
log.info('query finished')
|
||||||
GLib.idle_add(self.download_history.finished)
|
GLib.idle_add(self.download_history.finished)
|
||||||
self.set_current_page(Pages.SUMMARY)
|
self.set_current_page(Pages.SUMMARY)
|
||||||
self.summary.finished()
|
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):
|
def _nec_mam_message_received(self, obj):
|
||||||
if obj.conn.name != self.account:
|
if obj.conn.name != self.account:
|
||||||
return
|
return
|
||||||
|
@ -193,9 +188,6 @@ class HistorySyncAssistant(Gtk.Assistant):
|
||||||
app.ged.remove_event_handler('archiving-count-received',
|
app.ged.remove_event_handler('archiving-count-received',
|
||||||
ged.GUI1,
|
ged.GUI1,
|
||||||
self._received_count)
|
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',
|
app.ged.remove_event_handler('archiving-interval-finished',
|
||||||
ged.GUI1,
|
ged.GUI1,
|
||||||
self._received_finished)
|
self._received_finished)
|
||||||
|
|
|
@ -5404,7 +5404,7 @@ class RosterWindow:
|
||||||
self.on_privacy_lists_menuitem_activate, account)
|
self.on_privacy_lists_menuitem_activate, account)
|
||||||
else:
|
else:
|
||||||
privacy_lists_menuitem.set_sensitive(False)
|
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(
|
archiving_preferences_menuitem.connect(
|
||||||
'activate',
|
'activate',
|
||||||
self.on_archiving_preferences_menuitem_activate, account)
|
self.on_archiving_preferences_menuitem_activate, account)
|
||||||
|
|
|
@ -174,7 +174,8 @@ class ServerInfoDialog(Gtk.Dialog):
|
||||||
Feature('XEP-0280: Message Carbons',
|
Feature('XEP-0280: Message Carbons',
|
||||||
con.carbons_available, nbxmpp.NS_CARBONS, carbons_enabled),
|
con.carbons_available, nbxmpp.NS_CARBONS, carbons_enabled),
|
||||||
Feature('XEP-0313: Message Archive Management',
|
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),
|
mam_enabled),
|
||||||
Feature('XEP-0363: HTTP File Upload',
|
Feature('XEP-0363: HTTP File Upload',
|
||||||
con.get_module('HTTPUpload').available,
|
con.get_module('HTTPUpload').available,
|
||||||
|
|
Loading…
Reference in New Issue