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
					
				
					 26 changed files with 1403 additions and 1462 deletions
				
			
		| 
						 | 
				
			
			@ -29,6 +29,7 @@ from gajim import history_window
 | 
			
		|||
from gajim import disco
 | 
			
		||||
from gajim.history_sync import HistorySyncAssistant
 | 
			
		||||
from gajim.server_info import ServerInfoDialog
 | 
			
		||||
from gajim.gtk.mam_preferences import MamPreferences
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# General Actions
 | 
			
		||||
| 
						 | 
				
			
			@ -181,14 +182,13 @@ def on_import_contacts(action, param):
 | 
			
		|||
# Advanced Actions
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def on_archiving_preferences(action, param):
 | 
			
		||||
def on_mam_preferences(action, param):
 | 
			
		||||
    account = param.get_string()
 | 
			
		||||
    if 'archiving_preferences' in interface.instances[account]:
 | 
			
		||||
        interface.instances[account]['archiving_preferences'].window.\
 | 
			
		||||
            present()
 | 
			
		||||
    window = app.get_app_window(MamPreferences, account)
 | 
			
		||||
    if window is None:
 | 
			
		||||
        MamPreferences(account)
 | 
			
		||||
    else:
 | 
			
		||||
        interface.instances[account]['archiving_preferences'] = \
 | 
			
		||||
            dialogs.Archiving313PreferencesWindow(account)
 | 
			
		||||
        window.present()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def on_history_sync(action, param):
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -356,7 +356,7 @@ class GajimApplication(Gtk.Application):
 | 
			
		|||
            ('-profile', app_actions.on_profile, 'feature', 's'),
 | 
			
		||||
            ('-xml-console', app_actions.on_xml_console, 'always', 's'),
 | 
			
		||||
            ('-server-info', app_actions.on_server_info, 'online', 's'),
 | 
			
		||||
            ('-archive', app_actions.on_archiving_preferences, 'feature', 's'),
 | 
			
		||||
            ('-archive', app_actions.on_mam_preferences, 'feature', 's'),
 | 
			
		||||
            ('-sync-history', app_actions.on_history_sync, 'online', 's'),
 | 
			
		||||
            ('-privacylists', app_actions.on_privacy_lists, 'feature', 's'),
 | 
			
		||||
            ('-send-server-message',
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -809,8 +809,13 @@ class ChatControl(ChatControlBase):
 | 
			
		|||
    def _nec_mam_decrypted_message_received(self, obj):
 | 
			
		||||
        if obj.conn.name != self.account:
 | 
			
		||||
            return
 | 
			
		||||
        if obj.with_ != self.contact.jid:
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        if obj.muc_pm:
 | 
			
		||||
            if not obj.with_ == self.contact.get_full_jid():
 | 
			
		||||
                return
 | 
			
		||||
        else:
 | 
			
		||||
            if not obj.with_.bareMatch(self.contact.jid):
 | 
			
		||||
                return
 | 
			
		||||
 | 
			
		||||
        kind = '' # incoming
 | 
			
		||||
        if obj.kind == KindConstant.CHAT_MSG_SENT:
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -595,11 +595,17 @@ def prefers_app_menu():
 | 
			
		|||
        return False
 | 
			
		||||
    return app.prefers_app_menu()
 | 
			
		||||
 | 
			
		||||
def get_app_window(cls):
 | 
			
		||||
def get_app_window(cls, account=None):
 | 
			
		||||
    for win in app.get_windows():
 | 
			
		||||
        if isinstance(cls, str):
 | 
			
		||||
            if type(win).__name__ == cls:
 | 
			
		||||
                if account is not None:
 | 
			
		||||
                    if account != win.account:
 | 
			
		||||
                        continue
 | 
			
		||||
                return win
 | 
			
		||||
        elif isinstance(win, cls):
 | 
			
		||||
            if account is not None:
 | 
			
		||||
                if account != win.account:
 | 
			
		||||
                    continue
 | 
			
		||||
            return win
 | 
			
		||||
    return None
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -305,7 +305,6 @@ class Config:
 | 
			
		|||
            'use_keyring': [opt_bool, True, _('If true, Gajim will use the Systems Keyring to store account passwords.')],
 | 
			
		||||
            'pgp_encoding': [ opt_str, '', _('Sets the encoding used by python-gnupg'), True],
 | 
			
		||||
            'remote_commands': [opt_bool, False, _('If true, Gajim will execute XEP-0146 Commands.')],
 | 
			
		||||
            'mam_blacklist': [opt_str, '', _('All non-compliant MAM Groupchats')],
 | 
			
		||||
    }, {})
 | 
			
		||||
 | 
			
		||||
    __options_per_key = {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -121,9 +121,6 @@ class CommonConnection:
 | 
			
		|||
        self.privacy_rules_supported = False
 | 
			
		||||
        self.vcard_supported = False
 | 
			
		||||
        self.private_storage_supported = False
 | 
			
		||||
        self.archiving_namespace = None
 | 
			
		||||
        self.archiving_supported = False
 | 
			
		||||
        self.archiving_313_supported = False
 | 
			
		||||
        self.roster_supported = True
 | 
			
		||||
        self.blocking_supported = False
 | 
			
		||||
        self.addressing_supported = False
 | 
			
		||||
| 
						 | 
				
			
			@ -1611,12 +1608,11 @@ class Connection(CommonConnection, ConnectionHandlers):
 | 
			
		|||
 | 
			
		||||
            if obj.fjid == our_jid:
 | 
			
		||||
                if nbxmpp.NS_MAM_2 in obj.features:
 | 
			
		||||
                    self.archiving_namespace = nbxmpp.NS_MAM_2
 | 
			
		||||
                    self.get_module('MAM').archiving_namespace = nbxmpp.NS_MAM_2
 | 
			
		||||
                elif nbxmpp.NS_MAM_1 in obj.features:
 | 
			
		||||
                    self.archiving_namespace = nbxmpp.NS_MAM_1
 | 
			
		||||
                if self.archiving_namespace:
 | 
			
		||||
                    self.archiving_supported = True
 | 
			
		||||
                    self.archiving_313_supported = True
 | 
			
		||||
                    self.get_module('MAM').archiving_namespace = nbxmpp.NS_MAM_1
 | 
			
		||||
                if self.get_module('MAM').archiving_namespace:
 | 
			
		||||
                    self.get_module('MAM').available = True
 | 
			
		||||
                    get_action(self.name + '-archive').set_enabled(True)
 | 
			
		||||
                for identity in obj.identities:
 | 
			
		||||
                    if identity['category'] == 'pubsub':
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -45,8 +45,8 @@ from gajim.common.caps_cache import muc_caps_cache
 | 
			
		|||
from gajim.common.protocol.caps import ConnectionCaps
 | 
			
		||||
from gajim.common.protocol.bytestream import ConnectionSocks5Bytestream
 | 
			
		||||
from gajim.common.protocol.bytestream import ConnectionIBBytestream
 | 
			
		||||
from gajim.common.message_archiving import ConnectionArchive313
 | 
			
		||||
from gajim.common.connection_handlers_events import *
 | 
			
		||||
from gajim.common.modules.misc import parse_eme
 | 
			
		||||
 | 
			
		||||
from gajim.common import ged
 | 
			
		||||
from gajim.common.nec import NetworkEvent
 | 
			
		||||
| 
						 | 
				
			
			@ -295,7 +295,9 @@ class ConnectionHandlersBase:
 | 
			
		|||
        # XEPs that are based on Message
 | 
			
		||||
        self._message_namespaces = set([nbxmpp.NS_HTTP_AUTH,
 | 
			
		||||
                                        nbxmpp.NS_PUBSUB_EVENT,
 | 
			
		||||
                                        nbxmpp.NS_ROSTERX])
 | 
			
		||||
                                        nbxmpp.NS_ROSTERX,
 | 
			
		||||
                                        nbxmpp.NS_MAM_1,
 | 
			
		||||
                                        nbxmpp.NS_MAM_2])
 | 
			
		||||
 | 
			
		||||
        app.ged.register_event_handler('iq-error-received', ged.CORE,
 | 
			
		||||
            self._nec_iq_error_received)
 | 
			
		||||
| 
						 | 
				
			
			@ -303,10 +305,6 @@ class ConnectionHandlersBase:
 | 
			
		|||
            self._nec_presence_received)
 | 
			
		||||
        app.ged.register_event_handler('message-received', ged.CORE,
 | 
			
		||||
            self._nec_message_received)
 | 
			
		||||
        app.ged.register_event_handler('mam-message-received', ged.CORE,
 | 
			
		||||
            self._nec_message_received)
 | 
			
		||||
        app.ged.register_event_handler('mam-gc-message-received', ged.CORE,
 | 
			
		||||
            self._nec_message_received)
 | 
			
		||||
        app.ged.register_event_handler('decrypted-message-received', ged.CORE,
 | 
			
		||||
            self._nec_decrypted_message_received)
 | 
			
		||||
        app.ged.register_event_handler('gc-message-received', ged.CORE,
 | 
			
		||||
| 
						 | 
				
			
			@ -319,10 +317,6 @@ class ConnectionHandlersBase:
 | 
			
		|||
            self._nec_presence_received)
 | 
			
		||||
        app.ged.remove_event_handler('message-received', ged.CORE,
 | 
			
		||||
            self._nec_message_received)
 | 
			
		||||
        app.ged.remove_event_handler('mam-message-received', ged.CORE,
 | 
			
		||||
            self._nec_message_received)
 | 
			
		||||
        app.ged.remove_event_handler('mam-gc-message-received', ged.CORE,
 | 
			
		||||
            self._nec_message_received)
 | 
			
		||||
        app.ged.remove_event_handler('decrypted-message-received', ged.CORE,
 | 
			
		||||
            self._nec_decrypted_message_received)
 | 
			
		||||
        app.ged.remove_event_handler('gc-message-received', ged.CORE,
 | 
			
		||||
| 
						 | 
				
			
			@ -460,37 +454,15 @@ class ConnectionHandlersBase:
 | 
			
		|||
        app.plugin_manager.extension_point(
 | 
			
		||||
            'decrypt', self, obj, self._on_message_received)
 | 
			
		||||
        if not obj.encrypted:
 | 
			
		||||
            # XEP-0380
 | 
			
		||||
            enc_tag = obj.stanza.getTag('encryption', namespace=nbxmpp.NS_EME)
 | 
			
		||||
            if enc_tag:
 | 
			
		||||
                ns = enc_tag.getAttr('namespace')
 | 
			
		||||
                if ns:
 | 
			
		||||
                    if ns == 'urn:xmpp:otr:0':
 | 
			
		||||
                        obj.msgtxt = _('This message was encrypted with OTR '
 | 
			
		||||
                        'and could not be decrypted.')
 | 
			
		||||
                    elif ns == 'jabber:x:encrypted':
 | 
			
		||||
                        obj.msgtxt = _('This message was encrypted with Legacy '
 | 
			
		||||
                        'OpenPGP and could not be decrypted. You can install '
 | 
			
		||||
                        'the PGP plugin to handle those messages.')
 | 
			
		||||
                    elif ns == 'urn:xmpp:openpgp:0':
 | 
			
		||||
                        obj.msgtxt = _('This message was encrypted with '
 | 
			
		||||
                        'OpenPGP for XMPP and could not be decrypted.')
 | 
			
		||||
                    else:
 | 
			
		||||
                        enc_name = enc_tag.getAttr('name')
 | 
			
		||||
                        if not enc_name:
 | 
			
		||||
                            enc_name = ns
 | 
			
		||||
                        obj.msgtxt = _('This message was encrypted with %s '
 | 
			
		||||
                        'and could not be decrypted.') % enc_name
 | 
			
		||||
            eme = parse_eme(obj.stanza)
 | 
			
		||||
            if eme is not None:
 | 
			
		||||
                obj.msgtxt = eme
 | 
			
		||||
            self._on_message_received(obj)
 | 
			
		||||
 | 
			
		||||
    def _on_message_received(self, obj):
 | 
			
		||||
        if isinstance(obj, MessageReceivedEvent):
 | 
			
		||||
            app.nec.push_incoming_event(
 | 
			
		||||
                DecryptedMessageReceivedEvent(
 | 
			
		||||
                    None, conn=self, msg_obj=obj, stanza_id=obj.unique_id))
 | 
			
		||||
        else:
 | 
			
		||||
            app.nec.push_incoming_event(
 | 
			
		||||
                MamDecryptedMessageReceivedEvent(None, **vars(obj)))
 | 
			
		||||
        app.nec.push_incoming_event(
 | 
			
		||||
            DecryptedMessageReceivedEvent(
 | 
			
		||||
                None, conn=self, msg_obj=obj, stanza_id=obj.unique_id))
 | 
			
		||||
 | 
			
		||||
    def _nec_decrypted_message_received(self, obj):
 | 
			
		||||
        if obj.conn.name != self.name:
 | 
			
		||||
| 
						 | 
				
			
			@ -564,7 +536,7 @@ class ConnectionHandlersBase:
 | 
			
		|||
    def _check_for_mam_compliance(self, room_jid, stanza_id):
 | 
			
		||||
        namespace = muc_caps_cache.get_mam_namespace(room_jid)
 | 
			
		||||
        if stanza_id is None and namespace == nbxmpp.NS_MAM_2:
 | 
			
		||||
            helpers.add_to_mam_blacklist(room_jid)
 | 
			
		||||
            log.warning('%s announces mam:2 without stanza-id')
 | 
			
		||||
 | 
			
		||||
    def _nec_gc_message_received(self, obj):
 | 
			
		||||
        if obj.conn.name != self.name:
 | 
			
		||||
| 
						 | 
				
			
			@ -743,11 +715,10 @@ class ConnectionHandlersBase:
 | 
			
		|||
 | 
			
		||||
        return sess
 | 
			
		||||
 | 
			
		||||
class ConnectionHandlers(ConnectionArchive313,
 | 
			
		||||
ConnectionSocks5Bytestream, ConnectionDisco, ConnectionCaps,
 | 
			
		||||
ConnectionHandlersBase, ConnectionJingle, ConnectionIBBytestream):
 | 
			
		||||
class ConnectionHandlers(ConnectionSocks5Bytestream, ConnectionDisco,
 | 
			
		||||
                         ConnectionCaps, ConnectionHandlersBase,
 | 
			
		||||
                         ConnectionJingle, ConnectionIBBytestream):
 | 
			
		||||
    def __init__(self):
 | 
			
		||||
        ConnectionArchive313.__init__(self)
 | 
			
		||||
        ConnectionSocks5Bytestream.__init__(self)
 | 
			
		||||
        ConnectionIBBytestream.__init__(self)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -772,9 +743,6 @@ ConnectionHandlersBase, ConnectionJingle, ConnectionIBBytestream):
 | 
			
		|||
 | 
			
		||||
        app.nec.register_incoming_event(StreamConflictReceivedEvent)
 | 
			
		||||
        app.nec.register_incoming_event(MessageReceivedEvent)
 | 
			
		||||
        app.nec.register_incoming_event(ArchivingErrorReceivedEvent)
 | 
			
		||||
        app.nec.register_incoming_event(
 | 
			
		||||
            Archiving313PreferencesChangedReceivedEvent)
 | 
			
		||||
        app.nec.register_incoming_event(NotificationEvent)
 | 
			
		||||
 | 
			
		||||
        app.ged.register_event_handler('roster-set-received',
 | 
			
		||||
| 
						 | 
				
			
			@ -799,7 +767,6 @@ ConnectionHandlersBase, ConnectionJingle, ConnectionIBBytestream):
 | 
			
		|||
    def cleanup(self):
 | 
			
		||||
        ConnectionHandlersBase.cleanup(self)
 | 
			
		||||
        ConnectionCaps.cleanup(self)
 | 
			
		||||
        ConnectionArchive313.cleanup(self)
 | 
			
		||||
        app.ged.remove_event_handler('roster-set-received',
 | 
			
		||||
            ged.CORE, self._nec_roster_set_received)
 | 
			
		||||
        app.ged.remove_event_handler('roster-received', ged.CORE,
 | 
			
		||||
| 
						 | 
				
			
			@ -1343,8 +1310,6 @@ ConnectionHandlersBase, ConnectionJingle, ConnectionIBBytestream):
 | 
			
		|||
        con.RegisterHandler('iq', self._DiscoverItemsGetCB, 'get',
 | 
			
		||||
            nbxmpp.NS_DISCO_ITEMS)
 | 
			
		||||
 | 
			
		||||
        con.RegisterHandler('iq', self._ArchiveCB, ns=nbxmpp.NS_MAM_1)
 | 
			
		||||
        con.RegisterHandler('iq', self._ArchiveCB, ns=nbxmpp.NS_MAM_2)
 | 
			
		||||
        con.RegisterHandler('iq', self._JingleCB, 'result')
 | 
			
		||||
        con.RegisterHandler('iq', self._JingleCB, 'error')
 | 
			
		||||
        con.RegisterHandler('iq', self._JingleCB, 'set', nbxmpp.NS_JINGLE)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -77,7 +77,10 @@ class HelperEvent:
 | 
			
		|||
            del self.conn.groupchat_jids[self.id_]
 | 
			
		||||
        else:
 | 
			
		||||
            self.fjid = helpers.get_full_jid_from_iq(self.stanza)
 | 
			
		||||
        self.jid, self.resource = app.get_room_and_nick_from_fjid(self.fjid)
 | 
			
		||||
        if self.fjid is None:
 | 
			
		||||
            self.jid = None
 | 
			
		||||
        else:
 | 
			
		||||
            self.jid, self.resource = app.get_room_and_nick_from_fjid(self.fjid)
 | 
			
		||||
 | 
			
		||||
    def get_id(self):
 | 
			
		||||
        self.id_ = self.stanza.getID()
 | 
			
		||||
