Philipp Hörist pushed to branch master at gajim / gajim

Commits:

13 changed files:

Changes:

  • data/style/gajim.css
    ... ... @@ -12,3 +12,12 @@ popover#EmoticonPopover flowboxchild > label { font-size: 24px; }
    12 12
     popover#EmoticonPopover notebook label { font-size: 24px; }
    
    13 13
     popover#EmoticonPopover flowbox { padding-left: 5px; padding-right: 6px; }
    
    14 14
     popover#EmoticonPopover flowboxchild { padding-top: 5px; padding-bottom: 5px; }
    
    15
    +
    
    16
    +/* HistorySyncAssistant */
    
    17
    +#HistorySyncAssistant list { border: 1px solid; border-color: @borders; }
    
    18
    +#HistorySyncAssistant progressbar text { color: #000; font-size: 18px; padding: 10px;}
    
    19
    +#HistorySyncAssistant list > row { padding: 10px 30px 10px 30px; }
    
    20
    +#HistorySyncAssistant list > row > label { color: @insensitive_fg_color }
    
    21
    +#HistorySyncAssistant list > row.activatable > label { color: @theme_text_color; }
    
    22
    +#HistorySyncAssistant list > row.activatable:selected > label { color: @theme_selected_fg_color; }
    
    23
    +#FinishedLabel { font-size: 14px; font-weight: bold }

  • gajim/app_actions.py
    ... ... @@ -31,6 +31,7 @@ import shortcuts_window
    31 31
     import plugins.gui
    
    32 32
     import history_window
    
    33 33
     import disco
    
    34
    +from history_sync import HistorySyncAssistant
    
    34 35
     
    
    35 36
     
    
    36 37
     class AppActions():
    
    ... ... @@ -150,6 +151,14 @@ class AppActions():
    150 151
                     gajim.interface.instances[account]['archiving_preferences'] = \
    
    151 152
                         dialogs.ArchivingPreferencesWindow(account)
    
    152 153
     
    
    154
    +    def on_history_sync(self, action, param):
    
    155
    +        account = param.get_string()
    
    156
    +        if 'history_sync' in gajim.interface.instances[account]:
    
    157
    +            gajim.interface.instances[account]['history_sync'].present()
    
    158
    +        else:
    
    159
    +            gajim.interface.instances[account]['history_sync'] = \
    
    160
    +                    HistorySyncAssistant(account, gajim.interface.roster.window)
    
    161
    +
    
    153 162
         def on_privacy_lists(self, action, param):
    
    154 163
             account = param.get_string()
    
    155 164
             if 'privacy_lists' in gajim.interface.instances[account]:
    

  • gajim/common/config.py
    ... ... @@ -419,6 +419,7 @@ class Config:
    419 419
                         'oauth2_redirect_url': [ opt_str, 'https%3A%2F%2Fgajim.org%2Fmsnauth%2Findex.cgi', _('redirect_url for OAuth 2.0 authentication.')],
    
    420 420
                         'opened_chat_controls': [opt_str, '', _('Space separated list of JIDs for which we want to re-open a chat window on next startup.')],
    
    421 421
                         'last_mam_id': [opt_str, '', _('Last MAM id we are syncronized with')],
    
    422
    +                    'mam_start_date': [opt_int, 0, _('The earliest date we requested MAM history for')],
    
    422 423
                 }, {}),
    
    423 424
                 'statusmsg': ({
    
    424 425
                         'message': [ opt_str, '' ],
    

  • gajim/common/connection.py
    ... ... @@ -753,6 +753,21 @@ class Connection(CommonConnection, ConnectionHandlers):
    753 753
         def check_jid(self, jid):
    
    754 754
             return helpers.parse_jid(jid)
    
    755 755
     
    
    756
    +    def get_own_jid(self, full=False):
    
    757
    +        """
    
    758
    +        Return our own jid as JID
    
    759
    +        If full = True, this raises an exception if we cant provide
    
    760
    +        the full JID
    
    761
    +        """
    
    762
    +        if self.connection:
    
    763
    +            full_jid = self.connection._registered_name
    
    764
    +            return nbxmpp.JID(full_jid)
    
    765
    +        else:
    
    766
    +            if full:
    
    767
    +                raise exceptions.GajimGeneralException(
    
    768
    +                    'We are not connected, full JID unknown.')
    
    769
    +            return nbxmpp.JID(gajim.get_jid_from_account(self.name))
    
    770
    +
    
    756 771
         def reconnect(self):
    
    757 772
             # Do not try to reco while we are already trying
    
    758 773
             self.time_to_reconnect = None
    

  • gajim/common/connection_handlers_events.py
    ... ... @@ -18,6 +18,9 @@
    18 18
     ## along with Gajim. If not, see <http://www.gnu.org/licenses/>.
    
    19 19
     ##
    
    20 20
     
    
    21
    +# pylint: disable=no-init
    
    22
    +# pylint: disable=attribute-defined-outside-init
    
    23
    +
    
    21 24
     from calendar import timegm
    
    22 25
     import datetime
    
    23 26
     import hashlib
    
    ... ... @@ -41,6 +44,7 @@ from common.logger import LOG_DB_PATH
    41 44
     from common.pep import SUPPORTED_PERSONAL_USER_EVENTS
    
    42 45
     from common.jingle_transport import JingleTransportSocks5
    
    43 46
     from common.file_props import FilesProp
    
    47
    +from common.nec import NetworkEvent
    
    44 48
     
    
    45 49
     if gajim.HAVE_PYOPENSSL:
    
    46 50
         import OpenSSL.crypto
    
    ... ... @@ -1034,43 +1038,70 @@ class BeforeChangeShowEvent(nec.NetworkIncomingEvent):
    1034 1038
     
    
    1035 1039
     class MamMessageReceivedEvent(nec.NetworkIncomingEvent, HelperEvent):
    
    1036 1040
         name = 'mam-message-received'
    
    1037
    -    base_network_events = []
    
    1038
    -
    
    1039
    -    def init(self):
    
    1041
    +    base_network_events = ['raw-mam-message-received']
    
    1042
    +
    
    1043
    +    def __init__(self, name, base_event):
    
    1044
    +        '''
    
    1045
    +        Pre-Generated attributes on self:
    
    1046
    +
    
    1047
    +        :conn:          Connection instance
    
    1048
    +        :stanza:        Complete stanza Node
    
    1049
    +        :forwarded:     Forwarded Node
    
    1050
    +        :result:        Result Node
    
    1051
    +        '''
    
    1052
    +        self._set_base_event_vars_as_attributes(base_event)
    
    1040 1053
             self.additional_data = {}
    
    1041 1054
             self.encrypted = False
    
    1042
    -    
    
    1055
    +        self.groupchat = False
    
    1056
    +
    
    1043 1057
         def generate(self):
    
    1044
    -        if not self.stanza:
    
    1045
    -            return
    
    1046
    -        account = self.conn.name
    
    1047
    -        self.msg_ = self.stanza.getTag('message')
    
    1048
    -        # use timestamp of archived message, if available and archive timestamp otherwise
    
    1049
    -        delay = self.stanza.getTag('delay', namespace=nbxmpp.NS_DELAY2)
    
    1050
    -        delay2 = self.msg_.getTag('delay', namespace=nbxmpp.NS_DELAY2)
    
    1051
    -        if delay2:
    
    1052
    -            delay = delay2
    
    1058
    +        archive_jid = self.stanza.getFrom()
    
    1059
    +        own_jid = self.conn.get_own_jid()
    
    1060
    +        if archive_jid and not archive_jid.bareMatch(own_jid):
    
    1061
    +            # MAM Message not from our Archive
    
    1062
    +            log.info('MAM message not from our user archive')
    
    1063
    +            return False
    
    1064
    +
    
    1065
    +        self.msg_ = self.forwarded.getTag('message')
    
    1066
    +
    
    1067
    +        if self.msg_.getType() == 'groupchat':
    
    1068
    +            log.info('Received groupchat message from user archive')
    
    1069
    +            return False
    
    1070
    +
    
    1071
    +        self.msgtxt = self.msg_.getTagData('body')
    
    1072
    +        self.stanza_id = self.msg_.getID()
    
    1073
    +        self.mam_id = self.result.getID()
    
    1074
    +        self.query_id = self.result.getAttr('queryid')
    
    1075
    +
    
    1076
    +        # Use timestamp provided by archive,
    
    1077
    +        # Fallback: Use timestamp provided by user and issue a warning
    
    1078
    +        delay = self.forwarded.getTag('delay', namespace=nbxmpp.NS_DELAY2)
    
    1079
    +        if not delay:
    
    1080
    +            log.warning('No timestamp on archive Message, try fallback')
    
    1081
    +            delay = self.msg_.getTag('delay', namespace=nbxmpp.NS_DELAY2)
    
    1053 1082
             if not delay:
    
    1083
    +            log.error('Received MAM message without timestamp')
    
    1054 1084
                 return
    
    1055
    -        tim = delay.getAttr('stamp')
    
    1056
    -        tim = helpers.datetime_tuple(tim)
    
    1057
    -        self.tim = timegm(tim)
    
    1058
    -        to_ = self.msg_.getAttr('to')
    
    1059
    -        if to_:
    
    1060
    -            to_ = gajim.get_jid_without_resource(to_)
    
    1061
    -        else:
    
    1062
    -            to_ = gajim.get_jid_from_account(account)
    
    1063
    -        frm_ = gajim.get_jid_without_resource(self.msg_.getAttr('from'))
    
    1064
    -        self.msgtxt = self.msg_.getTagData('body')
    
    1065
    -        if to_ == gajim.get_jid_from_account(account):
    
    1066
    -            self.with_ = frm_
    
    1085
    +
    
    1086
    +        self.timestamp = helpers.parse_delay(delay)
    
    1087
    +
    
    1088
    +        frm = self.msg_.getFrom()
    
    1089
    +        to = self.msg_.getTo()
    
    1090
    +
    
    1091
    +        if not to or to.bareMatch(own_jid):
    
    1092
    +            self.with_ = str(frm)
    
    1067 1093
                 self.direction = 'from'
    
    1068
    -            self.resource = gajim.get_resource_from_jid(
    
    1069
    -                self.msg_.getAttr('from'))
    
    1094
    +            self.resource = frm.getResource()
    
    1070 1095
             else:
    
    1071
    -            self.with_ = to_
    
    1096
    +            self.with_ = str(to)
    
    1072 1097
                 self.direction = 'to'
    
    1073
    -            self.resource = gajim.get_resource_from_jid(self.msg_.getAttr('to'))
    
    1098
    +            self.resource = to.getResource()
    
    1099
    +
    
    1100
    +        log_message = \
    
    1101
    +            'received: mam-message: ' \
    
    1102
    +            'stanza id: {:15} - mam id: {:15} - query id: {}'.format(
    
    1103
    +                self.stanza_id, self.mam_id, self.query_id)
    
    1104
    +        log.debug(log_message)
    
    1074 1105
             return True
    
    1075 1106
     
    
    1076 1107
     class MamDecryptedMessageReceivedEvent(nec.NetworkIncomingEvent, HelperEvent):
    
    ... ... @@ -1084,14 +1115,14 @@ class MamDecryptedMessageReceivedEvent(nec.NetworkIncomingEvent, HelperEvent):
    1084 1115
                 self.additional_data = self.msg_obj.additional_data
    
    1085 1116
             self.with_ = self.msg_obj.with_
    
    1086 1117
             self.direction = self.msg_obj.direction
    
    1087
    -        self.tim = self.msg_obj.tim
    
    1118
    +        self.timestamp = self.msg_obj.timestamp
    
    1088 1119
             res = self.msg_obj.resource
    
    1089 1120
             self.msgtxt = self.msg_obj.msgtxt
    
    1090 1121
             is_pm = gajim.logger.jid_is_room_jid(self.with_)
    
    1091 1122
             if msg_.getAttr('type') == 'groupchat':
    
    1092 1123
                 if is_pm == False:
    
    1093 1124
                     log.warn('JID %s is marked as normal contact in database '
    
    1094
    -                         'but we got a groupchat message from it.')
    
    1125
    +                         'but we got a groupchat message from it.', self.with_)
    
    1095 1126
                     return
    
    1096 1127
                 if is_pm == None:
    
    1097 1128
                     gajim.logger.get_jid_id(self.with_, 'ROOM')
    
    ... ... @@ -1102,12 +1133,12 @@ class MamDecryptedMessageReceivedEvent(nec.NetworkIncomingEvent, HelperEvent):
    1102 1133
                     server = gajim.get_server_from_jid(self.with_)
    
    1103 1134
                     if server not in self.conn.mam_awaiting_disco_result:
    
    1104 1135
                         self.conn.mam_awaiting_disco_result[server] = [
    
    1105
    -                        [self.with_, self.direction, self.tim, self.msgtxt,
    
    1136
    +                        [self.with_, self.direction, self.timestamp, self.msgtxt,
    
    1106 1137
                             res]]
    
    1107 1138
                         self.conn.discoverInfo(server)
    
    1108 1139
                     else:
    
    1109 1140
                         self.conn.mam_awaiting_disco_result[server].append(
    
    1110
    -                        [self.with_, self.direction, self.tim, self.msgtxt,
    
    1141
    +                        [self.with_, self.direction, self.timestamp, self.msgtxt,
    
    1111 1142
                             res])
    
    1112 1143
                     return
    
    1113 1144
             return True
    
    ... ... @@ -1128,8 +1159,7 @@ class MessageReceivedEvent(nec.NetworkIncomingEvent, HelperEvent):
    1128 1159
             self.encrypted = False
    
    1129 1160
             account = self.conn.name
    
    1130 1161
     
    
    1131
    -        our_full_jid = gajim.get_jid_from_account(account, full=True)
    
    1132
    -        if self.stanza.getFrom() == our_full_jid:
    
    1162
    +        if self.stanza.getFrom() == self.conn.get_own_jid(full=True):
    
    1133 1163
                 # Drop messages sent from our own full jid
    
    1134 1164
                 # It can happen that when we sent message to our own bare jid
    
    1135 1165
                 # that the server routes that message back to us
    
    ... ... @@ -1216,8 +1246,16 @@ class MessageReceivedEvent(nec.NetworkIncomingEvent, HelperEvent):
    1216 1246
                                                     nbxmpp.NS_MAM_1,
    
    1217 1247
                                                     nbxmpp.NS_MAM_2):
    
    1218 1248
                 forwarded = result.getTag('forwarded', namespace=nbxmpp.NS_FORWARD)
    
    1219
    -            gajim.nec.push_incoming_event(MamMessageReceivedEvent(None,
    
    1220
    -                conn=self.conn, stanza=forwarded))
    
    1249
    +            if not forwarded:
    
    1250
    +                log.warning('Invalid MAM Message: no forwarded child')
    
    1251
    +                return
    
    1252
    +
    
    1253
    +            gajim.nec.push_incoming_event(
    
    1254
    +                NetworkEvent('raw-mam-message-received',
    
    1255
    +                             conn=self.conn,
    
    1256
    +                             stanza=self.stanza,
    
    1257
    +                             forwarded=forwarded,
    
    1258
    +                             result=result))
    
    1221 1259
                 return
    
    1222 1260
     
    
    1223 1261
             # Mediated invitation?
    
    ... ... @@ -1804,8 +1842,8 @@ class ArchivingFinishedReceivedEvent(nec.NetworkIncomingEvent):
    1804 1842
             if self.type_ != 'result' or not self.fin:
    
    1805 1843
                 return
    
    1806 1844
     
    
    1807
    -        self.queryid = self.fin.getAttr('queryid')
    
    1808
    -        if not self.queryid:
    
    1845
    +        self.query_id = self.fin.getAttr('queryid')
    
    1846
    +        if not self.query_id:
    
    1809 1847
                 return
    
    1810 1848
     
    
    1811 1849
             return True
    
    ... ... @@ -1822,8 +1860,8 @@ class ArchivingFinishedLegacyReceivedEvent(nec.NetworkIncomingEvent):
    1822 1860
             if not self.fin:
    
    1823 1861
                 return
    
    1824 1862
     
    
    1825
    -        self.queryid = self.fin.getAttr('queryid')
    
    1826
    -        if not self.queryid:
    
    1863
    +        self.query_id = self.fin.getAttr('queryid')
    
    1864
    +        if not self.query_id:
    
    1827 1865
                 return
    
    1828 1866
     
    
    1829 1867
             return True
    

  • gajim/common/gajim.py
    ... ... @@ -420,16 +420,13 @@ def jid_is_transport(jid):
    420 420
             return True
    
    421 421
         return False
    
    422 422
     
    
    423
    -def get_jid_from_account(account_name, full=False):
    
    423
    +def get_jid_from_account(account_name):
    
    424 424
         """
    
    425 425
         Return the jid we use in the given account
    
    426 426
         """
    
    427 427
         name = config.get_per('accounts', account_name, 'name')
    
    428 428
         hostname = config.get_per('accounts', account_name, 'hostname')
    
    429 429
         jid = name + '@' + hostname
    
    430
    -    if full:
    
    431
    -        resource = connections[account_name].server_resource
    
    432
    -        jid += '/' + resource
    
    433 430
         return jid
    
    434 431
     
    
    435 432
     def get_our_jids():
    

  • gajim/common/helpers.py
    ... ... @@ -43,11 +43,13 @@ import shlex
    43 43
     from common import caps_cache
    
    44 44
     import socket
    
    45 45
     import time
    
    46
    -import datetime
    
    46
    +from datetime import datetime, timedelta
    
    47 47
     
    
    48 48
     from encodings.punycode import punycode_encode
    
    49 49
     from string import Template
    
    50 50
     
    
    51
    +import nbxmpp
    
    52
    +
    
    51 53
     from common.i18n import Q_
    
    52 54
     from common.i18n import ngettext
    
    53 55
     
    
    ... ... @@ -589,16 +591,45 @@ def datetime_tuple(timestamp):
    589 591
         tim = time.strptime(date + 'T' + tim, '%Y%m%dT%H:%M:%S')
    
    590 592
         if zone:
    
    591 593
             zone = zone.replace(':', '')
    
    592
    -        tim = datetime.datetime.fromtimestamp(time.mktime(tim))
    
    594
    +        tim = datetime.fromtimestamp(time.mktime(tim))
    
    593 595
             if len(zone) > 2:
    
    594 596
                 zone = time.strptime(zone, '%H%M')
    
    595 597
             else:
    
    596 598
                 zone = time.strptime(zone, '%H')
    
    597
    -        zone = datetime.timedelta(hours=zone.tm_hour, minutes=zone.tm_min)
    
    599
    +        zone = timedelta(hours=zone.tm_hour, minutes=zone.tm_min)
    
    598 600
             tim += zone * sign
    
    599 601
             tim = tim.timetuple()
    
    600 602
         return tim
    
    601 603
     
    
    604
    +def parse_delay(timestamp):
    
    605
    +    '''
    
    606
    +    Parse a timestamp
    
    607
    +    https://xmpp.org/extensions/xep-0203.html
    
    608
    +    Note: Not all delay tags should be parsed with this method
    
    609
    +    see https://xmpp.org/extensions/xep-0082.html for more information
    
    610
    +
    
    611
    +    :param timestamp: a XEP-0203 fomated timestring string or a delay Node
    
    612
    +    
    
    613
    +    Examples:
    
    614
    +    '2017-11-05T01:41:20Z'
    
    615
    +    '2017-11-05T01:41:20.123Z'
    
    616
    +
    
    617
    +    return epoch UTC timestamp
    
    618
    +    '''
    
    619
    +    if isinstance(timestamp, nbxmpp.protocol.Node):
    
    620
    +        timestamp = timestamp.getAttr('stamp')
    
    621
    +    timestamp += '+0000'
    
    622
    +    try:
    
    623
    +        datetime_ = datetime.strptime(timestamp, '%Y-%m-%dT%H:%M:%SZ%z')
    
    624
    +    except ValueError:
    
    625
    +        try:
    
    626
    +            datetime_ = datetime.strptime(timestamp, '%Y-%m-%dT%H:%M:%S.%fZ%z')
    
    627
    +        except ValueError:
    
    628
    +            log.error('Could not parse delay timestamp: %s', timestamp)
    
    629
    +            raise
    
    630
    +    return datetime_.timestamp()
    
    631
    +
    
    632
    +
    
    602 633
     from common import gajim
    
    603 634
     if gajim.HAVE_PYCURL:
    
    604 635
         import pycurl
    
    ... ... @@ -1280,7 +1311,6 @@ def prepare_and_validate_gpg_keyID(account, jid, keyID):
    1280 1311
         return keyID
    
    1281 1312
     
    
    1282 1313
     def update_optional_features(account = None):
    
    1283
    -    import nbxmpp
    
    1284 1314
         if account:
    
    1285 1315
             accounts = [account]
    
    1286 1316
         else:
    

  • gajim/common/message_archiving.py
    ... ... @@ -22,10 +22,11 @@ import nbxmpp
    22 22
     from common import gajim
    
    23 23
     from common import ged
    
    24 24
     from common import helpers
    
    25
    -from common.connection_handlers_events import ArchivingReceivedEvent
    
    25
    +import common.connection_handlers_events as ev
    
    26 26
     
    
    27 27
     from calendar import timegm
    
    28 28
     from time import localtime
    
    29
    +from datetime import datetime, timedelta
    
    29 30
     
    
    30 31
     import logging
    
    31 32
     log = logging.getLogger('gajim.c.message_archiving')
    
    ... ... @@ -33,7 +34,6 @@ log = logging.getLogger('gajim.c.message_archiving')
    33 34
     ARCHIVING_COLLECTIONS_ARRIVED = 'archiving_collections_arrived'
    
    34 35
     ARCHIVING_COLLECTION_ARRIVED = 'archiving_collection_arrived'
    
    35 36
     ARCHIVING_MODIFICATIONS_ARRIVED = 'archiving_modifications_arrived'
    
    36
    -MAM_RESULTS_ARRIVED = 'mam_results_arrived'
    
    37 37
     
    
    38 38
     class ConnectionArchive:
    
    39 39
         def __init__(self):
    
    ... ... @@ -46,6 +46,9 @@ class ConnectionArchive313(ConnectionArchive):
    46 46
             self.archiving_313_supported = False
    
    47 47
             self.mam_awaiting_disco_result = {}
    
    48 48
             self.iq_answer = []
    
    49
    +        self.mam_query_date = None
    
    50
    +        self.mam_query_id = None
    
    51
    +        gajim.nec.register_incoming_event(ev.MamMessageReceivedEvent)
    
    49 52
             gajim.ged.register_event_handler('archiving-finished-legacy', ged.CORE,
    
    50 53
                 self._nec_result_finished)
    
    51 54
             gajim.ged.register_event_handler('archiving-finished', ged.CORE,
    
    ... ... @@ -108,48 +111,78 @@ class ConnectionArchive313(ConnectionArchive):
    108 111
             if obj.conn.name != self.name:
    
    109 112
                 return
    
    110 113
     
    
    111
    -        if obj.queryid not in self.awaiting_answers:
    
    114
    +        if obj.query_id != self.mam_query_id:
    
    112 115
                 return
    
    113 116
     
    
    114
    -        if self.awaiting_answers[obj.queryid][0] == MAM_RESULTS_ARRIVED:
    
    115
    -            set_ = obj.fin.getTag('set', namespace=nbxmpp.NS_RSM)
    
    116
    -            if set_:
    
    117
    -                last = set_.getTagData('last')
    
    118
    -                if last:
    
    119
    -                    gajim.config.set_per('accounts', self.name, 'last_mam_id', last)
    
    120
    -                    complete = obj.fin.getAttr('complete')
    
    121
    -                    if complete != 'true':
    
    122
    -                        self.request_archive(after=last)
    
    123
    -            del self.awaiting_answers[obj.queryid]
    
    117
    +        set_ = obj.fin.getTag('set', namespace=nbxmpp.NS_RSM)
    
    118
    +        if set_:
    
    119
    +            last = set_.getTagData('last')
    
    120
    +            complete = obj.fin.getAttr('complete')
    
    121
    +            if last:
    
    122
    +                gajim.config.set_per('accounts', self.name, 'last_mam_id', last)
    
    123
    +                if complete != 'true':
    
    124
    +                    self.request_archive(self.get_query_id(), after=last)
    
    125
    +            if complete == 'true':
    
    126
    +                self.mam_query_id = None
    
    127
    +                if self.mam_query_date:
    
    128
    +                    gajim.config.set_per(
    
    129
    +                        'accounts', self.name,
    
    130
    +                        'mam_start_date', self.mam_query_date.timestamp())
    
    131
    +                    self.mam_query_date = None
    
    124 132
     
    
    125 133
         def _nec_mam_decrypted_message_received(self, obj):
    
    126 134
             if obj.conn.name != self.name:
    
    127 135
                 return
    
    128
    -        gajim.logger.save_if_not_exists(obj.with_, obj.direction, obj.tim,
    
    136
    +        gajim.logger.save_if_not_exists(obj.with_, obj.direction, obj.timestamp,
    
    129 137
                 msg=obj.msgtxt, nick=obj.nick, additional_data=obj.additional_data)
    
    130 138
     
    
    131
    -    def request_archive(self, start=None, end=None, with_=None, after=None,
    
    132
    -    max=30):
    
    133
    -        iq_ = nbxmpp.Iq('set')
    
    134
    -        query = iq_.addChild('query', namespace=self.archiving_namespace)
    
    135
    -        x = query.addChild(node=nbxmpp.DataForm(typ='submit'))
    
    136
    -        x.addChild(node=nbxmpp.DataField(typ='hidden', name='FORM_TYPE', value=self.archiving_namespace))
    
    139
    +    def get_query_id(self):
    
    140
    +        self.mam_query_id = self.connection.getAnID()
    
    141
    +        return self.mam_query_id
    
    142
    +
    
    143
    +    def request_archive_on_signin(self):
    
    144
    +        mam_id = gajim.config.get_per('accounts', self.name, 'last_mam_id')
    
    145
    +        query_id = self.get_query_id()
    
    146
    +        if mam_id:
    
    147
    +            self.request_archive(query_id, after=mam_id)
    
    148
    +        else:
    
    149
    +            # First Start, we request the last week
    
    150
    +            self.mam_query_date = datetime.utcnow() - timedelta(days=7)
    
    151
    +            log.info('First start: query archive start: %s', self.mam_query_date)
    
    152
    +            self.request_archive(query_id, start=self.mam_query_date)
    
    153
    +
    
    154
    +    def request_archive(self, query_id, start=None, end=None, with_=None,
    
    155
    +                        after=None, max_=30):
    
    156
    +        namespace = self.archiving_namespace
    
    157
    +        iq = nbxmpp.Iq('set')
    
    158
    +        query = iq.addChild('query', namespace=namespace)
    
    159
    +        form = query.addChild(node=nbxmpp.DataForm(typ='submit'))
    
    160
    +        field = nbxmpp.DataField(typ='hidden',
    
    161
    +                                 name='FORM_TYPE',
    
    162
    +                                 value=namespace)
    
    163
    +        form.addChild(node=field)
    
    137 164
             if start:
    
    138
    -            x.addChild(node=nbxmpp.DataField(typ='text-single', name='start', value=start))
    
    165
    +            field = nbxmpp.DataField(typ='text-single',
    
    166
    +                                     name='start',
    
    167
    +                                     value=start.strftime('%Y-%m-%dT%H:%M:%SZ'))
    
    168
    +            form.addChild(node=field)
    
    139 169
             if end:
    
    140
    -            x.addChild(node=nbxmpp.DataField(typ='text-single', name='end', value=end))
    
    170
    +            field = nbxmpp.DataField(typ='text-single',
    
    171
    +                                     name='end',
    
    172
    +                                     value=end.strftime('%Y-%m-%dT%H:%M:%SZ'))
    
    173
    +            form.addChild(node=field)
    
    141 174
             if with_:
    
    142
    -            x.addChild(node=nbxmpp.DataField(typ='jid-single', name='with', value=with_))
    
    175
    +            field = nbxmpp.DataField(typ='jid-single', name='with', value=with_)
    
    176
    +            form.addChild(node=field)
    
    177
    +
    
    143 178
             set_ = query.setTag('set', namespace=nbxmpp.NS_RSM)
    
    144
    -        set_.setTagData('max', max)
    
    179
    +        set_.setTagData('max', max_)
    
    145 180
             if after:
    
    146 181
                 set_.setTagData('after', after)
    
    147
    -        queryid_ = self.connection.getAnID()
    
    148
    -        query.setAttr('queryid', queryid_)
    
    182
    +        query.setAttr('queryid', query_id)
    
    149 183
             id_ = self.connection.getAnID()
    
    150
    -        iq_.setID(id_)
    
    151
    -        self.awaiting_answers[queryid_] = (MAM_RESULTS_ARRIVED, )
    
    152
    -        self.connection.send(iq_)
    
    184
    +        iq.setID(id_)
    
    185
    +        self.connection.send(iq)
    
    153 186
     
    
    154 187
         def request_archive_preferences(self):
    
    155 188
             if not gajim.account_is_connected(self.name):
    
    ... ... @@ -367,8 +400,7 @@ class ConnectionArchive136(ConnectionArchive):
    367 400
             return ['may']
    
    368 401
     
    
    369 402
         def _ArchiveCB(self, con, iq_obj):
    
    370
    -        log.debug('_ArchiveCB %s' % iq_obj.getType())
    
    371
    -        gajim.nec.push_incoming_event(ArchivingReceivedEvent(None, conn=self,
    
    403
    +        gajim.nec.push_incoming_event(ev.ArchivingReceivedEvent(None, conn=self,
    
    372 404
                 stanza=iq_obj))
    
    373 405
             raise nbxmpp.NodeProcessed
    
    374 406
     
    

  • gajim/common/nec.py
    ... ... @@ -154,8 +154,13 @@ class NetworkEvent(object):
    154 154
     
    
    155 155
         def _set_kwargs_as_attributes(self, **kwargs):
    
    156 156
             for k, v in kwargs.items():
    
    157
    -            setattr(self, k, v)
    
    157
    +            if k not in ('name', 'base_network_events'):
    
    158
    +                setattr(self, k, v)
    
    158 159
     
    
    160
    +    def _set_base_event_vars_as_attributes(self, event):
    
    161
    +        for k, v in vars(event).items():
    
    162
    +            if k not in ('name', 'base_network_events'):
    
    163
    +                setattr(self, k, v)
    
    159 164
     
    
    160 165
     class NetworkIncomingEvent(NetworkEvent):
    
    161 166
         base_network_events = []
    

  • gajim/gajim.py
    ... ... @@ -326,6 +326,7 @@ class GajimApplication(Gtk.Application):
    326 326
                 ('-profile', action.on_profile, 'feature', 's'),
    
    327 327
                 ('-xml-console', action.on_xml_console, 'always', 's'),
    
    328 328
                 ('-archive', action.on_archiving_preferences, 'feature', 's'),
    
    329
    +            ('-sync-history', action.on_history_sync, 'online', 's'),
    
    329 330
                 ('-privacylists', action.on_privacy_lists, 'feature', 's'),
    
    330 331
                 ('-send-server-message',
    
    331 332
                     action.on_send_server_message, 'online', 's'),
    

  • gajim/gui_interface.py
    ... ... @@ -1187,11 +1187,7 @@ class Interface:
    1187 1187
                     time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime()))
    
    1188 1188
             if obj.conn.archiving_313_supported and gajim.config.get_per('accounts',
    
    1189 1189
             account, 'sync_logs_with_server'):
    
    1190
    -            mam_id = gajim.config.get_per('accounts', account, 'last_mam_id')
    
    1191
    -            if mam_id:
    
    1192
    -                obj.conn.request_archive(after=mam_id)
    
    1193
    -            else:
    
    1194
    -                obj.conn.request_archive(start='2013-02-24T03:51:42Z')
    
    1190
    +            obj.conn.request_archive_on_signin()
    
    1195 1191
     
    
    1196 1192
             invisible_show = gajim.SHOW_LIST.index('invisible')
    
    1197 1193
             # We cannot join rooms if we are invisible
    

  • gajim/gui_menu_builder.py
    ... ... @@ -671,6 +671,7 @@ def get_account_menu(account):
    671 671
                 ('-start-single-chat', _('Send Single Message...')),
    
    672 672
                 ('Advanced', [
    
    673 673
                     ('-archive', _('Archiving Preferences')),
    
    674
    +                ('-sync-history', _('Synchronise History')),
    
    674 675
                     ('-privacylists', _('Privacy Lists')),
    
    675 676
                     ('-xml-console', _('XML Console'))
    
    676 677
                     ]),
    

  • gajim/history_sync.py
    1
    +# -*- coding: utf-8 -*-
    
    2
    +#
    
    3
    +# Copyright (C) 2017 Philipp Hörist <philipp AT hoerist.com>
    
    4
    +#
    
    5
    +# This file is part of Gajim.
    
    6
    +#
    
    7
    +# Gajim is free software: you can redistribute it and/or modify
    
    8
    +# it under the terms of the GNU General Public License as published by
    
    9
    +# the Free Software Foundation, either version 3 of the License, or
    
    10
    +# (at your option) any later version.
    
    11
    +#
    
    12
    +# Gajim is distributed in the hope that it will be useful,
    
    13
    +# but WITHOUT ANY WARRANTY; without even the implied warranty of
    
    14
    +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    
    15
    +# GNU General Public License for more details.
    
    16
    +#
    
    17
    +# You should have received a copy of the GNU General Public License
    
    18
    +# along with Gajim. If not, see <http://www.gnu.org/licenses/>.
    
    19
    +
    
    20
    +import logging
    
    21
    +from enum import IntEnum
    
    22
    +from datetime import datetime, timedelta, timezone
    
    23
    +
    
    24
    +import nbxmpp
    
    25
    +from gi.repository import Gtk, GLib
    
    26
    +
    
    27
    +from common import gajim
    
    28
    +from common import ged
    
    29
    +from gtkgui_helpers import get_icon_pixmap
    
    30
    +
    
    31
    +log = logging.getLogger('gajim.c.message_archiving')
    
    32
    +
    
    33
    +class Pages(IntEnum):
    
    34
    +    TIME = 0
    
    35
    +    SYNC = 1
    
    36
    +    SUMMARY = 2
    
    37
    +
    
    38
    +class ArchiveState(IntEnum):
    
    39
    +    NEVER = 0
    
    40
    +    ALL = 1
    
    41
    +
    
    42
    +class HistorySyncAssistant(Gtk.Assistant):
    
    43
    +    def __init__(self, account, parent):
    
    44
    +        Gtk.Assistant.__init__(self)
    
    45
    +        self.set_title(_('Synchronise History'))
    
    46
    +        self.set_resizable(False)
    
    47
    +        self.set_default_size(300, -1)
    
    48
    +        self.set_name('HistorySyncAssistant')
    
    49
    +        self.set_transient_for(parent)
    
    50
    +        self.account = account
    
    51
    +        self.con = gajim.connections[self.account]
    
    52
    +        self.timedelta = None
    
    53
    +        self.now = datetime.utcnow()
    
    54
    +        self.query_id = None
    
    55
    +        self.count_query_id = None
    
    56
    +        self.start = None
    
    57
    +        self.end = None
    
    58
    +        self.next = None
    
    59
    +        self.hide_buttons()
    
    60
    +
    
    61
    +        mam_start = gajim.config.get_per('accounts', account, 'mam_start_date')
    
    62
    +        if not mam_start or mam_start == ArchiveState.NEVER:
    
    63
    +            self.current_start = self.now
    
    64
    +        elif mam_start == ArchiveState.ALL:
    
    65
    +            self.current_start = datetime.utcfromtimestamp(0)
    
    66
    +        else:
    
    67
    +            self.current_start = datetime.fromtimestamp(mam_start)
    
    68
    +
    
    69
    +        self.select_time = SelectTimePage(self)
    
    70
    +        self.append_page(self.select_time)
    
    71
    +        self.set_page_type(self.select_time, Gtk.AssistantPageType.INTRO)
    
    72
    +
    
    73
    +        self.download_history = DownloadHistoryPage(self)
    
    74
    +        self.append_page(self.download_history)
    
    75
    +        self.set_page_type(self.download_history, Gtk.AssistantPageType.PROGRESS)
    
    76
    +        self.set_page_complete(self.download_history, True)
    
    77
    +
    
    78
    +        self.summary = SummaryPage(self)
    
    79
    +        self.append_page(self.summary)
    
    80
    +        self.set_page_type(self.summary, Gtk.AssistantPageType.SUMMARY)
    
    81
    +        self.set_page_complete(self.summary, True)
    
    82
    +
    
    83
    +        gajim.ged.register_event_handler('archiving-finished',
    
    84
    +                                         ged.PRECORE,
    
    85
    +                                         self._nec_archiving_finished)
    
    86
    +        gajim.ged.register_event_handler('mam-decrypted-message-received',
    
    87
    +                                         ged.PRECORE,
    
    88
    +                                         self._nec_mam_message_received)
    
    89
    +
    
    90
    +        self.connect('prepare', self.on_page_change)
    
    91
    +        self.connect('destroy', self.on_destroy)
    
    92
    +        self.connect("cancel", self.on_close_clicked)
    
    93
    +        self.connect("close", self.on_close_clicked)
    
    94
    +
    
    95
    +        if mam_start == ArchiveState.ALL:
    
    96
    +            self.set_current_page(Pages.SUMMARY)
    
    97
    +            self.summary.nothing_to_do()
    
    98
    +
    
    99
    +        if self.con.mam_query_id:
    
    100
    +            self.set_current_page(Pages.SUMMARY)
    
    101
    +            self.summary.query_already_running()
    
    102
    +
    
    103
    +        self.show_all()
    
    104
    +
    
    105
    +    def hide_buttons(self):
    
    106
    +        '''
    
    107
    +        Hide some of the standard buttons that are included in Gtk.Assistant
    
    108
    +        '''
    
    109
    +        if self.get_property('use-header-bar'):
    
    110
    +            action_area = self.get_children()[1]
    
    111
    +        else:
    
    112
    +            box = self.get_children()[0]
    
    113
    +            content_box = box.get_children()[1]
    
    114
    +            action_area = content_box.get_children()[1]
    
    115
    +        for button in action_area.get_children():
    
    116
    +            button_name = Gtk.Buildable.get_name(button)
    
    117
    +            if button_name == 'back':
    
    118
    +                button.connect('show', self._on_show_button)
    
    119
    +            elif button_name == 'forward':
    
    120
    +                self.next = button
    
    121
    +                button.connect('show', self._on_show_button)
    
    122
    +
    
    123
    +    @staticmethod
    
    124
    +    def _on_show_button(button):
    
    125
    +        button.hide()
    
    126
    +
    
    127
    +    def prepare_query(self):
    
    128
    +        if self.timedelta:
    
    129
    +            self.start = self.now - self.timedelta
    
    130
    +        self.end = self.current_start
    
    131
    +
    
    132
    +        log.info('config: get mam_start_date: %s', self.current_start)
    
    133
    +        log.info('now: %s', self.now)
    
    134
    +        log.info('start: %s', self.start)
    
    135
    +        log.info('end: %s', self.end)
    
    136
    +
    
    137
    +        self.query_count()
    
    138
    +
    
    139
    +    def query_count(self):
    
    140
    +        self.count_query_id = self.con.connection.getAnID()
    
    141
    +        self.con.request_archive(self.count_query_id,
    
    142
    +                                 start=self.start,
    
    143
    +                                 end=self.end,
    
    144
    +                                 max_=0)
    
    145
    +
    
    146
    +    def query_messages(self, last=None):
    
    147
    +        self.query_id = self.con.connection.getAnID()
    
    148
    +        self.con.request_archive(self.query_id,
    
    149
    +                                 start=self.start,
    
    150
    +                                 end=self.end,
    
    151
    +                                 after=last,
    
    152
    +                                 max_=30)
    
    153
    +
    
    154
    +    def on_row_selected(self, listbox, row):
    
    155
    +        self.timedelta = row.get_child().get_delta()
    
    156
    +        if row:
    
    157
    +            self.set_page_complete(self.select_time, True)
    
    158
    +        else:
    
    159
    +            self.set_page_complete(self.select_time, False)
    
    160
    +
    
    161
    +    def on_page_change(self, assistant, page):
    
    162
    +        if page == self.download_history:
    
    163
    +            self.next.hide()
    
    164
    +            self.prepare_query()
    
    165
    +
    
    166
    +    def on_destroy(self, *args):
    
    167
    +        gajim.ged.remove_event_handler('archiving-finished',
    
    168
    +                                       ged.PRECORE,
    
    169
    +                                       self._nec_archiving_finished)
    
    170
    +        gajim.ged.remove_event_handler('mam-decrypted-message-received',
    
    171
    +                                       ged.PRECORE,
    
    172
    +                                       self._nec_mam_message_received)
    
    173
    +        del gajim.interface.instances[self.account]['history_sync']
    
    174
    +
    
    175
    +    def on_close_clicked(self, *args):
    
    176
    +        self.destroy()
    
    177
    +
    
    178
    +    def _nec_mam_message_received(self, obj):
    
    179
    +        if obj.conn.name != self.account:
    
    180
    +            return
    
    181
    +
    
    182
    +        if obj.msg_obj.query_id != self.query_id:
    
    183
    +            return
    
    184
    +
    
    185
    +        log.debug('received message')
    
    186
    +        GLib.idle_add(self.download_history.set_fraction)
    
    187
    +
    
    188
    +    def _nec_archiving_finished(self, obj):
    
    189
    +        if obj.conn.name != self.account:
    
    190
    +            return
    
    191
    +
    
    192
    +        if obj.query_id not in (self.query_id, self.count_query_id):
    
    193
    +            return
    
    194
    +
    
    195
    +        set_ = obj.fin.getTag('set', namespace=nbxmpp.NS_RSM)
    
    196
    +        if not set_:
    
    197
    +            log.error('invalid result')
    
    198
    +            log.error(obj.fin)
    
    199
    +            return
    
    200
    +
    
    201
    +        if obj.query_id == self.count_query_id:
    
    202
    +            count = set_.getTagData('count')
    
    203
    +            log.info('message count received: %s', count)
    
    204
    +            if count:
    
    205
    +                self.download_history.count = int(count)
    
    206
    +            self.query_messages()
    
    207
    +            return
    
    208
    +
    
    209
    +        if obj.query_id == self.query_id:
    
    210
    +            last = set_.getTagData('last')
    
    211
    +            complete = obj.fin.getAttr('complete')
    
    212
    +            if not last and complete != 'true':
    
    213
    +                log.error('invalid result')
    
    214
    +                log.error(obj.fin)
    
    215
    +                return
    
    216
    +
    
    217
    +            if complete != 'true':
    
    218
    +                self.query_messages(last)
    
    219
    +            else:
    
    220
    +                log.info('query finished')
    
    221
    +                GLib.idle_add(self.download_history.finished)
    
    222
    +                if self.start:
    
    223
    +                    timestamp = self.start.timestamp()
    
    224
    +                else:
    
    225
    +                    timestamp = ArchiveState.ALL
    
    226
    +                gajim.config.set_per('accounts', self.account,
    
    227
    +                                     'mam_start_date', timestamp)
    
    228
    +                log.debug('config: set mam_start_date: %s', timestamp)
    
    229
    +                self.set_current_page(Pages.SUMMARY)
    
    230
    +                self.summary.finished()
    
    231
    +
    
    232
    +
    
    233
    +class SelectTimePage(Gtk.Box):
    
    234
    +    def __init__(self, assistant):
    
    235
    +        super().__init__(orientation=Gtk.Orientation.VERTICAL)
    
    236
    +        self.set_spacing(18)
    
    237
    +        self.assistant = assistant
    
    238
    +        label = Gtk.Label(label=_('How far back do you want to go?'))
    
    239
    +
    
    240
    +        listbox = Gtk.ListBox()
    
    241
    +        listbox.set_hexpand(False)
    
    242
    +        listbox.set_halign(Gtk.Align.CENTER)
    
    243
    +        listbox.add(TimeOption(_('One Month'), 1))
    
    244
    +        listbox.add(TimeOption(_('Three Months'), 3))
    
    245
    +        listbox.add(TimeOption(_('One Year'), 12))
    
    246
    +        listbox.add(TimeOption(_('Everything')))
    
    247
    +        listbox.connect('row-selected', assistant.on_row_selected)
    
    248
    +
    
    249
    +        for row in listbox.get_children():
    
    250
    +            option = row.get_child()
    
    251
    +            if not option.get_delta():
    
    252
    +                continue
    
    253
    +            if assistant.now - option.get_delta() > assistant.current_start:
    
    254
    +                row.set_activatable(False)
    
    255
    +                row.set_selectable(False)
    
    256
    +
    
    257
    +        self.pack_start(label, True, True, 0)
    
    258
    +        self.pack_start(listbox, False, False, 0)
    
    259
    +
    
    260
    +class DownloadHistoryPage(Gtk.Box):
    
    261
    +    def __init__(self, assistant):
    
    262
    +        super().__init__(orientation=Gtk.Orientation.VERTICAL)
    
    263
    +        self.set_spacing(18)
    
    264
    +        self.assistant = assistant
    
    265
    +        self.count = 0
    
    266
    +        self.received = 0
    
    267
    +
    
    268
    +        pix = get_icon_pixmap('folder-download-symbolic', size=64)
    
    269
    +        image = Gtk.Image()
    
    270
    +        image.set_from_pixbuf(pix)
    
    271
    +
    
    272
    +        self.progress = Gtk.ProgressBar()
    
    273
    +        self.progress.set_show_text(True)
    
    274
    +        self.progress.set_text(_('Connecting...'))
    
    275
    +        self.progress.set_pulse_step(0.1)
    
    276
    +        self.progress.set_vexpand(True)
    
    277
    +        self.progress.set_valign(Gtk.Align.CENTER)
    
    278
    +
    
    279
    +        self.pack_start(image, False, False, 0)
    
    280
    +        self.pack_start(self.progress, False, False, 0)
    
    281
    +
    
    282
    +    def set_fraction(self):
    
    283
    +        self.received += 1
    
    284
    +        if self.count:
    
    285
    +            self.progress.set_fraction(self.received / self.count)
    
    286
    +            self.progress.set_text(_('%s of %s' % (self.received, self.count)))
    
    287
    +        else:
    
    288
    +            self.progress.pulse()
    
    289
    +            self.progress.set_text(_('Downloaded %s Messages' % self.received))
    
    290
    +
    
    291
    +    def finished(self):
    
    292
    +        self.progress.set_fraction(1)
    
    293
    +
    
    294
    +class SummaryPage(Gtk.Box):
    
    295
    +    def __init__(self, assistant):
    
    296
    +        super().__init__(orientation=Gtk.Orientation.VERTICAL)
    
    297
    +        self.set_spacing(18)
    
    298
    +        self.assistant = assistant
    
    299
    +
    
    300
    +        self.label = Gtk.Label()
    
    301
    +        self.label.set_name('FinishedLabel')
    
    302
    +        self.label.set_valign(Gtk.Align.CENTER)
    
    303
    +
    
    304
    +        self.pack_start(self.label, True, True, 0)
    
    305
    +
    
    306
    +    def finished(self):
    
    307
    +        received = self.assistant.download_history.received
    
    308
    +        finished = _('''
    
    309
    +        Finshed synchronising your History. 
    
    310
    +        {received} Messages downloaded.
    
    311
    +        '''.format(received=received))
    
    312
    +        self.label.set_text(finished)
    
    313
    +
    
    314
    +    def nothing_to_do(self):
    
    315
    +        nothing_to_do = _('''
    
    316
    +        Gajim is fully synchronised 
    
    317
    +        with the Archive.
    
    318
    +        ''')
    
    319
    +        self.label.set_text(nothing_to_do)
    
    320
    +
    
    321
    +    def query_already_running(self):
    
    322
    +        already_running = _('''
    
    323
    +        There is already a synchronisation in 
    
    324
    +        progress. Please try later.
    
    325
    +        ''')
    
    326
    +        self.label.set_text(already_running)
    
    327
    +
    
    328
    +class TimeOption(Gtk.Label):
    
    329
    +    def __init__(self, label, months=None):
    
    330
    +        super().__init__(label=label)
    
    331
    +        self.date = months
    
    332
    +        if months:
    
    333
    +            self.date = timedelta(days=30*months)
    
    334
    +
    
    335
    +    def get_delta(self):
    
    336
    +        return self.date