Add a configurable threshold for MAM in MUC

This commit is contained in:
Philipp Hörist 2018-09-15 20:45:38 +02:00 committed by Philipp Hörist
parent 7ece7dbaff
commit d35a9f6a10
8 changed files with 168 additions and 43 deletions

View File

@ -293,6 +293,9 @@ class Config:
'pgp_encoding': [opt_str, '', _('Sets the encoding used by python-gnupg'), True], 'pgp_encoding': [opt_str, '', _('Sets the encoding used by python-gnupg'), True],
'remote_commands': [opt_bool, False, _('If true, Gajim will execute XEP-0146 Commands.')], 'remote_commands': [opt_bool, False, _('If true, Gajim will execute XEP-0146 Commands.')],
'dark_theme': [opt_int, 2, _('2: System, 1: Enabled, 0: Disabled')], 'dark_theme': [opt_int, 2, _('2: System, 1: Enabled, 0: Disabled')],
'threshold_options': [opt_str, '1, 2, 4, 10, 0', _('Options in days which can be chosen in the sync threshold menu'), True],
'public_room_sync_threshold': [opt_int, 1, _('Maximum history in days we request from a public room archive. 0: As much as possible')],
'private_room_sync_threshold': [opt_int, 0, _('Maximum history in days we request from a private room archive. 0: As much as possible')],
}, {}) # type: Tuple[Dict[str, List[Any]], Dict[Any, Any]] }, {}) # type: Tuple[Dict[str, List[Any]], Dict[Any, Any]]
__options_per_key = { __options_per_key = {

View File

@ -186,6 +186,13 @@ class Chatstate(IntEnum):
return self.name.lower() return self.name.lower()
class SyncThreshold(IntEnum):
NO_THRESHOLD = 0
def __str__(self):
return str(self.value)
ACTIVITIES = { ACTIVITIES = {
'doing_chores': { 'doing_chores': {
'category': _('Doing Chores'), 'category': _('Doing Chores'),

View File

@ -51,6 +51,7 @@ from gajim.common import configpaths
from gajim.common.i18n import Q_ from gajim.common.i18n import Q_
from gajim.common.i18n import _ from gajim.common.i18n import _
from gajim.common.i18n import ngettext from gajim.common.i18n import ngettext
from gajim.common.caps_cache import muc_caps_cache
try: try:
import precis_i18n.codec # pylint: disable=unused-import import precis_i18n.codec # pylint: disable=unused-import
@ -1481,3 +1482,13 @@ def call_counter(func):
self._connect_machine_calls += 1 self._connect_machine_calls += 1
return func(self, restart=False) return func(self, restart=False)
return helper return helper
def get_sync_threshold(jid, archive_info):
if archive_info is None or archive_info.sync_threshold is None:
if muc_caps_cache.supports(jid, 'muc#roomconfig_membersonly'):
threshold = app.config.get('private_room_sync_threshold')
else:
threshold = app.config.get('public_room_sync_threshold')
app.logger.set_archive_infos(jid, sync_threshold=threshold)
return threshold
return archive_info.sync_threshold

View File

@ -78,11 +78,12 @@ LOGS_SQL_STATEMENT = '''
jid_id INTEGER PRIMARY KEY UNIQUE, jid_id INTEGER PRIMARY KEY UNIQUE,
last_mam_id TEXT, last_mam_id TEXT,
oldest_mam_timestamp TEXT, oldest_mam_timestamp TEXT,
last_muc_timestamp TEXT last_muc_timestamp TEXT,
sync_threshold INTEGER
); );
CREATE INDEX idx_logs_jid_id_time ON logs (jid_id, time DESC); CREATE INDEX idx_logs_jid_id_time ON logs (jid_id, time DESC);
CREATE INDEX idx_logs_stanza_id ON logs (stanza_id); CREATE INDEX idx_logs_stanza_id ON logs (stanza_id);
PRAGMA user_version=1; PRAGMA user_version=2;
''' '''
CACHE_SQL_STATEMENT = ''' CACHE_SQL_STATEMENT = '''
@ -214,12 +215,16 @@ class Logger:
'''CREATE INDEX IF NOT EXISTS idx_logs_stanza_id '''CREATE INDEX IF NOT EXISTS idx_logs_stanza_id
ON logs(stanza_id)''', ON logs(stanza_id)''',
'PRAGMA user_version=1' 'PRAGMA user_version=1'
] ]
self._execute_multiple(con, statements) self._execute_multiple(con, statements)
if self._get_user_version(con) < 2: if self._get_user_version(con) < 2:
pass statements = [
'ALTER TABLE last_archive_message ADD COLUMN "sync_threshold" INTEGER',
'PRAGMA user_version=2'
]
self._execute_multiple(con, statements)
def _migrate_cache(self, con): def _migrate_cache(self, con):
if self._get_user_version(con) == 0: if self._get_user_version(con) == 0:
@ -1394,20 +1399,20 @@ class Logger:
self._con.execute(sql, (sha, account_jid_id, jid_id)) self._con.execute(sql, (sha, account_jid_id, jid_id))
self._timeout_commit() self._timeout_commit()
def get_archive_timestamp(self, jid, type_=None): def get_archive_infos(self, jid):
""" """
Get the last archive id/timestamp for a jid Get the archive infos
:param jid: The jid that belongs to the avatar :param jid: The jid that belongs to the avatar
""" """
jid_id = self.get_jid_id(jid, type_=type_) jid_id = self.get_jid_id(jid, type_=JIDConstant.ROOM_TYPE)
sql = '''SELECT * FROM last_archive_message WHERE jid_id = ?''' sql = '''SELECT * FROM last_archive_message WHERE jid_id = ?'''
return self._con.execute(sql, (jid_id,)).fetchone() return self._con.execute(sql, (jid_id,)).fetchone()
def set_archive_timestamp(self, jid, **kwargs): def set_archive_infos(self, jid, **kwargs):
""" """
Set the last archive id/timestamp Set archive infos
:param jid: The jid that belongs to the avatar :param jid: The jid that belongs to the avatar
@ -1419,20 +1424,28 @@ class Logger:
:param last_muc_timestamp: The timestamp of the last message we :param last_muc_timestamp: The timestamp of the last message we
received in a MUC received in a MUC
:param sync_threshold: The max days that we request from a
MUC archive
""" """
jid_id = self.get_jid_id(jid) jid_id = self.get_jid_id(jid)
exists = self.get_archive_timestamp(jid) exists = self.get_archive_infos(jid)
if not exists: if not exists:
sql = '''INSERT INTO last_archive_message VALUES (?, ?, ?, ?)''' sql = '''INSERT INTO last_archive_message
(jid_id, last_mam_id, oldest_mam_timestamp,
last_muc_timestamp, sync_threshold)
VALUES (?, ?, ?, ?, ?)'''
self._con.execute(sql, ( self._con.execute(sql, (
jid_id, jid_id,
kwargs.get('last_mam_id', None), kwargs.get('last_mam_id', None),
kwargs.get('oldest_mam_timestamp', None), kwargs.get('oldest_mam_timestamp', None),
kwargs.get('last_muc_timestamp', None))) kwargs.get('last_muc_timestamp', None),
kwargs.get('sync_threshold', None)
))
else: else:
args = ' = ?, '.join(kwargs.keys()) + ' = ?' args = ' = ?, '.join(kwargs.keys()) + ' = ?'
sql = '''UPDATE last_archive_message SET {} sql = '''UPDATE last_archive_message SET {}
WHERE jid_id = ?'''.format(args) WHERE jid_id = ?'''.format(args)
self._con.execute(sql, tuple(kwargs.values()) + (jid_id,)) self._con.execute(sql, tuple(kwargs.values()) + (jid_id,))
log.info('Save archive timestamps: %s', kwargs) log.info('Save archive infos: %s', kwargs)
self._timeout_commit() self._timeout_commit()

View File

@ -15,14 +15,18 @@
# XEP-0313: Message Archive Management # XEP-0313: Message Archive Management
import logging import logging
import time
from datetime import datetime, timedelta from datetime import datetime, timedelta
import nbxmpp import nbxmpp
from gajim.common import app from gajim.common import app
from gajim.common.nec import NetworkIncomingEvent from gajim.common.nec import NetworkIncomingEvent
from gajim.common.const import ArchiveState, JIDConstant, KindConstant from gajim.common.const import ArchiveState
from gajim.common.const import KindConstant
from gajim.common.const import SyncThreshold
from gajim.common.caps_cache import muc_caps_cache from gajim.common.caps_cache import muc_caps_cache
from gajim.common.helpers import get_sync_threshold
from gajim.common.modules.misc import parse_delay from gajim.common.modules.misc import parse_delay
from gajim.common.modules.misc import parse_oob from gajim.common.modules.misc import parse_oob
from gajim.common.modules.misc import parse_correction from gajim.common.modules.misc import parse_correction
@ -352,7 +356,7 @@ class MAM:
log.warning('MAM request for %s already running', own_jid) log.warning('MAM request for %s already running', own_jid)
return return
archive = app.logger.get_archive_timestamp(own_jid) archive = app.logger.get_archive_infos(own_jid)
# Migration of last_mam_id from config to DB # Migration of last_mam_id from config to DB
if archive is not None: if archive is not None:
@ -379,16 +383,12 @@ class MAM:
self._send_archive_query(query, query_id, start_date) self._send_archive_query(query, query_id, start_date)
def request_archive_on_muc_join(self, jid): def request_archive_on_muc_join(self, jid):
archive = app.logger.get_archive_timestamp( archive = app.logger.get_archive_infos(jid)
jid, type_=JIDConstant.ROOM_TYPE) threshold = get_sync_threshold(jid, archive)
log.info('Threshold for %s: %s', jid, threshold)
query_id = self._get_query_id(jid) query_id = self._get_query_id(jid)
start_date = None start_date = None
if archive is not None: if archive is None or archive.last_mam_id is 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 # First Start, we dont request history
# Depending on what a MUC saves, there could be thousands # Depending on what a MUC saves, there could be thousands
# of Messages even in just one day. # of Messages even in just one day.
@ -397,6 +397,37 @@ class MAM:
query = self._get_archive_query( query = self._get_archive_query(
query_id, jid=jid, start=start_date) query_id, jid=jid, start=start_date)
elif threshold == SyncThreshold.NO_THRESHOLD:
# Not our first join and no threshold set
log.info('Request from archive: %s, after mam-id %s',
jid, archive.last_mam_id)
query = self._get_archive_query(
query_id, jid=jid, after=archive.last_mam_id)
else:
# Not our first join, check how much time elapsed since our
# last join and check against threshold
last_timestamp = archive.last_muc_timestamp
if last_timestamp is None:
log.info('No last muc timestamp found ( mam:1? )')
last_timestamp = 0
last = datetime.utcfromtimestamp(float(last_timestamp))
if datetime.utcnow() - last > timedelta(days=threshold):
# To much time has elapsed since last join, apply threshold
start_date = datetime.utcnow() - timedelta(days=threshold)
log.info('Too much time elapsed since last join, '
'request from: %s, threshold: %s',
start_date, threshold)
query = self._get_archive_query(
query_id, jid=jid, start=start_date)
else:
# Request from last mam-id
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)
if jid in self._catch_up_finished: if jid in self._catch_up_finished:
self._catch_up_finished.remove(jid) self._catch_up_finished.remove(jid)
self._send_archive_query(query, query_id, start_date, groupchat=True) self._send_archive_query(query, query_id, start_date, groupchat=True)
@ -424,20 +455,22 @@ class MAM:
return return
complete = fin.getAttr('complete') complete = fin.getAttr('complete')
app.logger.set_archive_timestamp(
jid, last_mam_id=last, last_muc_timestamp=None)
if complete != 'true': if complete != 'true':
app.logger.set_archive_infos(jid, last_mam_id=last)
self._mam_query_ids.pop(jid) self._mam_query_ids.pop(jid)
query_id = self._get_query_id(jid) query_id = self._get_query_id(jid)
query = self._get_archive_query(query_id, jid=jid, after=last) query = self._get_archive_query(query_id, jid=jid, after=last)
self._send_archive_query(query, query_id, groupchat=groupchat) self._send_archive_query(query, query_id, groupchat=groupchat)
else: else:
self._mam_query_ids.pop(jid) self._mam_query_ids.pop(jid)
if start_date is not None: app.logger.set_archive_infos(
app.logger.set_archive_timestamp( jid, last_mam_id=last, last_muc_timestamp=time.time())
jid, if start_date is not None and not groupchat:
last_mam_id=last, # Record the earliest timestamp we request from
oldest_mam_timestamp=start_date.timestamp()) # the account archive. For the account archive we only
# set start_date at the very first request.
app.logger.set_archive_infos(
jid, oldest_mam_timestamp=start_date.timestamp())
self._catch_up_finished.append(jid) self._catch_up_finished.append(jid)
log.info('End of MAM query, last mam id: %s', last) log.info('End of MAM query, last mam id: %s', last)
@ -481,7 +514,7 @@ class MAM:
if last is None: if last is None:
app.nec.push_incoming_event(ArchivingIntervalFinished( app.nec.push_incoming_event(ArchivingIntervalFinished(
None, query_id=query_id)) None, query_id=query_id))
app.logger.set_archive_timestamp( app.logger.set_archive_infos(
jid, oldest_mam_timestamp=timestamp) jid, oldest_mam_timestamp=timestamp)
log.info('End of MAM request, no items retrieved') log.info('End of MAM request, no items retrieved')
return return
@ -491,7 +524,7 @@ class MAM:
self.request_archive_interval(start_date, end_date, last, query_id) self.request_archive_interval(start_date, end_date, last, query_id)
else: else:
log.info('Request finished') log.info('Request finished')
app.logger.set_archive_timestamp( app.logger.set_archive_infos(
jid, oldest_mam_timestamp=timestamp) jid, oldest_mam_timestamp=timestamp)
app.nec.push_incoming_event(ArchivingIntervalFinished( app.nec.push_incoming_event(ArchivingIntervalFinished(
None, query_id=query_id)) None, query_id=query_id))
@ -536,15 +569,18 @@ class MAM:
return iq return iq
def save_archive_id(self, jid, stanza_id, timestamp): def save_archive_id(self, jid, stanza_id, timestamp):
if stanza_id is None:
return
if jid is None: if jid is None:
jid = self._con.get_own_jid().getStripped() jid = self._con.get_own_jid().getStripped()
if jid not in self._catch_up_finished: if jid not in self._catch_up_finished:
return return
log.info('Save: %s: %s, %s', jid, stanza_id, timestamp) log.info('Save: %s: %s, %s', jid, stanza_id, timestamp)
app.logger.set_archive_timestamp( if stanza_id is None:
jid, last_mam_id=stanza_id, last_muc_timestamp=timestamp) # mam:1
app.logger.set_archive_infos(jid, last_muc_timestamp=timestamp)
else:
# mam:2
app.logger.set_archive_infos(
jid, last_mam_id=stanza_id, last_muc_timestamp=timestamp)
def request_mam_preferences(self): def request_mam_preferences(self):
log.info('Request MAM preferences') log.info('Request MAM preferences')

View File

@ -60,6 +60,7 @@ from gajim.common import i18n
from gajim.common import contacts from gajim.common import contacts
from gajim.common.const import StyleAttr from gajim.common.const import StyleAttr
from gajim.common.const import Chatstate from gajim.common.const import Chatstate
from gajim.chat_control import ChatControl from gajim.chat_control import ChatControl
from gajim.chat_control_base import ChatControlBase from gajim.chat_control_base import ChatControlBase
@ -548,7 +549,7 @@ class GroupchatControl(ChatControlBase):
('request-voice-', self._on_request_voice), ('request-voice-', self._on_request_voice),
('execute-command-', self._on_execute_command), ('execute-command-', self._on_execute_command),
('upload-avatar-', self._on_upload_avatar), ('upload-avatar-', self._on_upload_avatar),
] ]
for action in actions: for action in actions:
action_name, func = action action_name, func = action
@ -575,6 +576,17 @@ class GroupchatControl(ChatControlBase):
act.connect('change-state', self._on_notify_on_all_messages) act.connect('change-state', self._on_notify_on_all_messages)
self.parent_win.window.add_action(act) self.parent_win.window.add_action(act)
archive_info = app.logger.get_archive_infos(self.contact.jid)
threshold = helpers.get_sync_threshold(self.contact.jid,
archive_info)
inital = GLib.Variant.new_string(str(threshold))
act = Gio.SimpleAction.new_stateful(
'choose-sync-' + self.control_id,
inital.get_type(), inital)
act.connect('change-state', self._on_sync_threshold)
self.parent_win.window.add_action(act)
def update_actions(self): def update_actions(self):
if self.parent_win is None: if self.parent_win is None:
return return
@ -638,6 +650,25 @@ class GroupchatControl(ChatControlBase):
win.lookup_action('upload-avatar-' + self.control_id).set_enabled( win.lookup_action('upload-avatar-' + self.control_id).set_enabled(
self.is_connected and vcard_support and contact.affiliation == 'owner') self.is_connected and vcard_support and contact.affiliation == 'owner')
# Sync Threshold
has_mam = muc_caps_cache.has_mam(self.room_jid)
win.lookup_action('choose-sync-' + self.control_id).set_enabled(has_mam)
def _on_room_created(self):
if self.parent_win is None:
return
win = self.parent_win.window
self.update_actions()
# After the room has been created, reevaluate threshold
if muc_caps_cache.has_mam(self.contact.jid):
archive_info = app.logger.get_archive_infos(self.contact.jid)
threshold = helpers.get_sync_threshold(self.contact.jid,
archive_info)
win.change_action_state('choose-sync-%s' % self.control_id,
GLib.Variant('s', str(threshold)))
def _connect_window_state_change(self, parent_win): def _connect_window_state_change(self, parent_win):
if self._state_change_handler_id is None: if self._state_change_handler_id is None:
id_ = parent_win.window.connect('notify::is-maximized', id_ = parent_win.window.connect('notify::is-maximized',
@ -755,6 +786,11 @@ class GroupchatControl(ChatControlBase):
app.config.set_per('rooms', self.contact.jid, app.config.set_per('rooms', self.contact.jid,
'notify_on_all_messages', param.get_boolean()) 'notify_on_all_messages', param.get_boolean())
def _on_sync_threshold(self, action, param):
threshold = param.get_string()
action.set_state(param)
app.logger.set_archive_infos(self.contact.jid, sync_threshold=threshold)
def _on_execute_command(self, action, param): def _on_execute_command(self, action, param):
""" """
Execute AdHoc commands on the current room Execute AdHoc commands on the current room
@ -1838,7 +1874,7 @@ class GroupchatControl(ChatControlBase):
self.print_conversation(_('Room logging is enabled')) self.print_conversation(_('Room logging is enabled'))
if '201' in obj.status_code: if '201' in obj.status_code:
app.connections[self.account].get_module('Discovery').disco_muc( app.connections[self.account].get_module('Discovery').disco_muc(
self.room_jid, self.update_actions, update=True) self.room_jid, self._on_room_created, update=True)
self.print_conversation(_('A new room has been created')) self.print_conversation(_('A new room has been created'))
if '210' in obj.status_code: if '210' in obj.status_code:
self.print_conversation(\ self.print_conversation(\

View File

@ -53,7 +53,7 @@ class HistorySyncAssistant(Gtk.Assistant):
own_jid = self.con.get_own_jid().getStripped() own_jid = self.con.get_own_jid().getStripped()
mam_start = ArchiveState.NEVER mam_start = ArchiveState.NEVER
archive = app.logger.get_archive_timestamp(own_jid) archive = app.logger.get_archive_infos(own_jid)
if archive is not None and archive.oldest_mam_timestamp is not None: if archive is not None and archive.oldest_mam_timestamp is not None:
mam_start = int(float(archive.oldest_mam_timestamp)) mam_start = int(float(archive.oldest_mam_timestamp))

View File

@ -23,6 +23,7 @@ from gajim import message_control
from gajim.gtkgui_helpers import get_action from gajim.gtkgui_helpers import get_action
from gajim.common import app from gajim.common import app
from gajim.common import helpers from gajim.common import helpers
from gajim.common.i18n import ngettext
def build_resources_submenu(contacts, account, action, room_jid=None, def build_resources_submenu(contacts, account, action, room_jid=None,
@ -634,7 +635,8 @@ def get_groupchat_menu(control_id):
('win.configure-', _('Configure Room')), ('win.configure-', _('Configure Room')),
('win.upload-avatar-', _('Upload Avatar…')), ('win.upload-avatar-', _('Upload Avatar…')),
('win.destroy-', _('Destroy Room')), ('win.destroy-', _('Destroy Room')),
]), ]),
(_('Sync Threshold'), []),
('win.change-nick-', _('Change Nick')), ('win.change-nick-', _('Change Nick')),
('win.bookmark-', _('Bookmark Room')), ('win.bookmark-', _('Bookmark Room')),
('win.request-voice-', _('Request Voice')), ('win.request-voice-', _('Request Voice')),
@ -643,7 +645,7 @@ def get_groupchat_menu(control_id):
('win.execute-command-', _('Execute command')), ('win.execute-command-', _('Execute command')),
('win.browse-history-', _('History')), ('win.browse-history-', _('History')),
('win.disconnect-', _('Disconnect')), ('win.disconnect-', _('Disconnect')),
] ]
def build_menu(preset): def build_menu(preset):
menu = Gio.Menu() menu = Gio.Menu()
@ -656,11 +658,28 @@ def get_groupchat_menu(control_id):
menu.append(label, action_name + control_id) menu.append(label, action_name + control_id)
else: else:
label, sub_menu = item label, sub_menu = item
# This is a submenu if not sub_menu:
submenu = build_menu(sub_menu) # Sync threshold menu
submenu = build_sync_menu()
else:
# This is a submenu
submenu = build_menu(sub_menu)
menu.append_submenu(label, submenu) menu.append_submenu(label, submenu)
return menu return menu
def build_sync_menu():
menu = Gio.Menu()
days = app.config.get('threshold_options').split(',')
days = [int(day) for day in days]
action_name = 'win.choose-sync-%s::' % control_id
for day in days:
if day == 0:
label = _('No threshold')
else:
label = ngettext('%i day', '%i days', day, day, day)
menu.append(label, '%s%s' % (action_name, day))
return menu
return build_menu(groupchat_menu) return build_menu(groupchat_menu)