| 
						 | 
				
			
			@ -630,240 +633,6 @@ class BeforeChangeShowEvent(nec.NetworkIncomingEvent):
 | 
			
		|||
    name = 'before-change-show'
 | 
			
		||||
    base_network_events = []
 | 
			
		||||
 | 
			
		||||
class MamMessageReceivedEvent(nec.NetworkIncomingEvent, HelperEvent):
 | 
			
		||||
    name = 'mam-message-received'
 | 
			
		||||
    base_network_events = ['raw-mam-message-received']
 | 
			
		||||
 | 
			
		||||
    def __init__(self, name, base_event):
 | 
			
		||||
        '''
 | 
			
		||||
        Pre-Generated attributes on self:
 | 
			
		||||
 | 
			
		||||
        :conn:          Connection instance
 | 
			
		||||
        :stanza:        Complete stanza Node
 | 
			
		||||
        :forwarded:     Forwarded Node
 | 
			
		||||
        :result:        Result Node
 | 
			
		||||
        '''
 | 
			
		||||
        self._set_base_event_vars_as_attributes(base_event)
 | 
			
		||||
        self.additional_data = {}
 | 
			
		||||
        self.encrypted = False
 | 
			
		||||
        self.groupchat = False
 | 
			
		||||
        self.nick = None
 | 
			
		||||
        self.self_message = None
 | 
			
		||||
        self.muc_pm = None
 | 
			
		||||
 | 
			
		||||
    def generate(self):
 | 
			
		||||
        account = self.conn.name
 | 
			
		||||
        archive_jid = self.stanza.getFrom()
 | 
			
		||||
        own_jid = self.conn.get_own_jid()
 | 
			
		||||
        if archive_jid and not archive_jid.bareMatch(own_jid):
 | 
			
		||||
            # MAM Message not from our Archive
 | 
			
		||||
            return False
 | 
			
		||||
 | 
			
		||||
        self.msg_ = self.forwarded.getTag('message', protocol=True)
 | 
			
		||||
 | 
			
		||||
        if self.msg_.getType() == 'groupchat':
 | 
			
		||||
            return False
 | 
			
		||||
 | 
			
		||||
        # use stanza-id as unique-id
 | 
			
		||||
        self.unique_id, origin_id = self.get_unique_id()
 | 
			
		||||
        self.message_id = self.msg_.getID()
 | 
			
		||||
 | 
			
		||||
        # Check for duplicates
 | 
			
		||||
        if app.logger.find_stanza_id(account,
 | 
			
		||||
                                     own_jid.getStripped(),
 | 
			
		||||
                                     self.unique_id, origin_id):
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        self.msgtxt = self.msg_.getTagData('body')
 | 
			
		||||
 | 
			
		||||
        frm = self.msg_.getFrom()
 | 
			
		||||
        # Some servers dont set the 'to' attribute when
 | 
			
		||||
        # we send a message to ourself
 | 
			
		||||
        to = self.msg_.getTo()
 | 
			
		||||
        if to is None:
 | 
			
		||||
            to = own_jid
 | 
			
		||||
 | 
			
		||||
        if frm.bareMatch(own_jid):
 | 
			
		||||
            self.with_ = to
 | 
			
		||||
            self.kind = KindConstant.CHAT_MSG_SENT
 | 
			
		||||
        else:
 | 
			
		||||
            self.with_ = frm
 | 
			
		||||
            self.kind = KindConstant.CHAT_MSG_RECV
 | 
			
		||||
 | 
			
		||||
        delay = self.forwarded.getTagAttr(
 | 
			
		||||
            'delay', 'stamp', namespace=nbxmpp.NS_DELAY2)
 | 
			
		||||
        if delay is None:
 | 
			
		||||
            log.error('Received MAM message without timestamp')
 | 
			
		||||
            log.error(self.stanza)
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        self.timestamp = helpers.parse_datetime(
 | 
			
		||||
            delay, check_utc=True, epoch=True)
 | 
			
		||||
        if self.timestamp is None:
 | 
			
		||||
            log.error('Received MAM message with invalid timestamp: %s', delay)
 | 
			
		||||
            log.error(self.stanza)
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        # Save timestamp added by the user
 | 
			
		||||
        user_delay = self.msg_.getTagAttr(
 | 
			
		||||
            'delay', 'stamp', namespace=nbxmpp.NS_DELAY2)
 | 
			
		||||
        if user_delay is not None:
 | 
			
		||||
            self.user_timestamp = helpers.parse_datetime(
 | 
			
		||||
                user_delay, check_utc=True, epoch=True)
 | 
			
		||||
            if self.user_timestamp is None:
 | 
			
		||||
                log.warning('Received MAM message with '
 | 
			
		||||
                            'invalid user timestamp: %s', user_delay)
 | 
			
		||||
                log.warning(self.stanza)
 | 
			
		||||
 | 
			
		||||
        log.debug('Received mam-message: unique id: %s', self.unique_id)
 | 
			
		||||
        return True
 | 
			
		||||
 | 
			
		||||
    def get_unique_id(self):
 | 
			
		||||
        stanza_id = self.get_stanza_id(self.result, query=True)
 | 
			
		||||
 | 
			
		||||
        if self._is_self_message(self.msg_) or self._is_muc_pm(self.msg_):
 | 
			
		||||
            origin_id = self.msg_.getOriginID()
 | 
			
		||||
            return stanza_id, origin_id
 | 
			
		||||
 | 
			
		||||
        if self.conn.get_own_jid().bareMatch(self.msg_.getFrom()):
 | 
			
		||||
            # message we sent
 | 
			
		||||
            origin_id = self.msg_.getOriginID()
 | 
			
		||||
            return stanza_id, origin_id
 | 
			
		||||
 | 
			
		||||
        # A message we received
 | 
			
		||||
        return stanza_id, None
 | 
			
		||||
 | 
			
		||||
class MamGcMessageReceivedEvent(nec.NetworkIncomingEvent, HelperEvent):
 | 
			
		||||
    name = 'mam-gc-message-received'
 | 
			
		||||
    base_network_events = ['raw-mam-message-received']
 | 
			
		||||
 | 
			
		||||
    def __init__(self, name, base_event):
 | 
			
		||||
        '''
 | 
			
		||||
        Pre-Generated attributes on self:
 | 
			
		||||
 | 
			
		||||
        :conn:          Connection instance
 | 
			
		||||
        :stanza:        Complete stanza Node
 | 
			
		||||
        :forwarded:     Forwarded Node
 | 
			
		||||
        :result:        Result Node
 | 
			
		||||
        :muc_pm:        True, if this is a MUC PM
 | 
			
		||||
                        propagated to MamDecryptedMessageReceivedEvent
 | 
			
		||||
        '''
 | 
			
		||||
        self._set_base_event_vars_as_attributes(base_event)
 | 
			
		||||
        self.additional_data = {}
 | 
			
		||||
        self.encrypted = False
 | 
			
		||||
        self.groupchat = True
 | 
			
		||||
        self.kind = KindConstant.GC_MSG
 | 
			
		||||
 | 
			
		||||
    def generate(self):
 | 
			
		||||
        account = self.conn.name
 | 
			
		||||
        self.msg_ = self.forwarded.getTag('message', protocol=True)
 | 
			
		||||
 | 
			
		||||
        if self.msg_.getType() != 'groupchat':
 | 
			
		||||
            return False
 | 
			
		||||
 | 
			
		||||
        try:
 | 
			
		||||
            self.room_jid = self.stanza.getFrom().getStripped()
 | 
			
		||||
        except AttributeError:
 | 
			
		||||
            log.warning('Received GC MAM message '
 | 
			
		||||
                        'without from attribute\n%s', self.stanza)
 | 
			
		||||
            return False
 | 
			
		||||
 | 
			
		||||
        self.unique_id = self.get_stanza_id(self.result, query=True)
 | 
			
		||||
        self.message_id = self.msg_.getID()
 | 
			
		||||
 | 
			
		||||
        # Check for duplicates
 | 
			
		||||
        if app.logger.find_stanza_id(account,
 | 
			
		||||
                                     self.room_jid,
 | 
			
		||||
                                     self.unique_id,
 | 
			
		||||
                                     groupchat=True):
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        self.msgtxt = self.msg_.getTagData('body')
 | 
			
		||||
        self.with_ = self.msg_.getFrom().getStripped()
 | 
			
		||||
        self.nick = self.msg_.getFrom().getResource()
 | 
			
		||||
 | 
			
		||||
        # Get the real jid if we have it
 | 
			
		||||
        self.real_jid = None
 | 
			
		||||
        muc_user = self.msg_.getTag('x', namespace=nbxmpp.NS_MUC_USER)
 | 
			
		||||
        if muc_user is not None:
 | 
			
		||||
            self.real_jid = muc_user.getTagAttr('item', 'jid')
 | 
			
		||||
 | 
			
		||||
        delay = self.forwarded.getTagAttr(
 | 
			
		||||
            'delay', 'stamp', namespace=nbxmpp.NS_DELAY2)
 | 
			
		||||
        if delay is None:
 | 
			
		||||
            log.error('Received MAM message without timestamp')
 | 
			
		||||
            log.error(self.stanza)
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        self.timestamp = helpers.parse_datetime(
 | 
			
		||||
            delay, check_utc=True, epoch=True)
 | 
			
		||||
        if self.timestamp is None:
 | 
			
		||||
            log.error('Received MAM message with invalid timestamp: %s', delay)
 | 
			
		||||
            log.error(self.stanza)
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        # Save timestamp added by the user
 | 
			
		||||
        user_delay = self.msg_.getTagAttr(
 | 
			
		||||
            'delay', 'stamp', namespace=nbxmpp.NS_DELAY2)
 | 
			
		||||
        if user_delay is not None:
 | 
			
		||||
            self.user_timestamp = helpers.parse_datetime(
 | 
			
		||||
                user_delay, check_utc=True, epoch=True)
 | 
			
		||||
            if self.user_timestamp is None:
 | 
			
		||||
                log.warning('Received MAM message with '
 | 
			
		||||
                            'invalid user timestamp: %s', user_delay)
 | 
			
		||||
                log.warning(self.stanza)
 | 
			
		||||
 | 
			
		||||
        log.debug('Received mam-gc-message: unique id: %s', self.unique_id)
 | 
			
		||||
        return True
 | 
			
		||||
 | 
			
		||||
class MamDecryptedMessageReceivedEvent(nec.NetworkIncomingEvent, HelperEvent):
 | 
			
		||||
    name = 'mam-decrypted-message-received'
 | 
			
		||||
    base_network_events = []
 | 
			
		||||
 | 
			
		||||
    def generate(self):
 | 
			
		||||
        self.correct_id = None
 | 
			
		||||
 | 
			
		||||
        if not self.msgtxt:
 | 
			
		||||
            # For example Chatstates, Receipts, Chatmarkers
 | 
			
		||||
            log.debug('Received MAM message without text')
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        replace = self.msg_.getTag('replace', namespace=nbxmpp.NS_CORRECT)
 | 
			
		||||
        if replace is not None:
 | 
			
		||||
            self.correct_id = replace.getAttr('id')
 | 
			
		||||
 | 
			
		||||
        self.get_oob_data(self.msg_)
 | 
			
		||||
 | 
			
		||||
        if self.groupchat:
 | 
			
		||||
            return True
 | 
			
		||||
 | 
			
		||||
        if not self.muc_pm:
 | 
			
		||||
            # muc_pm = False, means only there was no muc#user namespace
 | 
			
		||||
            # This could still be a muc pm, we check the database if we
 | 
			
		||||
            # know this jid. If not we disco it.
 | 
			
		||||
            self.muc_pm = app.logger.jid_is_room_jid(self.with_.getStripped())
 | 
			
		||||
            if self.muc_pm is None:
 | 
			
		||||
                # Check if this event is triggered after a disco, so we dont
 | 
			
		||||
                # run into an endless loop
 | 
			
		||||
                if hasattr(self, 'disco'):
 | 
			
		||||
                    log.error('JID not known even after sucessful disco')
 | 
			
		||||
                    log.error(self.with_.getStripped())
 | 
			
		||||
                    return
 | 
			
		||||
                # we don't know this JID, we need to disco it.
 | 
			
		||||
                server = self.with_.getDomain()
 | 
			
		||||
                if server not in self.conn.mam_awaiting_disco_result:
 | 
			
		||||
                    self.conn.mam_awaiting_disco_result[server] = [self]
 | 
			
		||||
                    self.conn.discoverInfo(server)
 | 
			
		||||
                else:
 | 
			
		||||
                    self.conn.mam_awaiting_disco_result[server].append(self)
 | 
			
		||||
                return
 | 
			
		||||
 | 
			
		||||
        if self.muc_pm:
 | 
			
		||||
            self.with_ = str(self.with_)
 | 
			
		||||
        else:
 | 
			
		||||
            self.with_ = self.with_.getStripped()
 | 
			
		||||
        return True
 | 
			
		||||
 | 
			
		||||
class MessageReceivedEvent(nec.NetworkIncomingEvent, HelperEvent):
 | 
			
		||||
    name = 'message-received'
 | 
			
		||||
    base_network_events = ['raw-message-received']
 | 
			
		||||
| 
						 | 
				
			
			@ -968,30 +737,6 @@ class MessageReceivedEvent(nec.NetworkIncomingEvent, HelperEvent):
 | 
			
		|||
                    return
 | 
			
		||||
                self.forwarded = True
 | 
			
		||||
 | 
			
		||||
        result = self.stanza.getTag('result', protocol=True)
 | 
			
		||||
        if result and result.getNamespace() in (nbxmpp.NS_MAM_1,
 | 
			
		||||
                                                nbxmpp.NS_MAM_2):
 | 
			
		||||
 | 
			
		||||
            if result.getAttr('queryid') not in self.conn.mam_query_ids:
 | 
			
		||||
                log.warning('Invalid MAM Message: unknown query id')
 | 
			
		||||
                log.debug(self.stanza)
 | 
			
		||||
                return
 | 
			
		||||
 | 
			
		||||
            forwarded = result.getTag('forwarded',
 | 
			
		||||
                                      namespace=nbxmpp.NS_FORWARD,
 | 
			
		||||
                                      protocol=True)
 | 
			
		||||
            if not forwarded:
 | 
			
		||||
                log.warning('Invalid MAM Message: no forwarded child')
 | 
			
		||||
                return
 | 
			
		||||
 | 
			
		||||
            app.nec.push_incoming_event(
 | 
			
		||||
                NetworkEvent('raw-mam-message-received',
 | 
			
		||||
                             conn=self.conn,
 | 
			
		||||
                             stanza=self.stanza,
 | 
			
		||||
                             forwarded=forwarded,
 | 
			
		||||
                             result=result))
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        # Mediated invitation?
 | 
			
		||||
        muc_user = self.stanza.getTag('x', namespace=nbxmpp.NS_MUC_USER)
 | 
			
		||||
        if muc_user:
 | 
			
		||||
| 
						 | 
				
			
			@ -1085,7 +830,7 @@ class MessageReceivedEvent(nec.NetworkIncomingEvent, HelperEvent):
 | 
			
		|||
            return
 | 
			
		||||
 | 
			
		||||
        # Messages we receive live
 | 
			
		||||
        if self.conn.archiving_namespace != nbxmpp.NS_MAM_2:
 | 
			
		||||
        if self.conn.get_module('MAM').archiving_namespace != nbxmpp.NS_MAM_2:
 | 
			
		||||
            # Only mam:2 ensures valid stanza-id
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -1498,77 +1243,6 @@ class JingleErrorReceivedEvent(nec.NetworkIncomingEvent):
 | 
			
		|||
        self.sid = self.jingle_session.sid
 | 
			
		||||
        return True
 | 
			
		||||
 | 
			
		||||
class ArchivingReceivedEvent(nec.NetworkIncomingEvent):
 | 
			
		||||
    name = 'archiving-received'
 | 
			
		||||
    base_network_events = []
 | 
			
		||||
 | 
			
		||||
    def generate(self):
 | 
			
		||||
        self.type_ = self.stanza.getType()
 | 
			
		||||
        if self.type_ not in ('result', 'set', 'error'):
 | 
			
		||||
            return
 | 
			
		||||
        return True
 | 
			
		||||
 | 
			
		||||
