[Git][gajim/gajim][master] 2 commits: Refactor MAM into own module
Philipp Hörist
gitlab at dev.gajim.org
Sun Jul 15 14:37:03 CEST 2018
Philipp Hörist pushed to branch master at gajim / gajim
Commits:
ebbe06d5 by Philipp Hörist at 2018-07-15T14:26:00+02:00
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
- - - - -
dd664643 by Philipp Hörist at 2018-07-15T14:32:08+02:00
Move History Sync Dialog into gtk module
- - - - -
25 changed files:
- gajim/app_actions.py
- gajim/application.py
- gajim/chat_control.py
- gajim/common/app.py
- gajim/common/config.py
- gajim/common/connection.py
- gajim/common/connection_handlers.py
- gajim/common/connection_handlers_events.py
- gajim/common/helpers.py
- gajim/common/logger.py
- − gajim/common/message_archiving.py
- + gajim/common/modules/date_and_time.py
- + gajim/common/modules/mam.py
- + gajim/common/modules/misc.py
- − gajim/data/gui/archiving_313_preferences_item.ui
- gajim/data/gui/archiving_313_preferences_window.ui → gajim/data/gui/mam_preferences.ui
- gajim/dialogs.py
- gajim/groupchat_control.py
- + gajim/gtk/__init__.py
- gajim/history_sync.py → gajim/gtk/history_sync.py
- + gajim/gtk/mam_preferences.py
- + gajim/gtk/util.py
- gajim/gui_interface.py
- gajim/roster_window.py
- gajim/server_info.py
Changes:
=====================================
gajim/app_actions.py
=====================================
--- a/gajim/app_actions.py
+++ b/gajim/app_actions.py
@@ -27,8 +27,9 @@ from gajim import accounts_window
import gajim.plugins.gui
from gajim import history_window
from gajim import disco
-from gajim.history_sync import HistorySyncAssistant
+from gajim.gtk.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):
=====================================
gajim/application.py
=====================================
--- a/gajim/application.py
+++ b/gajim/application.py
@@ -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',
=====================================
gajim/chat_control.py
=====================================
--- a/gajim/chat_control.py
+++ b/gajim/chat_control.py
@@ -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:
=====================================
gajim/common/app.py
=====================================
--- a/gajim/common/app.py
+++ b/gajim/common/app.py
@@ -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
=====================================
gajim/common/config.py
=====================================
--- a/gajim/common/config.py
+++ b/gajim/common/config.py
@@ -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 = {
=====================================
gajim/common/connection.py
=====================================
--- a/gajim/common/connection.py
+++ b/gajim/common/connection.py
@@ -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':
=====================================
gajim/common/connection_handlers.py
=====================================
--- a/gajim/common/connection_handlers.py
+++ b/gajim/common/connection_handlers.py
@@ -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)
=====================================
gajim/common/connection_handlers_events.py
=====================================
--- a/gajim/common/connection_handlers_events.py
+++ b/gajim/common/connection_handlers_events.py
@@ -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 = []
=====================================
gajim/common/helpers.py
=====================================
--- a/gajim/common/helpers.py
+++ b/gajim/common/helpers.py
@@ -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(',')
=====================================
gajim/common/logger.py
=====================================
--- a/gajim/common/logger.py
+++ b/gajim/common/logger.py
@@ -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):
=====================================
gajim/common/message_archiving.py deleted
=====================================
--- a/gajim/common/message_archiving.py
+++ /dev/null
@@ -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
=====================================
gajim/common/modules/date_and_time.py
=====================================
--- /dev/null
+++ b/gajim/common/modules/date_and_time.py
@@ -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
=====================================
gajim/common/modules/mam.py
=====================================
--- /dev/null
+++ b/gajim/common/modules/mam.py
@@ -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'
=====================================
gajim/common/modules/misc.py
=====================================
--- /dev/null
+++ b/gajim/common/modules/misc.py
@@ -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)
=====================================
gajim/data/gui/archiving_313_preferences_item.ui deleted
=====================================
--- a/gajim/data/gui/archiving_313_preferences_item.ui
+++ /dev/null
@@ -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>
=====================================
gajim/data/gui/archiving_313_preferences_window.ui → gajim/data/gui/mam_preferences.ui
=====================================
--- a/gajim/data/gui/archiving_313_preferences_window.ui
+++ b/gajim/data/gui/mam_preferences.ui
@@ -1,57 +1,169 @@
<?xml version="1.0" encoding="UTF-8"?>
-<!-- Generated with glade 3.20.0 -->
+<!-- Generated with glade 3.22.1 -->
<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">
+ <object class="GtkListStore" id="default_store">
<columns>
- <!-- column-name jid -->
+ <!-- column-name text -->
<column type="gchararray"/>
- <!-- column-name archive_pref -->
- <column type="gchararray"/>
- </columns>
- </object>
- <object class="GtkListStore" id="default_pref_liststore">
- <columns>
- <!-- column-name gchararray1 -->
+ <!-- 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="GtkImage" id="remove_image">
- <property name="visible">True</property>
- <property name="can_focus">False</property>
- <property name="stock">gtk-remove</property>
+ <object class="GtkListStore" id="preferences_store">
+ <columns>
+ <!-- column-name jid -->
+ <column type="gchararray"/>
+ <!-- column-name gboolean1 -->
+ <column type="gboolean"/>
+ </columns>
</object>
- <object class="GtkWindow" id="archiving_313_pref">
+ <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="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"/>
+ <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" id="pref_grid">
+ <object class="GtkGrid">
<property name="visible">True</property>
<property name="can_focus">False</property>
- <property name="row_spacing">5</property>
- <property name="column_spacing">10</property>
+ <property name="halign">start</property>
+ <property name="column_spacing">5</property>
<child>
- <object class="GtkScrolledWindow" id="scrolledwindow1">
+ <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>
@@ -59,21 +171,27 @@
<property name="vexpand">True</property>
<property name="shadow_type">in</property>
<child>
- <object class="GtkTreeView" id="archive_view">
+ <object class="GtkTreeView" id="pref_view">
<property name="visible">True</property>
<property name="can_focus">True</property>
- <property name="model">archive_items_liststore</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"/>
+ <object class="GtkCellRendererText" id="cellrenderertext3">
+ <property name="editable">True</property>
+ <property name="placeholder_text">user at example.org</property>
+ <signal name="edited" handler="_jid_edited" swapped="no"/>
+ </object>
<attributes>
<attribute name="text">0</attribute>
</attributes>
@@ -82,14 +200,17 @@
</child>
<child>
<object class="GtkTreeViewColumn" id="treeviewcolumn2">
- <property name="title" translatable="yes">Preference</property>
+ <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="GtkCellRendererText" id="cellrenderertext4"/>
+ <object class="GtkCellRendererToggle">
+ <signal name="toggled" handler="_pref_toggled" swapped="no"/>
+ </object>
<attributes>
- <attribute name="text">1</attribute>
+ <attribute name="active">1</attribute>
</attributes>
</child>
</object>
@@ -98,117 +219,18 @@
</child>
</object>
<packing>
- <property name="left_attach">0</property>
- <property name="top_attach">1</property>
- <property name="width">2</property>
+ <property name="index">-1</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>
+ <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>
=====================================
gajim/dialogs.py
=====================================
--- a/gajim/dialogs.py
+++ b/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
=====================================
gajim/groupchat_control.py
=====================================
--- a/gajim/groupchat_control.py
+++ b/gajim/groupchat_control.py
@@ -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)
=====================================
gajim/gtk/__init__.py
=====================================
--- /dev/null
+++ b/gajim/gtk/__init__.py
=====================================
gajim/history_sync.py → gajim/gtk/history_sync.py
=====================================
--- a/gajim/history_sync.py
+++ b/gajim/gtk/history_sync.py
@@ -1,17 +1,14 @@
-# -*- coding: utf-8 -*-
-#
# Copyright (C) 2017 Philipp Hörist <philipp AT hoerist.com>
#
# 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, either version 3 of the License, or
-# (at your option) any later version.
+# 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
+# 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
@@ -25,8 +22,8 @@ from gi.repository import Gtk, GLib
from gajim.common import app
from gajim.common import ged
-from gajim.gtkgui_helpers import get_icon_pixmap
from gajim.common.const import ArchiveState
+from gajim.gtk.util import load_icon
log = logging.getLogger('gajim.c.message_archiving')
@@ -40,7 +37,7 @@ class Pages(IntEnum):
class HistorySyncAssistant(Gtk.Assistant):
def __init__(self, account, parent):
Gtk.Assistant.__init__(self)
- self.set_title(_('Synchronise History'))
+ # self.set_title(_('Synchronise History'))
self.set_resizable(False)
self.set_default_size(300, -1)
self.set_name('HistorySyncAssistant')
@@ -54,7 +51,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 +84,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)
@@ -107,10 +100,6 @@ class HistorySyncAssistant(Gtk.Assistant):
self.set_current_page(Pages.SUMMARY)
self.summary.nothing_to_do()
- # if self.con.mam_query_ids:
- # self.set_current_page(Pages.SUMMARY)
- # self.summary.query_already_running()
-
self.show_all()
def hide_buttons(self):
@@ -145,33 +134,34 @@ 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:
+ def _nec_mam_message_received(self, event):
+ if event.conn.name != self.account:
return
- if obj.result.getAttr('queryid') != self.query_id:
+ result = event.stanza.getTag('result')
+ queryid = result.getAttr('queryid')
+ if queryid != self.query_id:
return
log.debug('received message')
@@ -193,9 +183,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)
@@ -244,9 +231,8 @@ class DownloadHistoryPage(Gtk.Box):
self.count = 0
self.received = 0
- pix = get_icon_pixmap('folder-download-symbolic', size=64)
- image = Gtk.Image()
- image.set_from_pixbuf(pix)
+ surface = load_icon('folder-download-symbolic', self, size=64)
+ image = Gtk.Image.new_from_surface(surface)
self.progress = Gtk.ProgressBar()
self.progress.set_show_text(True)
@@ -312,7 +298,7 @@ class TimeOption(Gtk.Label):
super().__init__(label=label)
self.date = months
if months:
- self.date = timedelta(days=30*months)
+ self.date = timedelta(days=30 * months)
def get_delta(self):
return self.date
=====================================
gajim/gtk/mam_preferences.py
=====================================
--- /dev/null
+++ b/gajim/gtk/mam_preferences.py
@@ -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)
=====================================
gajim/gtk/util.py
=====================================
--- /dev/null
+++ b/gajim/gtk/util.py
@@ -0,0 +1,77 @@
+# 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
+import logging
+
+from gi.repository import Gtk
+from gi.repository import GLib
+import xml.etree.ElementTree as ET
+
+from gajim.common import i18n
+from gajim.common import configpaths
+
+_icon_theme = Gtk.IconTheme.get_default()
+_icon_theme.append_search_path(configpaths.get('ICONS'))
+
+log = logging.getLogger('gajim.gtk.util')
+
+
+def load_icon(icon_name, widget, size=16,
+ flags=Gtk.IconLookupFlags.FORCE_SIZE):
+
+ scale = widget.get_scale_factor()
+ if not scale:
+ log.warning('Could not determine scale factor')
+ scale = 1
+
+ try:
+ iconinfo = _icon_theme.lookup_icon_for_scale(
+ icon_name, size, scale, flags)
+ return iconinfo.load_surface(None)
+ except GLib.GError as e:
+ log.error('Unable to load icon %s: %s', icon_name, str(e))
+
+
+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)
=====================================
gajim/gui_interface.py
=====================================
--- a/gajim/gui_interface.py
+++ b/gajim/gui_interface.py
@@ -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
=====================================
gajim/roster_window.py
=====================================
--- a/gajim/roster_window.py
+++ b/gajim/roster_window.py
@@ -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)
=====================================
gajim/server_info.py
=====================================
--- a/gajim/server_info.py
+++ b/gajim/server_info.py
@@ -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,
View it on GitLab: https://dev.gajim.org/gajim/gajim/compare/72ee9af79c79e95c05a9b1a11f5f2a7f11e088c1...dd664643bd72281b2c777527db692f44338d3d29
--
View it on GitLab: https://dev.gajim.org/gajim/gajim/compare/72ee9af79c79e95c05a9b1a11f5f2a7f11e088c1...dd664643bd72281b2c777527db692f44338d3d29
You're receiving this email because of your account on dev.gajim.org.
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.gajim.org/pipermail/commits/attachments/20180715/0e96dad1/attachment-0001.html>
More information about the Commits
mailing list