class ArchivingErrorReceivedEvent(nec.NetworkIncomingEvent):
 | 
			
		||||
    name = 'archiving-error-received'
 | 
			
		||||
    base_network_events = ['archiving-received']
 | 
			
		||||
 | 
			
		||||
    def generate(self):
 | 
			
		||||
        self.conn = self.base_event.conn
 | 
			
		||||
        self.stanza = self.base_event.stanza
 | 
			
		||||
        self.type_ = self.base_event.type_
 | 
			
		||||
 | 
			
		||||
        if self.type_ == 'error':
 | 
			
		||||
            self.error_msg = self.stanza.getErrorMsg()
 | 
			
		||||
            return True
 | 
			
		||||
 | 
			
		||||
class ArchivingCountReceived(nec.NetworkIncomingEvent):
 | 
			
		||||
    name = 'archiving-count-received'
 | 
			
		||||
    base_network_events = []
 | 
			
		||||
 | 
			
		||||
    def generate(self):
 | 
			
		||||
        return True
 | 
			
		||||
 | 
			
		||||
class ArchivingIntervalFinished(nec.NetworkIncomingEvent):
 | 
			
		||||
    name = 'archiving-interval-finished'
 | 
			
		||||
    base_network_events = []
 | 
			
		||||
 | 
			
		||||
    def generate(self):
 | 
			
		||||
        return True
 | 
			
		||||
 | 
			
		||||
class ArchivingQueryID(nec.NetworkIncomingEvent):
 | 
			
		||||
    name = 'archiving-query-id'
 | 
			
		||||
    base_network_events = []
 | 
			
		||||
 | 
			
		||||
    def generate(self):
 | 
			
		||||
        return True
 | 
			
		||||
 | 
			
		||||
class Archiving313PreferencesChangedReceivedEvent(nec.NetworkIncomingEvent):
 | 
			
		||||
    name = 'archiving-313-preferences-changed-received'
 | 
			
		||||
    base_network_events = ['archiving-received']
 | 
			
		||||
 | 
			
		||||
    def generate(self):
 | 
			
		||||
        self.conn = self.base_event.conn
 | 
			
		||||
        self.stanza = self.base_event.stanza
 | 
			
		||||
        self.type_ = self.base_event.type_
 | 
			
		||||
        self.items = []
 | 
			
		||||
        self.default = None
 | 
			
		||||
        self.id = self.stanza.getID()
 | 
			
		||||
        self.answer = None
 | 
			
		||||
        prefs = self.stanza.getTag('prefs')
 | 
			
		||||
 | 
			
		||||
        if self.type_ != 'result' or not prefs:
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        self.default = prefs.getAttr('default')
 | 
			
		||||
 | 
			
		||||
        for item in prefs.getTag('always').getTags('jid'):
 | 
			
		||||
            self.items.append((item.getData(), 'Always'))
 | 
			
		||||
 | 
			
		||||
        for item in prefs.getTag('never').getTags('jid'):
 | 
			
		||||
            self.items.append((item.getData(), 'Never'))
 | 
			
		||||
 | 
			
		||||
        return True
 | 
			
		||||
 | 
			
		||||
class AccountCreatedEvent(nec.NetworkIncomingEvent):
 | 
			
		||||
    name = 'account-created'
 | 
			
		||||
    base_network_events = []
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -43,7 +43,7 @@ import shlex
 | 
			
		|||
from gajim.common import caps_cache
 | 
			
		||||
import socket
 | 
			
		||||
import time
 | 
			
		||||
from datetime import datetime, timedelta, timezone, tzinfo
 | 
			
		||||
from datetime import datetime, timedelta
 | 
			
		||||
from distutils.version import LooseVersion as V
 | 
			
		||||
 | 
			
		||||
from encodings.punycode import punycode_encode
 | 
			
		||||
| 
						 | 
				
			
			@ -89,77 +89,6 @@ log = logging.getLogger('gajim.c.helpers')
 | 
			
		|||
 | 
			
		||||
special_groups = (_('Transports'), _('Not in Roster'), _('Observers'), _('Groupchats'))
 | 
			
		||||
 | 
			
		||||
# Patterns for DateTime parsing XEP-0082
 | 
			
		||||
PATTERN_DATETIME = re.compile(
 | 
			
		||||
        r'([0-9]{4}-[0-9]{2}-[0-9]{2})'
 | 
			
		||||
        r'T'
 | 
			
		||||
        r'([0-9]{2}:[0-9]{2}:[0-9]{2})'
 | 
			
		||||
        r'(\.[0-9]{0,6})?'
 | 
			
		||||
        r'(?:[0-9]+)?'
 | 
			
		||||
        r'(?:(Z)|(?:([-+][0-9]{2}):([0-9]{2})))$'
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
PATTERN_DELAY = re.compile(
 | 
			
		||||
        r'([0-9]{4}-[0-9]{2}-[0-9]{2})'
 | 
			
		||||
        r'T'
 | 
			
		||||
        r'([0-9]{2}:[0-9]{2}:[0-9]{2})'
 | 
			
		||||
        r'(\.[0-9]{0,6})?'
 | 
			
		||||
        r'(?:[0-9]+)?'
 | 
			
		||||
        r'(?:(Z)|(?:([-+][0]{2}):([0]{2})))$'
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
ZERO = timedelta(0)
 | 
			
		||||
HOUR = timedelta(hours=1)
 | 
			
		||||
SECOND = timedelta(seconds=1)
 | 
			
		||||
 | 
			
		||||
STDOFFSET = timedelta(seconds=-time.timezone)
 | 
			
		||||
if time.daylight:
 | 
			
		||||
    DSTOFFSET = timedelta(seconds=-time.altzone)
 | 
			
		||||
else:
 | 
			
		||||
    DSTOFFSET = STDOFFSET
 | 
			
		||||
 | 
			
		||||
DSTDIFF = DSTOFFSET - STDOFFSET
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class LocalTimezone(tzinfo):
 | 
			
		||||
    '''
 | 
			
		||||
    A class capturing the platform's idea of local time.
 | 
			
		||||
    May result in wrong values on historical times in
 | 
			
		||||
    timezones where UTC offset and/or the DST rules had
 | 
			
		||||
    changed in the past.
 | 
			
		||||
    '''
 | 
			
		||||
    def fromutc(self, dt):
 | 
			
		||||
        assert dt.tzinfo is self
 | 
			
		||||
        stamp = (dt - datetime(1970, 1, 1, tzinfo=self)) // SECOND
 | 
			
		||||
        args = time.localtime(stamp)[:6]
 | 
			
		||||
        dst_diff = DSTDIFF // SECOND
 | 
			
		||||
        # Detect fold
 | 
			
		||||
        fold = (args == time.localtime(stamp - dst_diff))
 | 
			
		||||
        return datetime(*args, microsecond=dt.microsecond,
 | 
			
		||||
                        tzinfo=self, fold=fold)
 | 
			
		||||
 | 
			
		||||
    def utcoffset(self, dt):
 | 
			
		||||
        if self._isdst(dt):
 | 
			
		||||
            return DSTOFFSET
 | 
			
		||||
        else:
 | 
			
		||||
            return STDOFFSET
 | 
			
		||||
 | 
			
		||||
    def dst(self, dt):
 | 
			
		||||
        if self._isdst(dt):
 | 
			
		||||
            return DSTDIFF
 | 
			
		||||
        else:
 | 
			
		||||
            return ZERO
 | 
			
		||||
 | 
			
		||||
    def tzname(self, dt):
 | 
			
		||||
        return 'local'
 | 
			
		||||
 | 
			
		||||
    def _isdst(self, dt):
 | 
			
		||||
        tt = (dt.year, dt.month, dt.day,
 | 
			
		||||
              dt.hour, dt.minute, dt.second,
 | 
			
		||||
              dt.weekday(), 0, 0)
 | 
			
		||||
        stamp = time.mktime(tt)
 | 
			
		||||
        tt = time.localtime(stamp)
 | 
			
		||||
        return tt.tm_isdst > 0
 | 
			
		||||
 | 
			
		||||
class InvalidFormat(Exception):
 | 
			
		||||
    pass
 | 
			
		||||
| 
						 | 
				
			
			@ -673,56 +602,6 @@ def datetime_tuple(timestamp):
 | 
			
		|||
        tim = tim.timetuple()
 | 
			
		||||
    return tim
 | 
			
		||||
 | 
			
		||||
def parse_datetime(timestring, check_utc=False, convert='utc', epoch=False):
 | 
			
		||||
    '''
 | 
			
		||||
    Parse a XEP-0082 DateTime Profile String
 | 
			
		||||
    https://xmpp.org/extensions/xep-0082.html
 | 
			
		||||
 | 
			
		||||
    :param timestring: a XEP-0082 DateTime profile formated string
 | 
			
		||||
 | 
			
		||||
    :param check_utc:  if True, returns None if timestring is not
 | 
			
		||||
                       a timestring expressing UTC
 | 
			
		||||
 | 
			
		||||
    :param convert:    convert the given timestring to utc or local time
 | 
			
		||||
 | 
			
		||||
    :param epoch:      if True, returns the time in epoch
 | 
			
		||||
 | 
			
		||||
    Examples:
 | 
			
		||||
    '2017-11-05T01:41:20Z'
 | 
			
		||||
    '2017-11-05T01:41:20.123Z'
 | 
			
		||||
    '2017-11-05T01:41:20.123+05:00'
 | 
			
		||||
 | 
			
		||||
    return a datetime or epoch
 | 
			
		||||
    '''
 | 
			
		||||
    if convert not in (None, 'utc', 'local'):
 | 
			
		||||
        raise TypeError('"%s" is not a valid value for convert')
 | 
			
		||||
    if check_utc:
 | 
			
		||||
        match = PATTERN_DELAY.match(timestring)
 | 
			
		||||
    else:
 | 
			
		||||
        match = PATTERN_DATETIME.match(timestring)
 | 
			
		||||
 | 
			
		||||
    if match:
 | 
			
		||||
        timestring = ''.join(match.groups(''))
 | 
			
		||||
        strformat = '%Y-%m-%d%H:%M:%S%z'
 | 
			
		||||
        if match.group(3):
 | 
			
		||||
            # Fractional second addendum to Time
 | 
			
		||||
            strformat = '%Y-%m-%d%H:%M:%S.%f%z'
 | 
			
		||||
        if match.group(4):
 | 
			
		||||
            # UTC string denoted by addition of the character 'Z'
 | 
			
		||||
            timestring = timestring[:-1] + '+0000'
 | 
			
		||||
        try:
 | 
			
		||||
            date_time = datetime.strptime(timestring, strformat)
 | 
			
		||||
        except ValueError:
 | 
			
		||||
            pass
 | 
			
		||||
        else:
 | 
			
		||||
            if not check_utc and convert == 'utc':
 | 
			
		||||
                date_time = date_time.astimezone(timezone.utc)
 | 
			
		||||
            if convert == 'local':
 | 
			
		||||
                date_time = date_time.astimezone(LocalTimezone())
 | 
			
		||||
            if epoch:
 | 
			
		||||
                return date_time.timestamp()
 | 
			
		||||
            return date_time
 | 
			
		||||
    return None
 | 
			
		||||
 | 
			
		||||
from gajim.common import app
 | 
			
		||||
if app.is_installed('PYCURL'):
 | 
			
		||||
| 
						 | 
				
			
			@ -1003,6 +882,9 @@ def get_full_jid_from_iq(iq_obj):
 | 
			
		|||
    """
 | 
			
		||||
    Return the full jid (with resource) from an iq
 | 
			
		||||
    """
 | 
			
		||||
    jid = iq_obj.getFrom()
 | 
			
		||||
    if jid is None:
 | 
			
		||||
        return None
 | 
			
		||||
    return parse_jid(str(iq_obj.getFrom()))
 | 
			
		||||
 | 
			
		||||
def get_jid_from_iq(iq_obj):
 | 
			
		||||
| 
						 | 
				
			
			@ -1626,21 +1508,3 @@ def get_emoticon_theme_path(theme):
 | 
			
		|||
    emoticons_user_path = os.path.join(configpaths.get('MY_EMOTS'), theme)
 | 
			
		||||
    if os.path.exists(emoticons_user_path):
 | 
			
		||||
        return emoticons_user_path
 | 
			
		||||
 | 
			
		||||
def add_to_mam_blacklist(jid):
 | 
			
		||||
    config_value = app.config.get('mam_blacklist')
 | 
			
		||||
    if not config_value:
 | 
			
		||||
        config_value = [jid]
 | 
			
		||||
    else:
 | 
			
		||||
        if jid in config_value:
 | 
			
		||||
            return
 | 
			
		||||
        config_value = config_value.split(',')
 | 
			
		||||
        config_value.append(jid)
 | 
			
		||||
    log.warning('Found not-compliant MUC. %s added to MAM Blacklist', jid)
 | 
			
		||||
    app.config.set('mam_blacklist', ','.join(config_value))
 | 
			
		||||
 | 
			
		||||
def get_mam_blacklist():
 | 
			
		||||
    config_value = app.config.get('mam_blacklist')
 | 
			
		||||
    if not config_value:
 | 
			
		||||
        return []
 | 
			
		||||
    return config_value.split(',')
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -374,14 +374,10 @@ class Logger:
 | 
			
		|||
        """
 | 
			
		||||
        Return True if it's a room jid, False if it's not, None if we don't know
 | 
			
		||||
        """
 | 
			
		||||
        row = self._con.execute(
 | 
			
		||||
            'SELECT type FROM jids WHERE jid=?', (jid,)).fetchone()
 | 
			
		||||
        if row is None:
 | 
			
		||||
            return None
 | 
			
		||||
        else:
 | 
			
		||||
            if row.type == JIDConstant.ROOM_TYPE:
 | 
			
		||||
                return True
 | 
			
		||||
            return False
 | 
			
		||||
        jid_ = self._jid_ids.get(jid)
 | 
			
		||||
        if jid_ is None:
 | 
			
		||||
            return
 | 
			
		||||
        return jid_.type == JIDConstant.ROOM_TYPE
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def _get_family_jids(account, jid):
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
							
								
								
									
										144
									
								
								gajim/common/modules/date_and_time.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										144
									
								
								gajim/common/modules/date_and_time.py
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,144 @@
 | 
			
		|||
# This file is part of Gajim.
 | 
			
		||||
#
 | 
			
		||||
# Gajim is free software; you can redistribute it and/or modify
 | 
			
		||||
# it under the terms of the GNU General Public License as published
 | 
			
		||||
# by the Free Software Foundation; version 3 only.
 | 
			
		||||
#
 | 
			
		||||
# Gajim is distributed in the hope that it will be useful,
 | 
			
		||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
			
		||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 | 
			
		||||
# GNU General Public License for more details.
 | 
			
		||||
#
 | 
			
		||||
# You should have received a copy of the GNU General Public License
 | 
			
		||||
# along with Gajim. If not, see <http://www.gnu.org/licenses/>.
 | 
			
		||||
 | 
			
		||||
# XEP-0082: XMPP Date and Time Profiles
 | 
			
		||||
 | 
			
		||||
import re
 | 
			
		||||
import time
 | 
			
		||||
from datetime import datetime, timedelta, timezone, tzinfo
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
PATTERN_DATETIME = re.compile(
 | 
			
		||||
    r'([0-9]{4}-[0-9]{2}-[0-9]{2})'
 | 
			
		||||
    r'T'
 | 
			
		||||
    r'([0-9]{2}:[0-9]{2}:[0-9]{2})'
 | 
			
		||||
    r'(\.[0-9]{0,6})?'
 | 
			
		||||
    r'(?:[0-9]+)?'
 | 
			
		||||
    r'(?:(Z)|(?:([-+][0-9]{2}):([0-9]{2})))$'
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
PATTERN_DELAY = re.compile(
 | 
			
		||||
    r'([0-9]{4}-[0-9]{2}-[0-9]{2})'
 | 
			
		||||
    r'T'
 | 
			
		||||
    r'([0-9]{2}:[0-9]{2}:[0-9]{2})'
 | 
			
		||||
    r'(\.[0-9]{0,6})?'
 | 
			
		||||
    r'(?:[0-9]+)?'
 | 
			
		||||
    r'(?:(Z)|(?:([-+][0]{2}):([0]{2})))$'
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
ZERO = timedelta(0)
 | 
			
		||||
HOUR = timedelta(hours=1)
 | 
			
		||||
SECOND = timedelta(seconds=1)
 | 
			
		||||
 | 
			
		||||
STDOFFSET = timedelta(seconds=-time.timezone)
 | 
			
		||||
if time.daylight:
 | 
			
		||||
    DSTOFFSET = timedelta(seconds=-time.altzone)
 | 
			
		||||
else:
 | 
			
		||||
    DSTOFFSET = STDOFFSET
 | 
			
		||||
 | 
			
		||||
DSTDIFF = DSTOFFSET - STDOFFSET
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class LocalTimezone(tzinfo):
 | 
			
		||||
    '''
 | 
			
		||||
    A class capturing the platform's idea of local time.
 | 
			
		||||
    May result in wrong values on historical times in
 | 
			
		||||
    timezones where UTC offset and/or the DST rules had
 | 
			
		||||
    changed in the past.
 | 
			
		||||
    '''
 | 
			
		||||
    def fromutc(self, dt):
 | 
			
		||||
        assert dt.tzinfo is self
 | 
			
		||||
        stamp = (dt - datetime(1970, 1, 1, tzinfo=self)) // SECOND
 | 
			
		||||
        args = time.localtime(stamp)[:6]
 | 
			
		||||
        dst_diff = DSTDIFF // SECOND
 | 
			
		||||
        # Detect fold
 | 
			
		||||
        fold = (args == time.localtime(stamp - dst_diff))
 | 
			
		||||
        return datetime(*args, microsecond=dt.microsecond,
 | 
			
		||||
                        tzinfo=self, fold=fold)
 | 
			
		||||
 | 
			
		||||
    def utcoffset(self, dt):
 | 
			
		||||
        if self._isdst(dt):
 | 
			
		||||
            return DSTOFFSET
 | 
			
		||||
        else:
 | 
			
		||||
            return STDOFFSET
 | 
			
		||||
 | 
			
		||||
    def dst(self, dt):
 | 
			
		||||
        if self._isdst(dt):
 | 
			
		||||
            return DSTDIFF
 | 
			
		||||
        else:
 | 
			
		||||
            return ZERO
 | 
			
		||||
 | 
			
		||||
    def tzname(self, dt):
 | 
			
		||||
        return 'local'
 | 
			
		||||
 | 
			
		||||
    def _isdst(self, dt):
 | 
			
		||||
        tt = (dt.year, dt.month, dt.day,
 | 
			
		||||
              dt.hour, dt.minute, dt.second,
 | 
			
		||||
              dt.weekday(), 0, 0)
 | 
			
		||||
        stamp = time.mktime(tt)
 | 
			
		||||
        tt = time.localtime(stamp)
 | 
			
		||||
        return tt.tm_isdst > 0
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def parse_datetime(timestring, check_utc=False,
 | 
			
		||||
                   convert='utc', epoch=False):
 | 
			
		||||
    '''
 | 
			
		||||
    Parse a XEP-0082 DateTime Profile String
 | 
			
		||||
 | 
			
		||||
    :param timestring: a XEP-0082 DateTime profile formated string
 | 
			
		||||
 | 
			
		||||
    :param check_utc:  if True, returns None if timestring is not
 | 
			
		||||
                       a timestring expressing UTC
 | 
			
		||||
 | 
			
		||||
    :param convert:    convert the given timestring to utc or local time
 | 
			
		||||
 | 
			
		||||
    :param epoch:      if True, returns the time in epoch
 | 
			
		||||
 | 
			
		||||
    Examples:
 | 
			
		||||
    '2017-11-05T01:41:20Z'
 | 
			
		||||
    '2017-11-05T01:41:20.123Z'
 | 
			
		||||
    '2017-11-05T01:41:20.123+05:00'
 | 
			
		||||
 | 
			
		||||
    return a datetime or epoch
 | 
			
		||||
    '''
 | 
			
		||||
    if convert not in (None, 'utc', 'local'):
 | 
			
		||||
        raise TypeError('"%s" is not a valid value for convert')
 | 
			
		||||
    if check_utc:
 | 
			
		||||
        match = PATTERN_DELAY.match(timestring)
 | 
			
		||||
    else:
 | 
			
		||||
        match = PATTERN_DATETIME.match(timestring)
 | 
			
		||||
 | 
			
		||||
    if match:
 | 
			
		||||
        timestring = ''.join(match.groups(''))
 | 
			
		||||
        strformat = '%Y-%m-%d%H:%M:%S%z'
 | 
			
		||||
        if match.group(3):
 | 
			
		||||
            # Fractional second addendum to Time
 | 
			
		||||
            strformat = '%Y-%m-%d%H:%M:%S.%f%z'
 | 
			
		||||
        if match.group(4):
 | 
			
		||||
            # UTC string denoted by addition of the character 'Z'
 | 
			
		||||
            timestring = timestring[:-1] + '+0000'
 | 
			
		||||
        try:
 | 
			
		||||
            date_time = datetime.strptime(timestring, strformat)
 | 
			
		||||
        except ValueError:
 | 
			
		||||
            pass
 | 
			
		||||
        else:
 | 
			
		||||
            if not check_utc and convert == 'utc':
 | 
			
		||||
                date_time = date_time.astimezone(timezone.utc)
 | 
			
		||||
            if convert == 'local':
 | 
			
		||||
                date_time = date_time.astimezone(LocalTimezone())
 | 
			
		||||
            if epoch:
 | 
			
		||||
                return date_time.timestamp()
 | 
			
		||||
            return date_time
 | 
			
		||||
    return None
 | 
			
		||||
							
								
								
									
										626
									
								
								gajim/common/modules/mam.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										626
									
								
								gajim/common/modules/mam.py
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,626 @@
 | 
			
		|||
# This file is part of Gajim.
 | 
			
		||||
#
 | 
			
		||||
# Gajim is free software; you can redistribute it and/or modify
 | 
			
		||||
# it under the terms of the GNU General Public License as published
 | 
			
		||||
# by the Free Software Foundation; version 3 only.
 | 
			
		||||
#
 | 
			
		||||
# Gajim is distributed in the hope that it will be useful,
 | 
			
		||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
			
		||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 | 
			
		||||
# GNU General Public License for more details.
 | 
			
		||||
#
 | 
			
		||||
# You should have received a copy of the GNU General Public License
 | 
			
		||||
# along with Gajim.  If not, see <http://www.gnu.org/licenses/>.
 | 
			
		||||
 | 
			
		||||
# XEP-0313: Message Archive Management
 | 
			
		||||
 | 
			
		||||
import logging
 | 
			
		||||
from datetime import datetime, timedelta
 | 
			
		||||
 | 
			
		||||
import nbxmpp
 | 
			
		||||
 | 
			
		||||
from gajim.common import app
 | 
			
		||||
from gajim.common.nec import NetworkIncomingEvent
 | 
			
		||||
from gajim.common.const import ArchiveState, JIDConstant, KindConstant
 | 
			
		||||
from gajim.common.caps_cache import muc_caps_cache
 | 
			
		||||
from gajim.common.modules.misc import parse_delay
 | 
			
		||||
from gajim.common.modules.misc import parse_oob
 | 
			
		||||
from gajim.common.modules.misc import parse_correction
 | 
			
		||||
from gajim.common.modules.misc import parse_eme
 | 
			
		||||
 | 
			
		||||
log = logging.getLogger('gajim.c.m.archiving')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class MAM:
 | 
			
		||||
    def __init__(self, con):
 | 
			
		||||
        self._con = con
 | 
			
		||||
        self._account = con.name
 | 
			
		||||
 | 
			
		||||
        self.handlers = [
 | 
			
		||||
            ('message', self._mam_message_received, nbxmpp.NS_MAM_1),
 | 
			
		||||
            ('message', self._mam_message_received, nbxmpp.NS_MAM_2)
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
        self.available = False
 | 
			
		||||
        self.archiving_namespace = None
 | 
			
		||||
        self._mam_query_ids = {}
 | 
			
		||||
 | 
			
		||||
    def _from_valid_archive(self, stanza, message, groupchat):
 | 
			
		||||
        if groupchat:
 | 
			
		||||
            expected_archive = message.getFrom()
 | 
			
		||||
        else:
 | 
			
		||||
            expected_archive = self._con.get_own_jid()
 | 
			
		||||
 | 
			
		||||
        archive_jid = stanza.getFrom()
 | 
			
		||||
        if archive_jid is None:
 | 
			
		||||
            if groupchat:
 | 
			
		||||
                return
 | 
			
		||||
            # Message from our own archive
 | 
			
		||||
            return self._con.get_own_jid()
 | 
			
		||||
        else:
 | 
			
		||||
            if archive_jid.bareMatch(expected_archive):
 | 
			
		||||
                return archive_jid
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def _is_self_message(message, groupchat):
 | 
			
		||||
        if groupchat:
 | 
			
		||||
            return False
 | 
			
		||||
        frm = message.getFrom()
 | 
			
		||||
        to = message.getTo()
 | 
			
		||||
        return frm.bareMatch(to)
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def _is_muc_pm(message, groupchat, with_):
 | 
			
		||||
        if groupchat:
 | 
			
		||||
            return False
 | 
			
		||||
        muc_user = message.getTag('x', namespace=nbxmpp.NS_MUC_USER)
 | 
			
		||||
        if muc_user is not None:
 | 
			
		||||
            return muc_user.getChildren() == []
 | 
			
		||||
        else:
 | 
			
		||||
            # muc#user namespace was added in MUC 1.28 so we need a fallback
 | 
			
		||||
            # Check if we know the jid, otherwise disco it
 | 
			
		||||
            if app.logger.jid_is_room_jid(with_.getStripped()):
 | 
			
		||||
                return True
 | 
			
		||||
            return False
 | 
			
		||||
 | 
			
		||||
    def _get_unique_id(self, result, message, groupchat, self_message, muc_pm):
 | 
			
		||||
        stanza_id = result.getAttr('id')
 | 
			
		||||
        if groupchat:
 | 
			
		||||
            return stanza_id, None
 | 
			
		||||
 | 
			
		||||
        origin_id = message.getOriginID()
 | 
			
		||||
        if self_message:
 | 
			
		||||
            return None, origin_id
 | 
			
		||||
 | 
			
		||||
        if muc_pm:
 | 
			
		||||
            return stanza_id, origin_id
 | 
			
		||||
 | 
			
		||||
        if self._con.get_own_jid().bareMatch(message.getFrom()):
 | 
			
		||||
            # message we sent
 | 
			
		||||
            return stanza_id, origin_id
 | 
			
		||||
 | 
			
		||||
        # A message we received
 | 
			
		||||
        return stanza_id, None
 | 
			
		||||
 | 
			
		||||
    def _mam_message_received(self, conn, stanza):
 | 
			
		||||
        app.nec.push_incoming_event(
 | 
			
		||||
            NetworkIncomingEvent('raw-mam-message-received',
 | 
			
		||||
                                 conn=self._con,
 | 
			
		||||
                                 stanza=stanza))
 | 
			
		||||
 | 
			
		||||
        result = stanza.getTag('result', protocol=True)
 | 
			
		||||
        queryid = result.getAttr('queryid')
 | 
			
		||||
        forwarded = result.getTag('forwarded',
 | 
			
		||||
                                  namespace=nbxmpp.NS_FORWARD,
 | 
			
		||||
                                  protocol=True)
 | 
			
		||||
        message = forwarded.getTag('message', protocol=True)
 | 
			
		||||
 | 
			
		||||
        groupchat = message.getType() == 'groupchat'
 | 
			
		||||
 | 
			
		||||
        archive_jid = self._from_valid_archive(stanza, message, groupchat)
 | 
			
		||||
        if archive_jid is None:
 | 
			
		||||
            log.warning('Message from invalid archive %s', stanza)
 | 
			
		||||
            raise nbxmpp.NodeProcessed
 | 
			
		||||
 | 
			
		||||
        log.info('Received message from archive: %s', archive_jid)
 | 
			
		||||
        if not self._is_valid_request(archive_jid, queryid):
 | 
			
		||||
            log.warning('Invalid MAM Message: unknown query id')
 | 
			
		||||
            log.debug(stanza)
 | 
			
		||||
            raise nbxmpp.NodeProcessed
 | 
			
		||||
 | 
			
		||||
        # Timestamp parsing
 | 
			
		||||
        timestamp = parse_delay(forwarded)
 | 
			
		||||
        if timestamp is None:
 | 
			
		||||
            raise nbxmpp.NodeProcessed
 | 
			
		||||
 | 
			
		||||
        user_timestamp = parse_delay(message)
 | 
			
		||||
 | 
			
		||||
        # Fix for self messaging
 | 
			
		||||
        if not groupchat:
 | 
			
		||||
            to = message.getTo()
 | 
			
		||||
            if to is None:
 | 
			
		||||
                # Some servers dont set the 'to' attribute when
 | 
			
		||||
                # we send a message to ourself
 | 
			
		||||
                message.setTo(self._con.get_own_jid())
 | 
			
		||||
 | 
			
		||||
        event_attrs = {}
 | 
			
		||||
 | 
			
		||||
        if groupchat:
 | 
			
		||||
            event_attrs.update(self._parse_gc_attrs(message))
 | 
			
		||||
        else:
 | 
			
		||||
            event_attrs.update(self._parse_chat_attrs(message))
 | 
			
		||||
 | 
			
		||||
        self_message = self._is_self_message(message, groupchat)
 | 
			
		||||
        muc_pm = self._is_muc_pm(message, groupchat, event_attrs['with_'])
 | 
			
		||||
 | 
			
		||||
        stanza_id, origin_id = self._get_unique_id(
 | 
			
		||||
            result, message, groupchat, self_message, muc_pm)
 | 
			
		||||
        message_id = message.getID()
 | 
			
		||||
 | 
			
		||||
        # Check for duplicates
 | 
			
		||||
        namespace = self.archiving_namespace
 | 
			
		||||
        if groupchat:
 | 
			
		||||
            namespace = muc_caps_cache.get_mam_namespace(
 | 
			
		||||
                archive_jid.getStripped())
 | 
			
		||||
 | 
			
		||||
        if namespace == nbxmpp.NS_MAM_2:
 | 
			
		||||
            # Search only with stanza-id for duplicates on mam:2
 | 
			
		||||
            if app.logger.find_stanza_id(self._account,
 | 
			
		||||
                                         archive_jid.getStripped(),
 | 
			
		||||
                                         stanza_id,
 | 
			
		||||
                                         origin_id,
 | 
			
		||||
                                         groupchat=groupchat):
 | 
			
		||||
                log.info('Found duplicate with stanza-id')
 | 
			
		||||
                raise nbxmpp.NodeProcessed
 | 
			
		||||
 | 
			
		||||
        msgtxt = message.getTagData('body')
 | 
			
		||||
 | 
			
		||||
        event_attrs.update(
 | 
			
		||||
            {'conn': self._con,
 | 
			
		||||
             'additional_data': {},
 | 
			
		||||
             'encrypted': False,
 | 
			
		||||
             'timestamp': timestamp,
 | 
			
		||||
             'user_timestamp': user_timestamp,
 | 
			
		||||
             'self_message': self_message,
 | 
			
		||||
             'groupchat': groupchat,
 | 
			
		||||
             'muc_pm': muc_pm,
 | 
			
		||||
             'stanza_id': stanza_id,
 | 
			
		||||
             'origin_id': origin_id,
 | 
			
		||||
             'message_id': message_id,
 | 
			
		||||
             'correct_id': None,
 | 
			
		||||
             'archive_jid': archive_jid,
 | 
			
		||||
             'msgtxt': msgtxt,
 | 
			
		||||
             'message': message,
 | 
			
		||||
             'namespace': namespace,
 | 
			
		||||
             })
 | 
			
		||||
 | 
			
		||||
        if groupchat:
 | 
			
		||||
            event = MamGcMessageReceivedEvent(None, **event_attrs)
 | 
			
		||||
        else:
 | 
			
		||||
            event = MamMessageReceivedEvent(None, **event_attrs)
 | 
			
		||||
 | 
			
		||||
        app.plugin_manager.extension_point(
 | 
			
		||||
            'decrypt', self._con, event, self._decryption_finished)
 | 
			
		||||
 | 
			
		||||
        if not event.encrypted:
 | 
			
		||||
            eme = parse_eme(event.message)
 | 
			
		||||
            if eme is not None:
 | 
			
		||||
                event.msgtxt = eme
 | 
			
		||||
            self._decryption_finished(event)
 | 
			
		||||
 | 
			
		||||
        raise nbxmpp.NodeProcessed
 | 
			
		||||
 | 
			
		||||
    def _parse_gc_attrs(self, message):
 | 
			
		||||
        with_ = message.getFrom()
 | 
			
		||||
        nick = message.getFrom().getResource()
 | 
			
		||||
 | 
			
		||||
        # Get the real jid if we have it
 | 
			
		||||
        real_jid = None
 | 
			
		||||
        muc_user = message.getTag('x', namespace=nbxmpp.NS_MUC_USER)
 | 
			
		||||
        if muc_user is not None:
 | 
			
		||||
            real_jid = muc_user.getTagAttr('item', 'jid')
 | 
			
		||||
            if real_jid is not None:
 | 
			
		||||
                real_jid = nbxmpp.JID(real_jid)
 | 
			
		||||
 | 
			
		||||
        return {'with_': with_,
 | 
			
		||||
                'nick': nick,
 | 
			
		||||
                'real_jid': real_jid,
 | 
			
		||||
                'kind': KindConstant.GC_MSG}
 | 
			
		||||
 | 
			
		||||
    def _parse_chat_attrs(self, message):
 | 
			
		||||
        frm = message.getFrom()
 | 
			
		||||
        to = message.getTo()
 | 
			
		||||
        if frm.bareMatch(self._con.get_own_jid()):
 | 
			
		||||
            with_ = to
 | 
			
		||||
            kind = KindConstant.CHAT_MSG_SENT
 | 
			
		||||
        else:
 | 
			
		||||
            with_ = frm
 | 
			
		||||
            kind = KindConstant.CHAT_MSG_RECV
 | 
			
		||||
 | 
			
		||||
        return {'with_': with_,
 | 
			
		||||
                'nick': None,
 | 
			
		||||
                'kind': kind}
 | 
			
		||||
 | 
			
		||||
    def _decryption_finished(self, event):
 | 
			
		||||
        if not event.msgtxt:
 | 
			
		||||
            # For example Chatstates, Receipts, Chatmarkers
 | 
			
		||||
            log.debug(event.message.getProperties())
 | 
			
		||||
            return
 | 
			
		||||
        log.debug(event.msgtxt)
 | 
			
		||||
 | 
			
		||||
        event.correct_id = parse_correction(event.message)
 | 
			
		||||
        parse_oob(event.message, event.additional_data)
 | 
			
		||||
 | 
			
		||||
        with_ = event.with_.getStripped()
 | 
			
		||||
        if event.muc_pm:
 | 
			
		||||
            # we store the message with the full JID
 | 
			
		||||
            with_ = str(event.with_)
 | 
			
		||||
 | 
			
		||||
        stanza_id = event.stanza_id
 | 
			
		||||
        if event.self_message:
 | 
			
		||||
            # Self messages can only be deduped with origin-id
 | 
			
		||||
            if event.origin_id is None:
 | 
			
		||||
                log.warning('Self message without origin-id found')
 | 
			
		||||
                return
 | 
			
		||||
            stanza_id = event.origin_id
 | 
			
		||||
 | 
			
		||||
        if event.namespace == nbxmpp.NS_MAM_1:
 | 
			
		||||
            if app.logger.search_for_duplicate(
 | 
			
		||||
                    self._account, with_, event.timestamp, event.msgtxt):
 | 
			
		||||
                log.info('Found duplicate with fallback for mam:1')
 | 
			
		||||
                return
 | 
			
		||||
 | 
			
		||||
        app.logger.insert_into_logs(self._account,
 | 
			
		||||
                                    with_,
 | 
			
		||||
                                    event.timestamp,
 | 
			
		||||
                                    event.kind,
 | 
			
		||||
                                    unread=False,
 | 
			
		||||
                                    message=event.msgtxt,
 | 
			
		||||
                                    contact_name=event.nick,
 | 
			
		||||
                                    additional_data=event.additional_data,
 | 
			
		||||
                                    stanza_id=stanza_id)
 | 
			
		||||
 | 
			
		||||
        app.nec.push_incoming_event(
 | 
			
		||||
            MamDecryptedMessageReceived(None, **vars(event)))
 | 
			
		||||
 | 
			
		||||
    def _is_valid_request(self, jid, query_id):
 | 
			
		||||
        if query_id is None:
 | 
			
		||||
            return False
 | 
			
		||||
 | 
			
		||||
        valid_id = self._mam_query_ids.get(jid.getStripped(), None)
 | 
			
		||||
        return valid_id == query_id
 | 
			
		||||
 | 
			
		||||
    def _get_query_id(self, jid):
 | 
			
		||||
        query_id = self._con.connection.getAnID()
 | 
			
		||||
        self._mam_query_ids[jid] = query_id
 | 
			
		||||
        return query_id
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def _parse_iq(stanza):
 | 
			
		||||
        if not nbxmpp.isResultNode(stanza):
 | 
			
		||||
            log.error('Error on MAM query: %s', stanza.getError())
 | 
			
		||||
            raise InvalidMamIQ
 | 
			
		||||
 | 
			
		||||
        fin = stanza.getTag('fin')
 | 
			
		||||
        if fin is None:
 | 
			
		||||
            log.error('Malformed MAM query result received: %s', stanza)
 | 
			
		||||
            raise InvalidMamIQ
 | 
			
		||||
 | 
			
		||||
        set_ = fin.getTag('set', namespace=nbxmpp.NS_RSM)
 | 
			
		||||
        if set_ is None:
 | 
			
		||||
            log.error(
 | 
			
		||||
                'Malformed MAM query result received (no "set" Node): %s',
 | 
			
		||||
                stanza)
 | 
			
		||||
            raise InvalidMamIQ
 | 
			
		||||
        return fin, set_
 | 
			
		||||
 | 
			
		||||
    def _get_from_jid(self, stanza):
 | 
			
		||||
        jid = stanza.getFrom()
 | 
			
		||||
        if jid is None:
 | 
			
		||||
            # No from means, iq from our own archive
 | 
			
		||||
            jid = self._con.get_own_jid().getStripped()
 | 
			
		||||
        else:
 | 
			
		||||
            jid = jid.getStripped()
 | 
			
		||||
        return jid
 | 
			
		||||
 | 
			
		||||
    def request_archive_count(self, start_date, end_date):
 | 
			
		||||
        jid = self._con.get_own_jid().getStripped()
 | 
			
		||||
        log.info('Request archive count from: %s', jid)
 | 
			
		||||
        query_id = self._get_query_id(jid)
 | 
			
		||||
        query = self._get_archive_query(
 | 
			
		||||
            query_id, start=start_date, end=end_date, max_=0)
 | 
			
		||||
        self._con.connection.SendAndCallForResponse(
 | 
			
		||||
            query, self._received_count, {'query_id': query_id})
 | 
			
		||||
        return query_id
 | 
			
		||||
 | 
			
		||||
    def _received_count(self, conn, stanza, query_id):
 | 
			
		||||
        try:
 | 
			
		||||
            _, set_ = self._parse_iq(stanza)
 | 
			
		||||
        except InvalidMamIQ:
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        jid = self._get_from_jid(stanza)
 | 
			
		||||
        self._mam_query_ids.pop(jid)
 | 
			
		||||
 | 
			
		||||
        count = set_.getTagData('count')
 | 
			
		||||
        log.info('Received archive count: %s', count)
 | 
			
		||||
        app.nec.push_incoming_event(ArchivingCountReceived(
 | 
			
		||||
            None, query_id=query_id, count=count))
 | 
			
		||||
 | 
			
		||||
    def request_archive_on_signin(self):
 | 
			
		||||
        own_jid = self._con.get_own_jid().getStripped()
 | 
			
		||||
 | 
			
		||||
        if own_jid in self._mam_query_ids:
 | 
			
		||||
            log.warning('MAM request for %s already running', own_jid)
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        archive = app.logger.get_archive_timestamp(own_jid)
 | 
			
		||||
 | 
			
		||||
        # Migration of last_mam_id from config to DB
 | 
			
		||||
        if archive is not None:
 | 
			
		||||
            mam_id = archive.last_mam_id
 | 
			
		||||
        else:
 | 
			
		||||
            mam_id = app.config.get_per(
 | 
			
		||||
                'accounts', self._account, 'last_mam_id')
 | 
			
		||||
            if mam_id:
 | 
			
		||||
                app.config.del_per('accounts', self._account, 'last_mam_id')
 | 
			
		||||
 | 
			
		||||
        start_date = None
 | 
			
		||||
        query_id = self._get_query_id(own_jid)
 | 
			
		||||
        if mam_id:
 | 
			
		||||
            log.info('MAM query after: %s', mam_id)
 | 
			
		||||
            query = self._get_archive_query(query_id, after=mam_id)
 | 
			
		||||
        else:
 | 
			
		||||
            # First Start, we request the last week
 | 
			
		||||
            start_date = datetime.utcnow() - timedelta(days=7)
 | 
			
		||||
            log.info('First start: query archive start: %s', start_date)
 | 
			
		||||
            query = self._get_archive_query(query_id, start=start_date)
 | 
			
		||||
        self._send_archive_query(query, query_id, start_date)
 | 
			
		||||
 | 
			
		||||
    def request_archive_on_muc_join(self, jid):
 | 
			
		||||
        archive = app.logger.get_archive_timestamp(
 | 
			
		||||
            jid, type_=JIDConstant.ROOM_TYPE)
 | 
			
		||||
        query_id = self._get_query_id(jid)
 | 
			
		||||
        start_date = None
 | 
			
		||||
        if archive is not None:
 | 
			
		||||
            log.info('Request from archive %s after %s:',
 | 
			
		||||
                     jid, archive.last_mam_id)
 | 
			
		||||
            query = self._get_archive_query(
 | 
			
		||||
                query_id, jid=jid, after=archive.last_mam_id)
 | 
			
		||||
        else:
 | 
			
		||||
            # First Start, we dont request history
 | 
			
		||||
            # Depending on what a MUC saves, there could be thousands
 | 
			
		||||
            # of Messages even in just one day.
 | 
			
		||||
            start_date = datetime.utcnow() - timedelta(days=1)
 | 
			
		||||
            log.info('First join: query archive %s from: %s', jid, start_date)
 | 
			
		||||
            query = self._get_archive_query(query_id, jid=jid, start=start_date)
 | 
			
		||||
        self._send_archive_query(query, query_id, start_date, groupchat=True)
 | 
			
		||||
 | 
			
		||||
    def _send_archive_query(self, query, query_id, start_date=None,
 | 
			
		||||
                            groupchat=False):
 | 
			
		||||
        self._con.connection.SendAndCallForResponse(
 | 
			
		||||
            query, self._result_finished, {'query_id': query_id,
 | 
			
		||||
                                           'start_date': start_date,
 | 
			
		||||
                                           'groupchat': groupchat})
 | 
			
		||||
 | 
			
		||||
    def _result_finished(self, conn, stanza, query_id, start_date, groupchat):
 | 
			
		||||
        try:
 | 
			
		||||
            fin, set_ = self._parse_iq(stanza)
 | 
			
		||||
        except InvalidMamIQ:
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        jid = self._get_from_jid(stanza)
 | 
			
		||||
 | 
			
		||||
        last = set_.getTagData('last')
 | 
			
		||||
        if last is None:
 | 
			
		||||
            log.info('End of MAM query, no items retrieved')
 | 
			
		||||
            self._mam_query_ids.pop(jid)
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        complete = fin.getAttr('complete')
 | 
			
		||||
        app.logger.set_archive_timestamp(jid, last_mam_id=last)
 | 
			
		||||
        if complete != 'true':
 | 
			
		||||
            self._mam_query_ids.pop(jid)
 | 
			
		||||
            query_id = self._get_query_id(jid)
 | 
			
		||||
            query = self._get_archive_query(query_id, jid=jid, after=last)
 | 
			
		||||
            self._send_archive_query(query, query_id, groupchat=groupchat)
 | 
			
		||||
        else:
 | 
			
		||||
            self._mam_query_ids.pop(jid)
 | 
			
		||||
            if start_date is not None:
 | 
			
		||||
                app.logger.set_archive_timestamp(
 | 
			
		||||
                    jid,
 | 
			
		||||
                    last_mam_id=last,
 | 
			
		||||
                    oldest_mam_timestamp=start_date.timestamp())
 | 
			
		||||
            log.info('End of MAM query, last mam id: %s', last)
 | 
			
		||||
 | 
			
		||||
    def request_archive_interval(self, start_date, end_date, after=None,
 | 
			
		||||
                                 query_id=None):
 | 
			
		||||
        jid = self._con.get_own_jid().getStripped()
 | 
			
		||||
        if after is None:
 | 
			
		||||
            log.info('Request intervall from %s to %s from %s',
 | 
			
		||||
                     start_date, end_date, jid)
 | 
			
		||||
        else:
 | 
			
		||||
            log.info('Query page after %s from %s',
 | 
			
		||||
                     after, jid)
 | 
			
		||||
        if query_id is None:
 | 
			
		||||
            query_id = self._get_query_id(jid)
 | 
			
		||||
        self._mam_query_ids[jid] = query_id
 | 
			
		||||
        query = self._get_archive_query(query_id, start=start_date,
 | 
			
		||||
                                        end=end_date, after=after, max_=30)
 | 
			
		||||
 | 
			
		||||
        self._con.connection.SendAndCallForResponse(
 | 
			
		||||
            query, self._intervall_result, {'query_id': query_id,
 | 
			
		||||
                                            'start_date': start_date,
 | 
			
		||||
                                            'end_date': end_date})
 | 
			
		||||
        return query_id
 | 
			
		||||
 | 
			
		||||
    def _intervall_result(self, conn, stanza, query_id,
 | 
			
		||||
                          start_date, end_date):
 | 
			
		||||
        try:
 | 
			
		||||
            fin, set_ = self._parse_iq(stanza)
 | 
			
		||||
        except InvalidMamIQ:
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        jid = self._get_from_jid(stanza)
 | 
			
		||||
        self._mam_query_ids.pop(jid)
 | 
			
		||||
        if start_date:
 | 
			
		||||
            timestamp = start_date.timestamp()
 | 
			
		||||
        else:
 | 
			
		||||
            timestamp = ArchiveState.ALL
 | 
			
		||||
 | 
			
		||||
        last = set_.getTagData('last')
 | 
			
		||||
        if last is None:
 | 
			
		||||
            app.nec.push_incoming_event(ArchivingIntervalFinished(
 | 
			
		||||
                None, query_id=query_id))
 | 
			
		||||
            app.logger.set_archive_timestamp(
 | 
			
		||||
                jid, oldest_mam_timestamp=timestamp)
 | 
			
		||||
            log.info('End of MAM request, no items retrieved')
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        complete = fin.getAttr('complete')
 | 
			
		||||
        if complete != 'true':
 | 
			
		||||
            self.request_archive_interval(start_date, end_date, last, query_id)
 | 
			
		||||
        else:
 | 
			
		||||
            log.info('Request finished')
 | 
			
		||||
            app.logger.set_archive_timestamp(
 | 
			
		||||
                jid, oldest_mam_timestamp=timestamp)
 | 
			
		||||
            app.nec.push_incoming_event(ArchivingIntervalFinished(
 | 
			
		||||
                None, query_id=query_id))
 | 
			
		||||
 | 
			
		||||
    def _get_archive_query(self, query_id, jid=None, start=None, end=None,
 | 
			
		||||
                           with_=None, after=None, max_=30):
 | 
			
		||||
        # Muc archive query?
 | 
			
		||||
        namespace = muc_caps_cache.get_mam_namespace(jid)
 | 
			
		||||
        if namespace is None:
 | 
			
		||||
            # Query to our own archive
 | 
			
		||||
            namespace = self.archiving_namespace
 | 
			
		||||
 | 
			
		||||
        iq = nbxmpp.Iq('set', to=jid)
 | 
			
		||||
        query = iq.addChild('query', namespace=namespace)
 | 
			
		||||
        form = query.addChild(node=nbxmpp.DataForm(typ='submit'))
 | 
			
		||||
        field = nbxmpp.DataField(typ='hidden',
 | 
			
		||||
                                 name='FORM_TYPE',
 | 
			
		||||
                                 value=namespace)
 | 
			
		||||
        form.addChild(node=field)
 | 
			
		||||
        if start:
 | 
			
		||||
            field = nbxmpp.DataField(typ='text-single',
 | 
			
		||||
                                     name='start',
 | 
			
		||||
                                     value=start.strftime('%Y-%m-%dT%H:%M:%SZ'))
 | 
			
		||||
            form.addChild(node=field)
 | 
			
		||||
        if end:
 | 
			
		||||
            field = nbxmpp.DataField(typ='text-single',
 | 
			
		||||
                                     name='end',
 | 
			
		||||
                                     value=end.strftime('%Y-%m-%dT%H:%M:%SZ'))
 | 
			
		||||
            form.addChild(node=field)
 | 
			
		||||
        if with_:
 | 
			
		||||
            field = nbxmpp.DataField(typ='jid-single', name='with', value=with_)
 | 
			
		||||
            form.addChild(node=field)
 | 
			
		||||
 | 
			
		||||
        set_ = query.setTag('set', namespace=nbxmpp.NS_RSM)
 | 
			
		||||
        set_.setTagData('max', max_)
 | 
			
		||||
        if after:
 | 
			
		||||
            set_.setTagData('after', after)
 | 
			
		||||
        query.setAttr('queryid', query_id)
 | 
			
		||||
        return iq
 | 
			
		||||
 | 
			
		||||
    def request_mam_preferences(self):
 | 
			
		||||
        log.info('Request MAM preferences')
 | 
			
		||||
        iq = nbxmpp.Iq('get', self.archiving_namespace)
 | 
			
		||||
        iq.setQuery('prefs')
 | 
			
		||||
        self._con.connection.SendAndCallForResponse(
 | 
			
		||||
            iq, self._preferences_received)
 | 
			
		||||
 | 
			
		||||
    def _preferences_received(self, stanza):
 | 
			
		||||
        if not nbxmpp.isResultNode(stanza):
 | 
			
		||||
            log.info('Error: %s', stanza.getError())
 | 
			
		||||
            app.nec.push_incoming_event(MAMPreferenceError(
 | 
			
		||||
                None, conn=self._con, error=stanza.getError()))
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        log.info('Received MAM preferences')
 | 
			
		||||
        prefs = stanza.getTag('prefs', namespace=self.archiving_namespace)
 | 
			
		||||
        if prefs is None:
 | 
			
		||||
            log.error('Malformed stanza (no prefs node): %s', stanza)
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        rules = []
 | 
			
		||||
        default = prefs.getAttr('default')
 | 
			
		||||
        for item in prefs.getTag('always').getTags('jid'):
 | 
			
		||||
            rules.append((item.getData(), 'Always'))
 | 
			
		||||
 | 
			
		||||
        for item in prefs.getTag('never').getTags('jid'):
 | 
			
		||||
            rules.append((item.getData(), 'Never'))
 | 
			
		||||
 | 
			
		||||
        app.nec.push_incoming_event(MAMPreferenceReceived(
 | 
			
		||||
            None, conn=self._con, rules=rules, default=default))
 | 
			
		||||
 | 
			
		||||
    def set_mam_preferences(self, rules, default):
 | 
			
		||||
        iq = nbxmpp.Iq(typ='set')
 | 
			
		||||
        prefs = iq.addChild(name='prefs',
 | 
			
		||||
                            namespace=self.archiving_namespace,
 | 
			
		||||
                            attrs={'default': default})
 | 
			
		||||
        always = prefs.addChild(name='always')
 | 
			
		||||
        never = prefs.addChild(name='never')
 | 
			
		||||
        for item in rules:
 | 
			
		||||
            jid, archive = item
 | 
			
		||||
            if archive:
 | 
			
		||||
                always.addChild(name='jid').setData(jid)
 | 
			
		||||
            else:
 | 
			
		||||
                never.addChild(name='jid').setData(jid)
 | 
			
		||||
 | 
			
		||||
        self._con.connection.SendAndCallForResponse(
 | 
			
		||||
            iq, self._preferences_saved)
 | 
			
		||||
 | 
			
		||||
    def _preferences_saved(self, stanza):
 | 
			
		||||
        if not nbxmpp.isResultNode(stanza):
 | 
			
		||||
            log.info('Error: %s', stanza.getError())
 | 
			
		||||
            app.nec.push_incoming_event(MAMPreferenceError(
 | 
			
		||||
                None, conn=self._con, error=stanza.getError()))
 | 
			
		||||
        else:
 | 
			
		||||
            log.info('Preferences saved')
 | 
			
		||||
            app.nec.push_incoming_event(
 | 
			
		||||
                MAMPreferenceSaved(None, conn=self._con))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class MamMessageReceivedEvent(NetworkIncomingEvent):
 | 
			
		||||
    name = 'mam-message-received'
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class MamGcMessageReceivedEvent(NetworkIncomingEvent):
 | 
			
		||||
    name = 'mam-message-received'
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class MamDecryptedMessageReceived(NetworkIncomingEvent):
 | 
			
		||||
    name = 'mam-decrypted-message-received'
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class MAMPreferenceError(NetworkIncomingEvent):
 | 
			
		||||
    name = 'mam-prefs-error'
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class MAMPreferenceReceived(NetworkIncomingEvent):
 | 
			
		||||
    name = 'mam-prefs-received'
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class MAMPreferenceSaved(NetworkIncomingEvent):
 | 
			
		||||
    name = 'mam-prefs-saved'
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ArchivingCountReceived(NetworkIncomingEvent):
 | 
			
		||||
    name = 'archiving-count-received'
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ArchivingIntervalFinished(NetworkIncomingEvent):
 | 
			
		||||
    name = 'archiving-interval-finished'
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ArchivingErrorReceived(NetworkIncomingEvent):
 | 
			
		||||
    name = 'archiving-error-received'
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class InvalidMamIQ(Exception):
 | 
			
		||||
    pass
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_instance(*args, **kwargs):
 | 
			
		||||
    return MAM(*args, **kwargs), 'MAM'
 | 
			
		||||
							
								
								
									
										113
									
								
								gajim/common/modules/misc.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										113
									
								
								gajim/common/modules/misc.py
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,113 @@
 | 
			
		|||
# This file is part of Gajim.
 | 
			
		||||
#
 | 
			
		||||
# Gajim is free software; you can redistribute it and/or modify
 | 
			
		||||
# it under the terms of the GNU General Public License as published
 | 
			
		||||
# by the Free Software Foundation; version 3 only.
 | 
			
		||||
#
 | 
			
		||||
# Gajim is distributed in the hope that it will be useful,
 | 
			
		||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
			
		||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 | 
			
		||||
# GNU General Public License for more details.
 | 
			
		||||
#
 | 
			
		||||
# You should have received a copy of the GNU General Public License
 | 
			
		||||
# along with Gajim. If not, see <http://www.gnu.org/licenses/>.
 | 
			
		||||
 | 
			
		||||
# All XEPs that dont need their own module
 | 
			
		||||
 | 
			
		||||
import logging
 | 
			
		||||
 | 
			
		||||
import nbxmpp
 | 
			
		||||
 | 
			
		||||
from gajim.common.modules.date_and_time import parse_datetime
 | 
			
		||||
 | 
			
		||||
log = logging.getLogger('gajim.c.m.misc')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# XEP-0380: Explicit Message Encryption
 | 
			
		||||
 | 
			
		||||
_eme_namespaces = {
 | 
			
		||||
    'urn:xmpp:otr:0':
 | 
			
		||||
        _('This message was encrypted with OTR '
 | 
			
		||||
          'and could not be decrypted.'),
 | 
			
		||||
    'jabber:x:encrypted':
 | 
			
		||||
        _('This message was encrypted with Legacy '
 | 
			
		||||
          'OpenPGP and could not be decrypted. You can install '
 | 
			
		||||
          'the PGP plugin to handle those messages.'),
 | 
			
		||||
    'urn:xmpp:openpgp:0':
 | 
			
		||||
        _('This message was encrypted with '
 | 
			
		||||
          'OpenPGP for XMPP and could not be decrypted.'),
 | 
			
		||||
    'fallback':
 | 
			
		||||
        _('This message was encrypted with %s '
 | 
			
		||||
          'and could not be decrypted.')
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def parse_eme(stanza):
 | 
			
		||||
    enc_tag = stanza.getTag('encryption', namespace=nbxmpp.NS_EME)
 | 
			
		||||
    if enc_tag is None:
 | 
			
		||||
        return
 | 
			
		||||
 | 
			
		||||
    ns = enc_tag.getAttr('namespace')
 | 
			
		||||
    if ns is None:
 | 
			
		||||
        log.warning('No namespace on EME message')
 | 
			
		||||
        return
 | 
			
		||||
 | 
			
		||||
    if ns in _eme_namespaces:
 | 
			
		||||
        log.info('Found not decrypted message: %s', ns)
 | 
			
		||||
        return _eme_namespaces.get(ns)
 | 
			
		||||
 | 
			
		||||
    enc_name = enc_tag.getAttr('name')
 | 
			
		||||
    log.info('Found not decrypted message: %s', enc_name or ns)
 | 
			
		||||
    return _eme_namespaces.get('fallback') % enc_name or ns
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# XEP-0203: Delayed Delivery
 | 
			
		||||
 | 
			
		||||
def parse_delay(stanza, epoch=True, convert='utc'):
 | 
			
		||||
    timestamp = None
 | 
			
		||||
    delay = stanza.getTagAttr(
 | 
			
		||||
        'delay', 'stamp', namespace=nbxmpp.NS_DELAY2)
 | 
			
		||||
    if delay is not None:
 | 
			
		||||
        timestamp = parse_datetime(delay, check_utc=True,
 | 
			
		||||
                                   epoch=epoch, convert=convert)
 | 
			
		||||
        if timestamp is None:
 | 
			
		||||
            log.warning('Invalid timestamp received: %s', delay)
 | 
			
		||||
            log.warning(stanza)
 | 
			
		||||
 | 
			
		||||
    return timestamp
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# XEP-0066: Out of Band Data
 | 
			
		||||
 | 
			
		||||
def parse_oob(stanza, dict_=None, key='Gajim'):
 | 
			
		||||
    oob_node = stanza.getTag('x', namespace=nbxmpp.NS_X_OOB)
 | 
			
		||||
    if oob_node is None:
 | 
			
		||||
        return
 | 
			
		||||
    result = {}
 | 
			
		||||
    url = oob_node.getTagData('url')
 | 
			
		||||
    if url is not None:
 | 
			
		||||
        result['oob_url'] = url
 | 
			
		||||
    desc = oob_node.getTagData('desc')
 | 
			
		||||
    if desc is not None:
 | 
			
		||||
        result['oob_desc'] = desc
 | 
			
		||||
 | 
			
		||||
    if dict_ is None:
 | 
			
		||||
        return result
 | 
			
		||||
 | 
			
		||||
    if key in dict_:
 | 
			
		||||
        dict_[key] += result
 | 
			
		||||
    else:
 | 
			
		||||
        dict_[key] = result
 | 
			
		||||
 | 
			
		||||
    return dict_
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# XEP-0308: Last Message Correction
 | 
			
		||||
 | 
			
		||||
def parse_correction(stanza):
 | 
			
		||||
    replace = stanza.getTag('replace', namespace=nbxmpp.NS_CORRECT)
 | 
			
		||||
    if replace is not None:
 | 
			
		||||
        id_ = replace.getAttr('id')
 | 
			
		||||
        if id_ is not None:
 | 
			
		||||
            return id_
 | 
			
		||||
        log.warning('No id attr found: %s' % stanza)
 | 
			
		||||
| 
						 | 
				
			
			@ -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>
 | 
			
		||||
							
								
								
									
										236
									
								
								gajim/data/gui/mam_preferences.ui
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										236
									
								
								gajim/data/gui/mam_preferences.ui
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,236 @@
 | 
			
		|||
<?xml version="1.0" encoding="UTF-8"?>
 | 
			
		||||
<!-- Generated with glade 3.22.1 -->
 | 
			
		||||
<interface>
 | 
			
		||||
  <requires lib="gtk+" version="3.12"/>
 | 
			
		||||
  <object class="GtkListStore" id="default_store">
 | 
			
		||||
    <columns>
 | 
			
		||||
      <!-- column-name text -->
 | 
			
		||||
      <column type="gchararray"/>
 | 
			
		||||
      <!-- column-name value -->
 | 
			
		||||
      <column type="gchararray"/>
 | 
			
		||||
    </columns>
 | 
			
		||||
    <data>
 | 
			
		||||
      <row>
 | 
			
		||||
        <col id="0" translatable="yes">Always</col>
 | 
			
		||||
        <col id="1">always</col>
 | 
			
		||||
      </row>
 | 
			
		||||
      <row>
 | 
			
		||||
        <col id="0" translatable="yes">Roster</col>
 | 
			
		||||
        <col id="1">roster</col>
 | 
			
		||||
      </row>
 | 
			
		||||
      <row>
 | 
			
		||||
        <col id="0" translatable="yes">Never</col>
 | 
			
		||||
        <col id="1">never</col>
 | 
			
		||||
      </row>
 | 
			
		||||
    </data>
 | 
			
		||||
  </object>
 | 
			
		||||
  <object class="GtkListStore" id="preferences_store">
 | 
			
		||||
    <columns>
 | 
			
		||||
      <!-- column-name jid -->
 | 
			
		||||
      <column type="gchararray"/>
 | 
			
		||||
      <!-- column-name gboolean1 -->
 | 
			
		||||
      <column type="gboolean"/>
 | 
			
		||||
    </columns>
 | 
			
		||||
  </object>
 | 
			
		||||
  <object class="GtkGrid" id="preferences_grid">
 | 
			
		||||
    <property name="width_request">400</property>
 | 
			
		||||
    <property name="height_request">300</property>
 | 
			
		||||
    <property name="visible">True</property>
 | 
			
		||||
    <property name="can_focus">False</property>
 | 
			
		||||
    <property name="margin_left">18</property>
 | 
			
		||||
    <property name="margin_right">18</property>
 | 
			
		||||
    <property name="margin_top">18</property>
 | 
			
		||||
    <property name="margin_bottom">18</property>
 | 
			
		||||
    <property name="row_spacing">5</property>
 | 
			
		||||
    <property name="column_spacing">10</property>
 | 
			
		||||
    <child>
 | 
			
		||||
      <object class="GtkButtonBox">
 | 
			
		||||
        <property name="visible">True</property>
 | 
			
		||||
        <property name="can_focus">False</property>
 | 
			
		||||
        <property name="halign">start</property>
 | 
			
		||||
        <property name="spacing">5</property>
 | 
			
		||||
        <property name="layout_style">start</property>
 | 
			
		||||
        <child>
 | 
			
		||||
          <object class="GtkButton" id="add_button">
 | 
			
		||||
            <property name="visible">True</property>
 | 
			
		||||
            <property name="can_focus">True</property>
 | 
			
		||||
            <property name="receives_default">True</property>
 | 
			
		||||
            <signal name="clicked" handler="_on_add" swapped="no"/>
 | 
			
		||||
            <child>
 | 
			
		||||
              <object class="GtkImage">
 | 
			
		||||
                <property name="visible">True</property>
 | 
			
		||||
                <property name="can_focus">False</property>
 | 
			
		||||
                <property name="icon_name">list-add-symbolic</property>
 | 
			
		||||
              </object>
 | 
			
		||||
            </child>
 | 
			
		||||
          </object>
 | 
			
		||||
          <packing>
 | 
			
		||||
            <property name="expand">False</property>
 | 
			
		||||
            <property name="fill">True</property>
 | 
			
		||||
            <property name="position">0</property>
 | 
			
		||||
            <property name="non_homogeneous">True</property>
 | 
			
		||||
          </packing>
 | 
			
		||||
        </child>
 | 
			
		||||
        <child>
 | 
			
		||||
          <object class="GtkButton" id="remove_button">
 | 
			
		||||
            <property name="visible">True</property>
 | 
			
		||||
            <property name="can_focus">True</property>
 | 
			
		||||
            <property name="receives_default">True</property>
 | 
			
		||||
            <signal name="clicked" handler="_on_remove" swapped="no"/>
 | 
			
		||||
            <child>
 | 
			
		||||
              <object class="GtkImage">
 | 
			
		||||
                <property name="visible">True</property>
 | 
			
		||||
                <property name="can_focus">False</property>
 | 
			
		||||
                <property name="icon_name">list-remove-symbolic</property>
 | 
			
		||||
              </object>
 | 
			
		||||
            </child>
 | 
			
		||||
          </object>
 | 
			
		||||
          <packing>
 | 
			
		||||
            <property name="expand">True</property>
 | 
			
		||||
            <property name="fill">True</property>
 | 
			
		||||
            <property name="position">1</property>
 | 
			
		||||
            <property name="non_homogeneous">True</property>
 | 
			
		||||
          </packing>
 | 
			
		||||
        </child>
 | 
			
		||||
      </object>
 | 
			
		||||
      <packing>
 | 
			
		||||
        <property name="left_attach">0</property>
 | 
			
		||||
        <property name="top_attach">2</property>
 | 
			
		||||
      </packing>
 | 
			
		||||
    </child>
 | 
			
		||||
    <child>
 | 
			
		||||
      <object class="GtkGrid">
 | 
			
		||||
        <property name="visible">True</property>
 | 
			
		||||
        <property name="can_focus">False</property>
 | 
			
		||||
        <property name="halign">start</property>
 | 
			
		||||
        <property name="column_spacing">5</property>
 | 
			
		||||
        <child>
 | 
			
		||||
          <object class="GtkLabel" id="default_label">
 | 
			
		||||
            <property name="visible">True</property>
 | 
			
		||||
            <property name="can_focus">False</property>
 | 
			
		||||
            <property name="label" translatable="yes">Default:</property>
 | 
			
		||||
            <property name="xalign">0</property>
 | 
			
		||||
          </object>
 | 
			
		||||
          <packing>
 | 
			
		||||
            <property name="left_attach">0</property>
 | 
			
		||||
            <property name="top_attach">0</property>
 | 
			
		||||
          </packing>
 | 
			
		||||
        </child>
 | 
			
		||||
        <child>
 | 
			
		||||
          <object class="GtkComboBox" id="default_cb">
 | 
			
		||||
            <property name="visible">True</property>
 | 
			
		||||
            <property name="can_focus">False</property>
 | 
			
		||||
            <property name="halign">start</property>
 | 
			
		||||
            <property name="hexpand">False</property>
 | 
			
		||||
            <property name="model">default_store</property>
 | 
			
		||||
            <property name="active">0</property>
 | 
			
		||||
            <property name="id_column">1</property>
 | 
			
		||||
            <child>
 | 
			
		||||
              <object class="GtkCellRendererText" id="cellrenderertext1"/>
 | 
			
		||||
              <attributes>
 | 
			
		||||
                <attribute name="text">0</attribute>
 | 
			
		||||
              </attributes>
 | 
			
		||||
            </child>
 | 
			
		||||
          </object>
 | 
			
		||||
          <packing>
 | 
			
		||||
            <property name="left_attach">1</property>
 | 
			
		||||
            <property name="top_attach">0</property>
 | 
			
		||||
          </packing>
 | 
			
		||||
        </child>
 | 
			
		||||
      </object>
 | 
			
		||||
      <packing>
 | 
			
		||||
        <property name="left_attach">0</property>
 | 
			
		||||
        <property name="top_attach">0</property>
 | 
			
		||||
      </packing>
 | 
			
		||||
    </child>
 | 
			
		||||
    <child>
 | 
			
		||||
      <object class="GtkButton" id="save_button">
 | 
			
		||||
        <property name="label">Save</property>
 | 
			
		||||
        <property name="visible">True</property>
 | 
			
		||||
        <property name="can_focus">True</property>
 | 
			
		||||
        <property name="receives_default">True</property>
 | 
			
		||||
        <property name="halign">end</property>
 | 
			
		||||
        <property name="always_show_image">True</property>
 | 
			
		||||
        <signal name="clicked" handler="_on_save" swapped="no"/>
 | 
			
		||||
      </object>
 | 
			
		||||
      <packing>
 | 
			
		||||
        <property name="left_attach">1</property>
 | 
			
		||||
        <property name="top_attach">2</property>
 | 
			
		||||
      </packing>
 | 
			
		||||
    </child>
 | 
			
		||||
    <child>
 | 
			
		||||
      <object class="GtkOverlay" id="overlay">
 | 
			
		||||
        <property name="visible">True</property>
 | 
			
		||||
        <property name="can_focus">False</property>
 | 
			
		||||
        <child>
 | 
			
		||||
          <object class="GtkScrolledWindow">
 | 
			
		||||
            <property name="height_request">150</property>
 | 
			
		||||
            <property name="visible">True</property>
 | 
			
		||||
            <property name="can_focus">True</property>
 | 
			
		||||
            <property name="hexpand">True</property>
 | 
			
		||||
            <property name="vexpand">True</property>
 | 
			
		||||
            <property name="shadow_type">in</property>
 | 
			
		||||
            <child>
 | 
			
		||||
              <object class="GtkTreeView" id="pref_view">
 | 
			
		||||
                <property name="visible">True</property>
 | 
			
		||||
                <property name="can_focus">True</property>
 | 
			
		||||
                <property name="model">preferences_store</property>
 | 
			
		||||
                <property name="search_column">0</property>
 | 
			
		||||
                <child internal-child="selection">
 | 
			
		||||
                  <object class="GtkTreeSelection" id="treeview-selection2"/>
 | 
			
		||||
                </child>
 | 
			
		||||
                <child>
 | 
			
		||||
                  <object class="GtkTreeViewColumn" id="treeviewcolumn1">
 | 
			
		||||
                    <property name="title" translatable="yes">Jabber ID</property>
 | 
			
		||||
                    <property name="expand">True</property>
 | 
			
		||||
                    <property name="clickable">True</property>
 | 
			
		||||
                    <property name="sort_indicator">True</property>
 | 
			
		||||
                    <property name="sort_column_id">0</property>
 | 
			
		||||
                    <child>
 | 
			
		||||
                      <object class="GtkCellRendererText" id="cellrenderertext3">
 | 
			
		||||
                        <property name="editable">True</property>
 | 
			
		||||
                        <property name="placeholder_text">user@example.org</property>
 | 
			
		||||
                        <signal name="edited" handler="_jid_edited" swapped="no"/>
 | 
			
		||||
                      </object>
 | 
			
		||||
                      <attributes>
 | 
			
		||||
                        <attribute name="text">0</attribute>
 | 
			
		||||
                      </attributes>
 | 
			
		||||
                    </child>
 | 
			
		||||
                  </object>
 | 
			
		||||
                </child>
 | 
			
		||||
                <child>
 | 
			
		||||
                  <object class="GtkTreeViewColumn" id="treeviewcolumn2">
 | 
			
		||||
                    <property name="title" translatable="yes">Archive</property>
 | 
			
		||||
                    <property name="clickable">True</property>
 | 
			
		||||
                    <property name="alignment">0.5</property>
 | 
			
		||||
                    <property name="sort_indicator">True</property>
 | 
			
		||||
                    <property name="sort_column_id">1</property>
 | 
			
		||||
                    <child>
 | 
			
		||||
                      <object class="GtkCellRendererToggle">
 | 
			
		||||
                        <signal name="toggled" handler="_pref_toggled" swapped="no"/>
 | 
			
		||||
                      </object>
 | 
			
		||||
                      <attributes>
 | 
			
		||||
                        <attribute name="active">1</attribute>
 | 
			
		||||
                      </attributes>
 | 
			
		||||
                    </child>
 | 
			
		||||
                  </object>
 | 
			
		||||
                </child>
 | 
			
		||||
              </object>
 | 
			
		||||
            </child>
 | 
			
		||||
          </object>
 | 
			
		||||
          <packing>
 | 
			
		||||
            <property name="index">-1</property>
 | 
			
		||||
          </packing>
 | 
			
		||||
        </child>
 | 
			
		||||
      </object>
 | 
			
		||||
      <packing>
 | 
			
		||||
        <property name="left_attach">0</property>
 | 
			
		||||
        <property name="top_attach">1</property>
 | 
			
		||||
        <property name="width">2</property>
 | 
			
		||||
      </packing>
 | 
			
		||||
    </child>
 | 
			
		||||
    <child>
 | 
			
		||||
      <placeholder/>
 | 
			
		||||
    </child>
 | 
			
		||||
  </object>
 | 
			
		||||
</interface>
 | 
			
		||||
							
								
								
									
										164
									
								
								gajim/dialogs.py
									
										
									
									
									
								
							
							
						
						
									
										164
									
								
								gajim/dialogs.py
									
										
									
									
									
								
							| 
						 | 
				
			
			@ -3871,170 +3871,6 @@ class RosterItemExchangeWindow:
 | 
			
		|||
        self.window.destroy()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Archiving313PreferencesWindow:
 | 
			
		||||
 | 
			
		||||
    default_dict = {'always': 0, 'roster': 1, 'never': 2}
 | 
			
		||||
    default_dict_cb = {0: 'always', 1: 'roster', 2: 'never'}
 | 
			
		||||
 | 
			
		||||
    def __init__(self, account):
 | 
			
		||||
        self.account = account
 | 
			
		||||
        self.idle_id = None
 | 
			
		||||
 | 
			
		||||
        # Connect to glade
 | 
			
		||||
        self.xml = gtkgui_helpers.get_gtk_builder(
 | 
			
		||||
            'archiving_313_preferences_window.ui')
 | 
			
		||||
        self.window = self.xml.get_object('archiving_313_pref')
 | 
			
		||||
 | 
			
		||||
        # Add Widgets
 | 
			
		||||
        for widget in ('archive_items_liststore', 'default_cb'):
 | 
			
		||||
            setattr(self, widget, self.xml.get_object(widget))
 | 
			
		||||
 | 
			
		||||
        self.window.set_title(_('Archiving Preferences for %s') % self.account)
 | 
			
		||||
 | 
			
		||||
        app.ged.register_event_handler(
 | 
			
		||||
            'archiving-313-preferences-changed-received', ged.GUI1,
 | 
			
		||||
            self._nec_archiving_313_changed_received)
 | 
			
		||||
        app.ged.register_event_handler(
 | 
			
		||||
            'archiving-error-received', ged.GUI1, self._nec_archiving_error)
 | 
			
		||||
 | 
			
		||||
        self.default_cb.set_active(0)
 | 
			
		||||
        self.set_widget_state(False)
 | 
			
		||||
        self.window.show_all()
 | 
			
		||||
        self.xml.connect_signals(self)
 | 
			
		||||
 | 
			
		||||
        self.idle_id = GLib.timeout_add_seconds(3, self._nec_archiving_error)
 | 
			
		||||
        app.connections[self.account].request_archive_preferences()
 | 
			
		||||
 | 
			
		||||
    def on_key_press_event(self, widget, event):
 | 
			
		||||
        if event.keyval == Gdk.KEY_Escape:
 | 
			
		||||
            self.window.destroy()
 | 
			
		||||
 | 
			
		||||
    def set_widget_state(self, state):
 | 
			
		||||
        for widget in ('default_cb', 'save_button', 'add_button',
 | 
			
		||||
            'remove_button'):
 | 
			
		||||
            self.xml.get_object(widget).set_sensitive(state)
 | 
			
		||||
 | 
			
		||||
    def _nec_archiving_313_changed_received(self, obj):
 | 
			
		||||
        if obj.conn.name != self.account:
 | 
			
		||||
            return
 | 
			
		||||
        try:
 | 
			
		||||
            GLib.source_remove(self.idle_id)
 | 
			
		||||
        except Exception as e:
 | 
			
		||||
            log.debug(e)
 | 
			
		||||
        self.set_widget_state(True)
 | 
			
		||||
        if obj.answer:
 | 
			
		||||
            def on_ok(dialog):
 | 
			
		||||
                self.window.destroy()
 | 
			
		||||
            dialog = HigDialog(
 | 
			
		||||
                self.window, Gtk.MessageType.INFO, Gtk.ButtonsType.OK,
 | 
			
		||||
                _('Success!'), _('Your Archiving Preferences have been saved!'),
 | 
			
		||||
                on_response_ok=on_ok, on_response_cancel=on_ok)
 | 
			
		||||
            dialog.popup()
 | 
			
		||||
        self.default_cb.set_active(self.default_dict[obj.default])
 | 
			
		||||
        self.archive_items_liststore.clear()
 | 
			
		||||
        for items in obj.items:
 | 
			
		||||
            self.archive_items_liststore.append(items)
 | 
			
		||||
 | 
			
		||||
    def _nec_archiving_error(self, obj=None):
 | 
			
		||||
        if obj and obj.conn.name != self.account:
 | 
			
		||||
            return
 | 
			
		||||
        try:
 | 
			
		||||
            GLib.source_remove(self.idle_id)
 | 
			
		||||
        except Exception as e:
 | 
			
		||||
            log.debug(e)
 | 
			
		||||
        if not obj:
 | 
			
		||||
            msg = _('No response from the Server')
 | 
			
		||||
        else:
 | 
			
		||||
            msg = _('Error received: {}').format(self.error_msg)
 | 
			
		||||
 | 
			
		||||
        dialog = HigDialog(
 | 
			
		||||
            self.window, Gtk.MessageType.INFO, Gtk.ButtonsType.OK,
 | 
			
		||||
            _('Error!'), msg)
 | 
			
		||||
        dialog.popup()
 | 
			
		||||
        self.set_widget_state(True)
 | 
			
		||||
        return
 | 
			
		||||
 | 
			
		||||
    def on_add_item_button_clicked(self, widget):
 | 
			
		||||
        key_name = 'item_archiving_preferences'
 | 
			
		||||
        if key_name in app.interface.instances[self.account]:
 | 
			
		||||
            app.interface.instances[self.account][key_name].window.present()
 | 
			
		||||
        else:
 | 
			
		||||
            app.interface.instances[self.account][key_name] = \
 | 
			
		||||
                ItemArchiving313PreferencesWindow(
 | 
			
		||||
                    self.account, self, self.window)
 | 
			
		||||
 | 
			
		||||
    def on_remove_item_button_clicked(self, widget):
 | 
			
		||||
        archive_view = self.xml.get_object('archive_view')
 | 
			
		||||
        mod, path = archive_view.get_selection().get_selected_rows()
 | 
			
		||||
        if path:
 | 
			
		||||
            iter_ = mod.get_iter(path)
 | 
			
		||||
            self.archive_items_liststore.remove(iter_)
 | 
			
		||||
 | 
			
		||||
    def on_save_button_clicked(self, widget):
 | 
			
		||||
        self.set_widget_state(False)
 | 
			
		||||
        items = []
 | 
			
		||||
        default = self.default_dict_cb[self.default_cb.get_active()]
 | 
			
		||||
        for item in self.archive_items_liststore:
 | 
			
		||||
            items.append((item[0].lower(), item[1].lower()))
 | 
			
		||||
        self.idle_id = GLib.timeout_add_seconds(3, self._nec_archiving_error)
 | 
			
		||||
        app.connections[self.account]. \
 | 
			
		||||
            set_archive_preferences(items, default)
 | 
			
		||||
 | 
			
		||||
    def on_close_button_clicked(self, widget):
 | 
			
		||||
        self.window.destroy()
 | 
			
		||||
 | 
			
		||||
    def on_archiving_preferences_window_destroy(self, widget):
 | 
			
		||||
        app.ged.remove_event_handler(
 | 
			
		||||
            'archiving-313-preferences-changed-received', ged.GUI1,
 | 
			
		||||
            self._nec_archiving_313_changed_received)
 | 
			
		||||
        app.ged.remove_event_handler(
 | 
			
		||||
            'archiving-error-received', ged.GUI1, self._nec_archiving_error)
 | 
			
		||||
        if 'archiving_preferences' in app.interface.instances[self.account]:
 | 
			
		||||
            del app.interface.instances[self.account]['archiving_preferences']
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ItemArchiving313PreferencesWindow:
 | 
			
		||||
 | 
			
		||||
    def __init__(self, account, archive, transient):
 | 
			
		||||
 | 
			
		||||
        self.account = account
 | 
			
		||||
        self.archive = archive
 | 
			
		||||
 | 
			
		||||
        self.xml = gtkgui_helpers.get_gtk_builder(
 | 
			
		||||
            'archiving_313_preferences_item.ui')
 | 
			
		||||
        self.window = self.xml.get_object('item_dialog')
 | 
			
		||||
        self.window.set_transient_for(transient)
 | 
			
		||||
        # Add Widgets
 | 
			
		||||
        for widget in ('jid_entry', 'pref_cb'):
 | 
			
		||||
            setattr(self, widget, self.xml.get_object(widget))
 | 
			
		||||
 | 
			
		||||
        self.window.set_title(_('Add JID'))
 | 
			
		||||
        self.pref_cb.set_active(0)
 | 
			
		||||
        self.window.show_all()
 | 
			
		||||
        self.xml.connect_signals(self)
 | 
			
		||||
 | 
			
		||||
    def on_ok_button_clicked(self, widget):
 | 
			
		||||
        if self.pref_cb.get_active() == 0:
 | 
			
		||||
            pref = 'Always'
 | 
			
		||||
        else:
 | 
			
		||||
            pref = 'Never'
 | 
			
		||||
        text = self.jid_entry.get_text()
 | 
			
		||||
        if not text:
 | 
			
		||||
            self.window.destroy()
 | 
			
		||||
            return
 | 
			
		||||
        else:
 | 
			
		||||
            self.archive.archive_items_liststore.append((text, pref))
 | 
			
		||||
        self.window.destroy()
 | 
			
		||||
 | 
			
		||||
    def on_cancel_button_clicked(self, widget):
 | 
			
		||||
        self.window.destroy()
 | 
			
		||||
 | 
			
		||||
    def on_item_archiving_preferences_window_destroy(self, widget):
 | 
			
		||||
        key_name = 'item_archiving_preferences'
 | 
			
		||||
        if key_name in app.interface.instances[self.account]:
 | 
			
		||||
            del app.interface.instances[self.account][key_name]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class PrivacyListWindow:
 | 
			
		||||
    """
 | 
			
		||||
    Window that is used for creating NEW or EDITING already there privacy lists
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1168,9 +1168,11 @@ class GroupchatControl(ChatControlBase):
 | 
			
		|||
        self._update_banner_state_image()
 | 
			
		||||
 | 
			
		||||
    def _nec_mam_decrypted_message_received(self, obj):
 | 
			
		||||
        if obj.conn.name != self.account:
 | 
			
		||||
            return
 | 
			
		||||
        if not obj.groupchat:
 | 
			
		||||
            return
 | 
			
		||||
        if obj.room_jid != self.room_jid:
 | 
			
		||||
        if obj.archive_jid != self.room_jid:
 | 
			
		||||
            return
 | 
			
		||||
        self.print_conversation(
 | 
			
		||||
            obj.msgtxt, contact=obj.nick,
 | 
			
		||||
| 
						 | 
				
			
			@ -1588,7 +1590,8 @@ class GroupchatControl(ChatControlBase):
 | 
			
		|||
 | 
			
		||||
        if muc_caps_cache.has_mam(self.room_jid):
 | 
			
		||||
            # Request MAM
 | 
			
		||||
            app.connections[self.account].request_archive_on_muc_join(
 | 
			
		||||
            con = app.connections[self.account]
 | 
			
		||||
            con.get_module('MAM').request_archive_on_muc_join(
 | 
			
		||||
                self.room_jid)
 | 
			
		||||
 | 
			
		||||
        app.gc_connected[self.account][self.room_jid] = True
 | 
			
		||||
| 
						 | 
				
			
			@ -2256,6 +2259,8 @@ class GroupchatControl(ChatControlBase):
 | 
			
		|||
            self._nec_signed_in)
 | 
			
		||||
        app.ged.remove_event_handler('decrypted-message-received', ged.GUI2,
 | 
			
		||||
            self._nec_decrypted_message_received)
 | 
			
		||||
        app.ged.remove_event_handler('mam-decrypted-message-received',
 | 
			
		||||
            ged.GUI1, self._nec_mam_decrypted_message_received)
 | 
			
		||||
        app.ged.remove_event_handler('gc-stanza-message-outgoing', ged.OUT_POSTCORE,
 | 
			
		||||
            self._message_sent)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										0
									
								
								gajim/gtk/__init__.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								gajim/gtk/__init__.py
									
										
									
									
									
										Normal file
									
								
							
							
								
								
									
										158
									
								
								gajim/gtk/mam_preferences.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										158
									
								
								gajim/gtk/mam_preferences.py
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,158 @@
 | 
			
		|||
# This file is part of Gajim.
 | 
			
		||||
#
 | 
			
		||||
# Gajim is free software; you can redistribute it and/or modify
 | 
			
		||||
# it under the terms of the GNU General Public License as published
 | 
			
		||||
# by the Free Software Foundation; version 3 only.
 | 
			
		||||
#
 | 
			
		||||
# Gajim is distributed in the hope that it will be useful,
 | 
			
		||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
			
		||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 | 
			
		||||
# GNU General Public License for more details.
 | 
			
		||||
#
 | 
			
		||||
# You should have received a copy of the GNU General Public License
 | 
			
		||||
# along with Gajim. If not, see <http://www.gnu.org/licenses/>.
 | 
			
		||||
 | 
			
		||||
import logging
 | 
			
		||||
 | 
			
		||||
from gi.repository import Gtk
 | 
			
		||||
from gi.repository import Gdk
 | 
			
		||||
 | 
			
		||||
from gajim.common import app
 | 
			
		||||
from gajim.common import ged
 | 
			
		||||
from gajim.gtk.util import get_builder
 | 
			
		||||
 | 
			
		||||
from gajim.dialogs import HigDialog
 | 
			
		||||
 | 
			
		||||
log = logging.getLogger('gajim.gtk.mam_preferences')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class MamPreferences(Gtk.ApplicationWindow):
 | 
			
		||||
    def __init__(self, account):
 | 
			
		||||
        Gtk.ApplicationWindow.__init__(self)
 | 
			
		||||
        self.set_application(app.app)
 | 
			
		||||
        self.set_position(Gtk.WindowPosition.CENTER)
 | 
			
		||||
        self.set_show_menubar(False)
 | 
			
		||||
        self.set_title(_('Archiving Preferences for %s') % account)
 | 
			
		||||
 | 
			
		||||
        self.connect('destroy', self._on_destroy)
 | 
			
		||||
        self.connect('key-press-event', self._on_key_press)
 | 
			
		||||
 | 
			
		||||
        self.account = account
 | 
			
		||||
        self._con = app.connections[account]
 | 
			
		||||
 | 
			
		||||
        self._builder = get_builder('mam_preferences.ui')
 | 
			
		||||
        self.add(self._builder.get_object('preferences_grid'))
 | 
			
		||||
 | 
			
		||||
        self._default = self._builder.get_object('default_cb')
 | 
			
		||||
        self._pref_store = self._builder.get_object('preferences_store')
 | 
			
		||||
        self._overlay = self._builder.get_object('overlay')
 | 
			
		||||
        self._spinner = Gtk.Spinner()
 | 
			
		||||
        self._overlay.add_overlay(self._spinner)
 | 
			
		||||
 | 
			
		||||
        app.ged.register_event_handler('mam-prefs-received', ged.GUI1,
 | 
			
		||||
                                       self._mam_prefs_received)
 | 
			
		||||
        app.ged.register_event_handler('mam-prefs-saved', ged.GUI1,
 | 
			
		||||
                                       self._mam_prefs_saved)
 | 
			
		||||
        app.ged.register_event_handler('mam-prefs-error', ged.GUI1,
 | 
			
		||||
                                       self._mam_prefs_error)
 | 
			
		||||
 | 
			
		||||
        self._set_grid_state(False)
 | 
			
		||||
        self._builder.connect_signals(self)
 | 
			
		||||
        self.show_all()
 | 
			
		||||
 | 
			
		||||
        self._activate_spinner()
 | 
			
		||||
 | 
			
		||||
        self._con.get_module('MAM').request_mam_preferences()
 | 
			
		||||
 | 
			
		||||
    def _mam_prefs_received(self, obj):
 | 
			
		||||
        if obj.conn.name != self.account:
 | 
			
		||||
            return
 | 
			
		||||
        self._disable_spinner()
 | 
			
		||||
        self._set_grid_state(True)
 | 
			
		||||
 | 
			
		||||
        self._default.set_active_id(obj.default)
 | 
			
		||||
        self._pref_store.clear()
 | 
			
		||||
        for item in obj.rules:
 | 
			
		||||
            self._pref_store.append(item)
 | 
			
		||||
 | 
			
		||||
    def _mam_prefs_saved(self, obj):
 | 
			
		||||
        if obj.conn.name != self.account:
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        self._disable_spinner()
 | 
			
		||||
 | 
			
		||||
        def on_ok(dialog):
 | 
			
		||||
            self.destroy()
 | 
			
		||||
        dialog = HigDialog(
 | 
			
		||||
            self, Gtk.MessageType.INFO, Gtk.ButtonsType.OK,
 | 
			
		||||
            _('Success!'), _('Your Archiving Preferences have been saved!'),
 | 
			
		||||
            on_response_ok=on_ok, on_response_cancel=on_ok)
 | 
			
		||||
        dialog.popup()
 | 
			
		||||
 | 
			
		||||
    def _mam_prefs_error(self, obj=None):
 | 
			
		||||
        if obj and obj.conn.name != self.account:
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        self._disable_spinner()
 | 
			
		||||
 | 
			
		||||
        if not obj:
 | 
			
		||||
            msg = _('No response from the Server')
 | 
			
		||||
        else:
 | 
			
		||||
            msg = _('Error received: {}').format(obj.error_msg)
 | 
			
		||||
 | 
			
		||||
        dialog = HigDialog(
 | 
			
		||||
            self, Gtk.MessageType.INFO, Gtk.ButtonsType.OK,
 | 
			
		||||
            _('Error!'), msg)
 | 
			
		||||
        dialog.popup()
 | 
			
		||||
        self._set_grid_state(True)
 | 
			
		||||
 | 
			
		||||
    def _set_grid_state(self, state):
 | 
			
		||||
        self._builder.get_object('preferences_grid').set_sensitive(state)
 | 
			
		||||
 | 
			
		||||
    def _jid_edited(self, renderer, path, new_text):
 | 
			
		||||
        iter_ = self._pref_store.get_iter(path)
 | 
			
		||||
        self._pref_store.set_value(iter_, 0, new_text)
 | 
			
		||||
 | 
			
		||||
    def _pref_toggled(self, renderer, path):
 | 
			
		||||
        iter_ = self._pref_store.get_iter(path)
 | 
			
		||||
        current_value = self._pref_store[iter_][1]
 | 
			
		||||
        self._pref_store.set_value(iter_, 1, not current_value)
 | 
			
		||||
 | 
			
		||||
    def _on_add(self, button):
 | 
			
		||||
        self._pref_store.append(['', False])
 | 
			
		||||
 | 
			
		||||
    def _on_remove(self, button):
 | 
			
		||||
        pref_view = self._builder.get_object('pref_view')
 | 
			
		||||
        mod, paths = pref_view.get_selection().get_selected_rows()
 | 
			
		||||
        for path in paths:
 | 
			
		||||
            iter_ = mod.get_iter(path)
 | 
			
		||||
            self._pref_store.remove(iter_)
 | 
			
		||||
 | 
			
		||||
    def _on_save(self, button):
 | 
			
		||||
        self._activate_spinner()
 | 
			
		||||
        self._set_grid_state(False)
 | 
			
		||||
        items = []
 | 
			
		||||
        default = self._default.get_active_id()
 | 
			
		||||
        for item in self._pref_store:
 | 
			
		||||
            items.append((item[0].lower(), item[1]))
 | 
			
		||||
        self._con.get_module('MAM').set_mam_preferences(items, default)
 | 
			
		||||
 | 
			
		||||
    def _activate_spinner(self):
 | 
			
		||||
        self._spinner.show()
 | 
			
		||||
        self._spinner.start()
 | 
			
		||||
 | 
			
		||||
    def _disable_spinner(self):
 | 
			
		||||
        self._spinner.hide()
 | 
			
		||||
        self._spinner.stop()
 | 
			
		||||
 | 
			
		||||
    def _on_key_press(self, widget, event):
 | 
			
		||||
        if event.keyval == Gdk.KEY_Escape:
 | 
			
		||||
            self.destroy()
 | 
			
		||||
 | 
			
		||||
    def _on_destroy(self, widget):
 | 
			
		||||
        app.ged.remove_event_handler('mam-prefs-received', ged.GUI1,
 | 
			
		||||
                                     self._mam_prefs_received)
 | 
			
		||||
        app.ged.remove_event_handler('mam-prefs-saved', ged.GUI1,
 | 
			
		||||
                                     self._mam_prefs_saved)
 | 
			
		||||
        app.ged.remove_event_handler('mam-prefs-error', ged.GUI1,
 | 
			
		||||
                                     self._mam_prefs_error)
 | 
			
		||||
							
								
								
									
										54
									
								
								gajim/gtk/util.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								gajim/gtk/util.py
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,54 @@
 | 
			
		|||
# This file is part of Gajim.
 | 
			
		||||
#
 | 
			
		||||
# Gajim is free software; you can redistribute it and/or modify
 | 
			
		||||
# it under the terms of the GNU General Public License as published
 | 
			
		||||
# by the Free Software Foundation; version 3 only.
 | 
			
		||||
#
 | 
			
		||||
# Gajim is distributed in the hope that it will be useful,
 | 
			
		||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
			
		||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 | 
			
		||||
# GNU General Public License for more details.
 | 
			
		||||
#
 | 
			
		||||
# You should have received a copy of the GNU General Public License
 | 
			
		||||
# along with Gajim. If not, see <http://www.gnu.org/licenses/>.
 | 
			
		||||
 | 
			
		||||
import os
 | 
			
		||||
import sys
 | 
			
		||||
 | 
			
		||||
from gi.repository import Gtk
 | 
			
		||||
import xml.etree.ElementTree as ET
 | 
			
		||||
 | 
			
		||||
from gajim.common import i18n
 | 
			
		||||
from gajim.common import configpaths
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_builder(file_name, widget=None):
 | 
			
		||||
    file_path = os.path.join(configpaths.get('GUI'), file_name)
 | 
			
		||||
    builder = _translate(file_path, widget)
 | 
			
		||||
    builder.set_translation_domain(i18n.DOMAIN)
 | 
			
		||||
    return builder
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _translate(gui_file, widget):
 | 
			
		||||
    """
 | 
			
		||||
    This is a workaround for non working translation on Windows
 | 
			
		||||
    """
 | 
			
		||||
    if sys.platform == "win32":
 | 
			
		||||
        tree = ET.parse(gui_file)
 | 
			
		||||
        for node in tree.iter():
 | 
			
		||||
            if 'translatable' in node.attrib:
 | 
			
		||||
                node.text = _(node.text)
 | 
			
		||||
        xml_text = ET.tostring(tree.getroot(),
 | 
			
		||||
                               encoding='unicode',
 | 
			
		||||
                               method='xml')
 | 
			
		||||
        if widget is not None:
 | 
			
		||||
            builder = Gtk.Builder()
 | 
			
		||||
            builder.add_objects_from_string(xml_text, [widget])
 | 
			
		||||
            return builder
 | 
			
		||||
        return Gtk.Builder.new_from_string(xml_text, -1)
 | 
			
		||||
    else:
 | 
			
		||||
        if widget is not None:
 | 
			
		||||
            builder = Gtk.Builder()
 | 
			
		||||
            builder.add_objects_from_file(gui_file, [widget])
 | 
			
		||||
            return builder
 | 
			
		||||
        return Gtk.Builder.new_from_file(gui_file)
 | 
			
		||||
| 
						 | 
				
			
			@ -1113,9 +1113,9 @@ class Interface:
 | 
			
		|||
            # Else disable autoaway
 | 
			
		||||
            app.sleeper_state[account] = 'off'
 | 
			
		||||
 | 
			
		||||
        if obj.conn.archiving_313_supported and app.config.get_per('accounts',
 | 
			
		||||
        if obj.conn.get_module('MAM').available and app.config.get_per('accounts',
 | 
			
		||||
        account, 'sync_logs_with_server'):
 | 
			
		||||
            obj.conn.request_archive_on_signin()
 | 
			
		||||
            obj.conn.get_module('MAM').request_archive_on_signin()
 | 
			
		||||
 | 
			
		||||
        invisible_show = app.SHOW_LIST.index('invisible')
 | 
			
		||||
        # We cannot join rooms if we are invisible
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -54,7 +54,6 @@ class HistorySyncAssistant(Gtk.Assistant):
 | 
			
		|||
        self.end = None
 | 
			
		||||
        self.next = None
 | 
			
		||||
        self.hide_buttons()
 | 
			
		||||
        self.event_id = id(self)
 | 
			
		||||
 | 
			
		||||
        own_jid = self.con.get_own_jid().getStripped()
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -88,9 +87,6 @@ class HistorySyncAssistant(Gtk.Assistant):
 | 
			
		|||
        app.ged.register_event_handler('archiving-count-received',
 | 
			
		||||
                                       ged.GUI1,
 | 
			
		||||
                                       self._received_count)
 | 
			
		||||
        app.ged.register_event_handler('archiving-query-id',
 | 
			
		||||
                                       ged.GUI1,
 | 
			
		||||
                                       self._new_query_id)
 | 
			
		||||
        app.ged.register_event_handler('archiving-interval-finished',
 | 
			
		||||
                                       ged.GUI1,
 | 
			
		||||
                                       self._received_finished)
 | 
			
		||||
| 
						 | 
				
			
			@ -145,28 +141,27 @@ class HistorySyncAssistant(Gtk.Assistant):
 | 
			
		|||
        log.info('start: %s', self.start)
 | 
			
		||||
        log.info('end: %s', self.end)
 | 
			
		||||
 | 
			
		||||
        self.con.request_archive_count(self.event_id, self.start, self.end)
 | 
			
		||||
        self.query_id = self.con.get_module('MAM').request_archive_count(
 | 
			
		||||
            self.start, self.end)
 | 
			
		||||
 | 
			
		||||
    def _received_count(self, event):
 | 
			
		||||
        if event.event_id != self.event_id:
 | 
			
		||||
        if event.query_id != self.query_id:
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        if event.count is not None:
 | 
			
		||||
            self.download_history.count = int(event.count)
 | 
			
		||||
        self.con.request_archive_interval(self.event_id, self.start, self.end)
 | 
			
		||||
        self.query_id = self.con.get_module('MAM').request_archive_interval(
 | 
			
		||||
            self.start, self.end)
 | 
			
		||||
 | 
			
		||||
    def _received_finished(self, event):
 | 
			
		||||
        if event.event_id != self.event_id:
 | 
			
		||||
        if event.query_id != self.query_id:
 | 
			
		||||
            return
 | 
			
		||||
        self.query_id = None
 | 
			
		||||
        log.info('query finished')
 | 
			
		||||
        GLib.idle_add(self.download_history.finished)
 | 
			
		||||
        self.set_current_page(Pages.SUMMARY)
 | 
			
		||||
        self.summary.finished()
 | 
			
		||||
 | 
			
		||||
    def _new_query_id(self, event):
 | 
			
		||||
        if event.event_id != self.event_id:
 | 
			
		||||
            return
 | 
			
		||||
        self.query_id = event.query_id
 | 
			
		||||
 | 
			
		||||
    def _nec_mam_message_received(self, obj):
 | 
			
		||||
        if obj.conn.name != self.account:
 | 
			
		||||
            return
 | 
			
		||||
| 
						 | 
				
			
			@ -193,9 +188,6 @@ class HistorySyncAssistant(Gtk.Assistant):
 | 
			
		|||
        app.ged.remove_event_handler('archiving-count-received',
 | 
			
		||||
                                     ged.GUI1,
 | 
			
		||||
                                     self._received_count)
 | 
			
		||||
        app.ged.remove_event_handler('archiving-query-id',
 | 
			
		||||
                                     ged.GUI1,
 | 
			
		||||
                                     self._new_query_id)
 | 
			
		||||
        app.ged.remove_event_handler('archiving-interval-finished',
 | 
			
		||||
                                     ged.GUI1,
 | 
			
		||||
                                     self._received_finished)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -5404,7 +5404,7 @@ class RosterWindow:
 | 
			
		|||
                    self.on_privacy_lists_menuitem_activate, account)
 | 
			
		||||
            else:
 | 
			
		||||
                privacy_lists_menuitem.set_sensitive(False)
 | 
			
		||||
            if app.connections[account].archiving_313_supported:
 | 
			
		||||
            if app.connections[account].get_module('MAM').available:
 | 
			
		||||
                archiving_preferences_menuitem.connect(
 | 
			
		||||
                    'activate',
 | 
			
		||||
                    self.on_archiving_preferences_menuitem_activate, account)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -174,7 +174,8 @@ class ServerInfoDialog(Gtk.Dialog):
 | 
			
		|||
            Feature('XEP-0280: Message Carbons',
 | 
			
		||||
                    con.carbons_available, nbxmpp.NS_CARBONS, carbons_enabled),
 | 
			
		||||
            Feature('XEP-0313: Message Archive Management',
 | 
			
		||||
                    con.archiving_namespace, con.archiving_namespace,
 | 
			
		||||
                    con.get_module('MAM').archiving_namespace,
 | 
			
		||||
                    con.get_module('MAM').archiving_namespace,
 | 
			
		||||
                    mam_enabled),
 | 
			
		||||
            Feature('XEP-0363: HTTP File Upload',
 | 
			
		||||
                    con.get_module('HTTPUpload').available,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		
		Reference in a new issue