diff options
author | lovetox <philipp@hoerist.com> | 2022-08-26 22:34:04 +0300 |
---|---|---|
committer | lovetox <philipp@hoerist.com> | 2022-08-31 22:46:40 +0300 |
commit | 5de6f7ea075b2a728f42d0b2f779d15963315e73 (patch) | |
tree | 77b6363164c3f4f118c14c164ca337773d8c6b4d | |
parent | 307b7bafdec7102c7e5a513893f4ef1113a18039 (diff) |
new: Add event storagearchive_cache
-rw-r--r-- | gajim/common/app.py | 2 | ||||
-rw-r--r-- | gajim/common/application.py | 4 | ||||
-rw-r--r-- | gajim/common/events.py | 101 | ||||
-rw-r--r-- | gajim/common/modules/contacts.py | 61 | ||||
-rw-r--r-- | gajim/common/modules/muc.py | 64 | ||||
-rw-r--r-- | gajim/common/storage/base.py | 21 | ||||
-rw-r--r-- | gajim/common/storage/events.py | 132 | ||||
-rw-r--r-- | gajim/gtk/chat_banner.py | 10 | ||||
-rw-r--r-- | gajim/gtk/chat_stack.py | 9 | ||||
-rw-r--r-- | gajim/gtk/control.py | 657 | ||||
-rw-r--r-- | gajim/gtk/conversation/view.py | 2 | ||||
-rw-r--r-- | gajim/gtk/groupchat_nick_completion.py | 8 | ||||
-rw-r--r-- | gajim/gtk/groupchat_roster.py | 8 |
13 files changed, 755 insertions, 324 deletions
diff --git a/gajim/common/app.py b/gajim/common/app.py index b27ed70bb..fa2155e97 100644 --- a/gajim/common/app.py +++ b/gajim/common/app.py @@ -59,6 +59,7 @@ if typing.TYPE_CHECKING: from gajim.gui.application import GajimApplication from gajim.common.storage.cache import CacheStorage from gajim.common.storage.archive import MessageArchiveStorage + from gajim.common.storage.events import EventStorage from gajim.common.cert_store import CertificateStore from gajim.common.call_manager import CallManager from gajim.common.preview import PreviewManager @@ -88,6 +89,7 @@ class Storage: def __init__(self): self.cache: CacheStorage = None self.archive: MessageArchiveStorage = None + self.events: EventStorage = None storage = Storage() diff --git a/gajim/common/application.py b/gajim/common/application.py index 5817d9eb1..41971bfd4 100644 --- a/gajim/common/application.py +++ b/gajim/common/application.py @@ -44,6 +44,7 @@ from gajim.common.events import AllowGajimUpdateCheck from gajim.common.events import GajimUpdateAvailable from gajim.common.client import Client from gajim.common.helpers import make_http_request +from gajim.common.storage.events import EventStorage from gajim.common.task_manager import TaskManager from gajim.common.settings import Settings from gajim.common.settings import LegacyConfig @@ -74,6 +75,9 @@ class CoreApplication: app.storage.cache = CacheStorage() app.storage.cache.init() + app.storage.events = EventStorage() + app.storage.events.init() + app.storage.archive = MessageArchiveStorage() app.storage.archive.init() diff --git a/gajim/common/events.py b/gajim/common/events.py index a4fac93d3..8640b2ebe 100644 --- a/gajim/common/events.py +++ b/gajim/common/events.py @@ -32,7 +32,10 @@ from nbxmpp.structs import ModerationData from nbxmpp.structs import LocationData from nbxmpp.structs import RosterItem from nbxmpp.structs import TuneData +from nbxmpp.const import Affiliation from nbxmpp.const import InviteType +from nbxmpp.const import Role +from nbxmpp.const import StatusCode from gajim.common.file_props import FileProp from gajim.common.const import JingleState @@ -676,3 +679,101 @@ class AllowGajimUpdateCheck(ApplicationEvent): class GajimUpdateAvailable(ApplicationEvent): name: str = field(init=False, default='gajim-update-available') version: str + + +@dataclass +class MUCNicknameChanged(ApplicationEvent): + name: str = field(init=False, default='muc-nickname-changed') + is_self: bool + new_name: str + old_name: str + timestamp: float + + +@dataclass +class MUCRoomConfigChanged(ApplicationEvent): + name: str = field(init=False, default='muc-room-config-changed') + timestamp: float + status_codes: set[StatusCode] + + +@dataclass +class MUCRoomConfigFinished(ApplicationEvent): + name: str = field(init=False, default='muc-room-config-finished') + timestamp: float + + +@dataclass +class MUCRoomPresenceError(ApplicationEvent): + name: str = field(init=False, default='muc-room-presence-error') + timestamp: float + error: Any + + +@dataclass +class MUCRoomKicked(ApplicationEvent): + name: str = field(init=False, default='muc-room-kicked') + timestamp: float + status_codes: Optional[set[StatusCode]] + reason: Optional[str] + actor: Optional[str] + + +@dataclass +class MUCRoomDestroyed(ApplicationEvent): + name: str = field(init=False, default='muc-room-destroyed') + timestamp: float + reason: Optional[str] + alternate: Optional[JID] + + +@dataclass +class MUCUserJoined(ApplicationEvent): + name: str = field(init=False, default='muc-user-joined') + timestamp: float + is_self: bool + nick: str + status_codes: Optional[set[StatusCode]] + + +@dataclass +class MUCUserLeft(ApplicationEvent): + name: str = field(init=False, default='muc-user-left') + timestamp: float + is_self: bool + nick: str + status_codes: Optional[set[StatusCode]] + reason: Optional[str] + actor: Optional[str] + + +@dataclass +class MUCUserRoleChanged(ApplicationEvent): + name: str = field(init=False, default='muc-user-role-changed') + timestamp: float + is_self: bool + nick: str + role: Role + reason: Optional[str] + actor: Optional[str] + + +@dataclass +class MUCUserAffiliationChanged(ApplicationEvent): + name: str = field(init=False, default='muc-user-affiliation-changed') + timestamp: float + is_self: bool + nick: str + affiliation: Affiliation + reason: Optional[str] + actor: Optional[str] + + +@dataclass +class MUCUserStatusShowChanged(ApplicationEvent): + name: str = field(init=False, default='muc-user-status-show-changed') + timestamp: float + is_self: bool + nick: str + status: str + show_value: str diff --git a/gajim/common/modules/contacts.py b/gajim/common/modules/contacts.py index e8e65fb39..8d89c2a9a 100644 --- a/gajim/common/modules/contacts.py +++ b/gajim/common/modules/contacts.py @@ -15,11 +15,14 @@ from __future__ import annotations from typing import Any +from typing import cast from typing import Iterator from typing import Optional from typing import Union from typing import overload +import time + import cairo from nbxmpp.const import Affiliation from nbxmpp.const import Chatstate @@ -29,9 +32,12 @@ from nbxmpp.protocol import JID from nbxmpp.structs import DiscoInfo from nbxmpp.structs import LocationData from nbxmpp.structs import TuneData +from nbxmpp.structs import MessageProperties from nbxmpp.structs import MucSubject +from nbxmpp.structs import PresenceProperties from gajim.common import app +from gajim.common import events from gajim.common import types from gajim.common.const import PresenceShowExt from gajim.common.const import SimpleClientState @@ -942,12 +948,32 @@ class GroupchatParticipant(CommonContact): if not self._presence.available and presence.available: self._presence = presence - self.notify('user-joined', *args) + + properties = cast(MessageProperties, args[0]) + event = events.MUCUserJoined( + timestamp=time.time(), + is_self=properties.is_muc_self_presence, + nick=self.name, + status_codes=properties.muc_status_codes) + + app.storage.events.store(self.room, event) + self.notify('user-joined', event) return if not presence.available: self._presence = presence - self.notify('user-left', *args) + + properties = cast(MessageProperties, args[0]) + event = events.MUCUserLeft( + timestamp=time.time(), + is_self=properties.is_muc_self_presence, + nick=self.name, + status_codes=properties.muc_status_codes, + reason=properties.muc_user.reason, + actor=properties.muc_user.actor) + + app.storage.events.store(self.room, event) + self.notify('user-left', event) return signals: list[str] = [] @@ -962,8 +988,37 @@ class GroupchatParticipant(CommonContact): signals.append('user-status-show-changed') self._presence = presence + for signal in signals: - self.notify(signal, *args) + properties = cast(PresenceProperties, args[0]) + if signal == 'user-affiliation-changed': + event = events.MUCUserAffiliationChanged( + timestamp=time.time(), + is_self=properties.is_muc_self_presence, + nick=self.name, + affiliation=self.affiliation, + reason=properties.muc_user.reason, + actor=properties.muc_user.actor) + + if signal == 'user-role-changed': + event = events.MUCUserRoleChanged( + timestamp=time.time(), + is_self=properties.is_muc_self_presence, + nick=self.name, + role=self.role, + reason=properties.muc_user.reason, + actor=properties.muc_user.actor) + + if signal == 'user-status-show-changed': + event = events.MUCUserStatusShowChanged( + timestamp=time.time(), + is_self=properties.is_muc_self_presence, + nick=self.name, + status=properties.status, + show_value=properties.show.value) + + app.storage.events.store(self.room, event) + self.notify(signal, event) def update_avatar(self, *args: Any) -> None: app.app.avatar_storage.invalidate_cache(self._jid) diff --git a/gajim/common/modules/muc.py b/gajim/common/modules/muc.py index 4521cacbd..98e7107b0 100644 --- a/gajim/common/modules/muc.py +++ b/gajim/common/modules/muc.py @@ -44,6 +44,7 @@ from nbxmpp.task import Task from gi.repository import GLib from gajim.common import app +from gajim.common import events from gajim.common import helpers from gajim.common import types from gajim.common.const import ClientState, KindConstant @@ -456,7 +457,11 @@ class MUC(BaseModule): self._log.info('Configuration finished: %s', jid) room = self._get_contact(jid.bare) - room.notify('room-config-finished') + event = events.MUCRoomConfigFinished(timestamp=time.time()) + + assert isinstance(room, GroupchatContact) + app.storage.events.store(room, event) + room.notify('room-config-finished', event) def update_presence(self) -> None: mucs = self._get_mucs_with_state([MUCJoinedState.JOINED, @@ -536,6 +541,11 @@ class MUC(BaseModule): self._set_muc_state(room_jid, MUCJoinedState.NOT_JOINED) else: + event = events.MUCRoomPresenceError( + timestamp=time.time(), + error=properties.error) + assert isinstance(room, GroupchatContact) + app.storage.events.store(room, event) room.notify('room-presence-error', properties) def _on_muc_user_presence(self, @@ -560,7 +570,15 @@ class MUC(BaseModule): self._set_muc_state(room_jid, MUCJoinedState.NOT_JOINED) self._con.get_module('Bookmarks').remove(room_jid) room.set_not_joined() - room.notify('room-destroyed', properties) + + event = events.MUCRoomDestroyed( + timestamp=time.time(), + reason=properties.muc_destroyed.reason, + alternate=properties.muc_destroyed.alternate) + assert isinstance(room, GroupchatContact) + app.storage.events.store(room, event) + + room.notify('room-destroyed', event) return if properties.is_nickname_changed: @@ -586,10 +604,16 @@ class MUC(BaseModule): presence = self._process_user_presence(properties) occupant.update_presence(presence, properties) - room.notify('user-nickname-changed', - occupant, - new_occupant, - properties) + event = events.MUCNicknameChanged( + timestamp=time.time(), + is_self=properties.is_muc_self_presence, + new_name=new_occupant.name, + old_name=occupant.name) + + assert isinstance(room, GroupchatContact) + app.storage.events.store(room, event) + + room.notify('user-nickname-changed', event, occupant, new_occupant) return is_joined = self._is_user_joined(properties.jid) @@ -616,7 +640,16 @@ class MUC(BaseModule): if properties.is_muc_self_presence and properties.is_kicked: self._set_muc_state(room_jid, MUCJoinedState.NOT_JOINED) room.set_not_joined() - room.notify('room-kicked', properties) + + event = events.MUCRoomKicked( + timestamp=time.time(), + status_codes=properties.muc_status_codes, + reason=properties.muc_user.reason, + actor=properties.muc_user.actor) + assert isinstance(room, GroupchatContact) + app.storage.events.store(room, event) + room.notify('room-kicked', event) + status_codes = properties.muc_status_codes or [] if StatusCode.REMOVED_SERVICE_SHUTDOWN in status_codes: self._start_rejoin_timeout(room_jid) @@ -889,12 +922,21 @@ class MUC(BaseModule): if not properties.is_muc_config_change: return - room_jid = str(properties.muc_jid) self._log.info('Received config change: %s %s', - room_jid, properties.muc_status_codes) + properties.muc_jid, properties.muc_status_codes) - room = self._get_contact(room_jid) - room.notify('room-config-changed', properties) + assert properties.muc_status_codes is not None + event = events.MUCRoomConfigChanged( + timestamp=time.time(), + status_codes=properties.muc_status_codes) + + assert properties.muc_jid is not None + room = self._get_contact(properties.muc_jid) + + assert isinstance(room, GroupchatContact) + app.storage.events.store(room, event) + + room.notify('room-config-changed', event) raise nbxmpp.NodeProcessed diff --git a/gajim/common/storage/base.py b/gajim/common/storage/base.py index 8d59fcd8e..929c15232 100644 --- a/gajim/common/storage/base.py +++ b/gajim/common/storage/base.py @@ -30,11 +30,15 @@ from pathlib import Path from gi.repository import GLib +import nbxmpp.const from nbxmpp.protocol import Iq from nbxmpp.protocol import JID from nbxmpp.structs import RosterItem from nbxmpp.structs import DiscoInfo from nbxmpp.structs import CommonError +from nbxmpp.const import Role +from nbxmpp.const import Affiliation +from nbxmpp.const import StatusCode from nbxmpp.modules.discovery import parse_disco_info @@ -98,6 +102,13 @@ sqlite3.register_converter('disco_info', _convert_disco_info) sqlite3.register_adapter(DiscoInfo, _adapt_disco_info) +def _convert_json(json_string: bytes) -> dict[str, Any]: + return json.loads(json_string, object_hook=json_decoder) + + +sqlite3.register_converter('JSON', _convert_json) + + class Encoder(json.JSONEncoder): def default(self, o: Any) -> Any: if isinstance(o, set): @@ -111,6 +122,10 @@ class Encoder(json.JSONEncoder): dct['__type'] = 'RosterItem' return dct + if isinstance(o, (Affiliation, Role, StatusCode)): + return {'value': o.value, + '__type': o.__class__.__name__} + return json.JSONEncoder.default(self, o) @@ -118,8 +133,10 @@ def json_decoder(dct: dict[str, Any]) -> Any: type_ = dct.get('__type') if type_ is None: return dct + if type_ == 'JID': return JID.from_string(dct['value']) + if type_ == 'RosterItem': return RosterItem(jid=dct['jid'], name=dct['name'], @@ -127,6 +144,10 @@ def json_decoder(dct: dict[str, Any]) -> Any: subscription=dct['subscription'], approved=dct['approved'], groups=set(dct['groups'])) + + if type_ in ('Affiliation', 'Role', 'StatusCode'): + return getattr(nbxmpp.const, type_)(dct['value']) + return dct diff --git a/gajim/common/storage/events.py b/gajim/common/storage/events.py new file mode 100644 index 000000000..38b128dba --- /dev/null +++ b/gajim/common/storage/events.py @@ -0,0 +1,132 @@ +# 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/>. + +from __future__ import annotations +import dataclasses + +from typing import Any +from typing import NamedTuple + +import json +import sqlite3 +import logging +from collections import namedtuple + +from nbxmpp.protocol import JID + +from gajim.common import events +from gajim.common.storage.base import SqliteStorage +from gajim.common.storage.base import Encoder +from gajim.common.types import ChatContactT + + +EVENTS_SQL_STATEMENT = ''' + CREATE TABLE events ( + account TEXT, + jid TEXT, + event TEXT, + timestamp REAL, + data TEXT); + + CREATE INDEX idx_account_jid ON events(account, jid); + + PRAGMA user_version=1; + ''' + +EVENT_CLASSES: dict[str, Any] = { + 'muc-nickname-changed': events.MUCNicknameChanged, + 'muc-room-config-changed': events.MUCRoomConfigChanged, + 'muc-room-config-finished': events.MUCRoomConfigFinished, + 'muc-room-presence-error': events.MUCRoomPresenceError, + 'muc-room-kicked': events.MUCRoomKicked, + 'muc-room-destroyed': events.MUCRoomDestroyed, + 'muc-user-joined': events.MUCUserJoined, + 'muc-user-left': events.MUCUserLeft, + 'muc-user-role-changed': events.MUCUserRoleChanged, + 'muc-user-affiliation-changed': events.MUCUserAffiliationChanged, + 'muc-user-status-show-changed': events.MUCUserStatusShowChanged, +} + +log = logging.getLogger('gajim.c.storage.events') + + +class EventRow(NamedTuple): + account: str + jid: JID + event: str + timestamp: float + data: dict[str, Any] + + +class EventStorage(SqliteStorage): + def __init__(self): + SqliteStorage.__init__(self, + log, + None, + EVENTS_SQL_STATEMENT) + + def init(self, **kwargs: Any) -> None: + SqliteStorage.init(self, + detect_types=sqlite3.PARSE_COLNAMES) + self._con.row_factory = self._namedtuple_factory + + @staticmethod + def _namedtuple_factory(cursor: sqlite3.Cursor, + row: tuple[Any, ...]) -> NamedTuple: + + assert cursor.description is not None + fields = [col[0] for col in cursor.description] + Row = namedtuple('Row', fields) # type: ignore + return Row(*row) + + def _migrate(self) -> None: + pass + + def store(self, + contact: ChatContactT, + event: Any + ) -> None: + + event_dict = dataclasses.asdict(event) + name = event_dict.pop('name') + timestamp = event_dict.pop('timestamp') + + insert_sql = ''' + INSERT INTO events(account, jid, event, timestamp, data) + VALUES(?, ?, ?, ?, ?)''' + + self._con.execute(insert_sql, (contact.account, + contact.jid, + name, + timestamp, + json.dumps(event_dict, cls=Encoder))) + + def load(self, + contact: ChatContactT, + before: bool, + timestamp: float + ) -> list[EventRow]: + + time_operator = '<' if before else '>' + + insert_sql = ''' + SELECT account, jid, event, timestamp, data as "data [JSON]" + FROM events WHERE account=? AND jid=? AND timestamp %s ? + ''' % time_operator + + for row in self._con.execute(insert_sql, (contact.account, + contact.jid, + timestamp)).fetchall(): + event_class = EVENT_CLASSES[row.event] + yield event_class(**row.data, timestamp=row.timestamp) diff --git a/gajim/gtk/chat_banner.py b/gajim/gtk/chat_banner.py index a6d48ed31..8e010c18d 100644 --- a/gajim/gtk/chat_banner.py +++ b/gajim/gtk/chat_banner.py @@ -21,8 +21,6 @@ from gi.repository import Gtk import cairo -from nbxmpp.structs import PresenceProperties - from gajim.common import app from gajim.common import ged from gajim.common import types @@ -184,13 +182,7 @@ class ChatBanner(Gtk.Box, EventHelper): if contact.is_joined: self._update_content() - def _on_user_role_changed(self, - _contact: GroupchatContact, - _signal_name: str, - user_contact: GroupchatParticipant, - properties: PresenceProperties - ) -> None: - + def _on_user_role_changed(self, *args: Any) -> None: self._update_content() def _on_user_state_changed(self, *args: Any) -> None: diff --git a/gajim/gtk/chat_stack.py b/gajim/gtk/chat_stack.py index 21faf766e..c10327b11 100644 --- a/gajim/gtk/chat_stack.py +++ b/gajim/gtk/chat_stack.py @@ -28,7 +28,6 @@ from gi.repository import Gtk from nbxmpp.errors import StanzaError from nbxmpp.protocol import JID from nbxmpp.structs import MessageProperties -from nbxmpp.structs import PresenceProperties from gajim.common import app from gajim.common import events @@ -297,23 +296,25 @@ class ChatStack(Gtk.Stack, EventHelper): contact: GroupchatContact, _signal_name: str, _user_contact: GroupchatParticipant, - _properties: PresenceProperties + _event: events.MUCUserJoined ) -> None: + self._update_group_chat_actions(contact) def _on_user_role_changed(self, contact: GroupchatContact, _signal_name: str, _user_contact: GroupchatParticipant, - _properties: PresenceProperties + _event: events.MUCUserRoleChanged ) -> None: + self._update_group_chat_actions(contact) def _on_user_affiliation_changed(self, contact: GroupchatContact, _signal_name: str, _user_contact: GroupchatParticipant, - _properties: PresenceProperties + _event: events.MUCUserAffiliationChanged ) -> None: self._update_group_chat_actions(contact) diff --git a/gajim/gtk/control.py b/gajim/gtk/control.py index 6236b4ef6..705d848cc 100644 --- a/gajim/gtk/control.py +++ b/gajim/gtk/control.py @@ -19,9 +19,6 @@ from typing import Optional from typing import Union from typing import cast -from collections import defaultdict -from collections import deque -from functools import partial import os import logging import time @@ -105,12 +102,6 @@ class ChatControl(EventHelper): self._muc_subjects: dict[ types.ChatContactT, tuple[float, MucSubject]] = {} - # Store 100 info messages per contact on a FIFO basis - self._info_messages: dict[ - types.ChatContactT, - deque[tuple[float, str]]] = defaultdict( - partial(deque, maxlen=100)) # pyright: ignore - self.widget = cast(Gtk.Box, self._ui.get_object('control_box')) self.widget.show_all() @@ -212,9 +203,6 @@ class ChatControl(EventHelper): for transfer in transfers: self.add_file_transfer(transfer) - for timestamp, message in self._info_messages[contact]: - self.add_info_message(message, timestamp) - if isinstance(contact, GroupchatContact): if (app.settings.get('show_subject_on_join') or not contact.is_joining): @@ -515,10 +503,6 @@ class ChatControl(EventHelper): timestamp: Optional[float] = None ) -> None: - if timestamp is None: - assert self._contact is not None - self._info_messages[self._contact].append((time.time(), text)) - self.conversation_view.add_info_message(text, timestamp) def add_file_transfer(self, transfer: HTTPFileTransfer) -> None: @@ -639,6 +623,42 @@ class ChatControl(EventHelper): # if self.conversation_view.reduce_message_count(before): # self._scrolled_view.set_history_complete(before, False) + for event in app.storage.events.load(self._contact, before, timestamp): + if isinstance(event, events.MUCUserJoined): + self._process_muc_user_joined(event) + + elif isinstance(event, events.MUCUserLeft): + self._process_muc_user_left(event) + + elif isinstance(event, events.MUCNicknameChanged): + self._process_muc_nickname_changed(event) + + elif isinstance(event, events.MUCRoomKicked): + self._process_muc_room_kicked(event) + + elif isinstance(event, events.MUCUserAffiliationChanged): + self._process_muc_user_affiliation_changed(event) + + elif isinstance(event, events.MUCUserRoleChanged): + self._process_muc_user_role_changed(event) + + elif isinstance(event, events.MUCUserStatusShowChanged): + self._process_muc_user_status_show_changed(event) + + elif isinstance(event, events.MUCRoomConfigChanged): + self._process_muc_room_config_changed(event) + + elif isinstance(event, events.MUCRoomConfigFinished): + self._process_muc_room_config_finished(event) + + elif isinstance(event, events.MUCRoomPresenceError): + self._process_muc_room_presence_error(event) + + elif isinstance(event, events.MUCRoomDestroyed): + self._process_muc_room_destroyed(event) + else: + raise ValueError('Unknown event: %s' % event) + self._scrolled_view.block_signals(False) def add_messages(self, messages: list[ConversationRow]): @@ -723,143 +743,213 @@ class ChatControl(EventHelper): additional_data=additional_data) def _on_user_nickname_changed(self, - contact: types.GroupchatContact, + _contact: types.GroupchatContact, _signal_name: str, - old_contact: types.GroupchatParticipant, - new_contact: types.GroupchatParticipant, - properties: PresenceProperties + event: events.MUCNicknameChanged, + _old_contact: types.GroupchatParticipant, + _new_contact: types.GroupchatParticipant ) -> None: - if properties.is_muc_self_presence: - message = _('You are now known as %s') % new_contact.name + self._process_muc_nickname_changed(event) + + def _process_muc_nickname_changed(self, + event: events.MUCNicknameChanged + ) -> None: + + if event.is_self: + message = _('You are now known as %s') % event.new_name else: message = _('{nick} is now known ' - 'as {new_nick}').format(nick=old_contact.name, - new_nick=new_contact.name) + 'as {new_nick}').format(nick=event.old_name, + new_nick=event.new_name) + self.add_info_message(message, event.timestamp) - self.add_info_message(message) + def _on_room_kicked(self, + _contact: GroupchatContact, + _signal_name: str, + event: events.MUCRoomKicked + ) -> None: - def _on_muc_user_status_show_changed(self, - contact: GroupchatContact, - _signal_name: str, - user_contact: GroupchatParticipant, - properties: PresenceProperties - ) -> None: + self._process_muc_room_kicked(event) - if not contact.settings.get('print_status'): + def _process_muc_room_kicked(self, event: events.MUCRoomKicked) -> None: + status_codes = event.status_codes or [] + + reason = event.reason + reason = '' if reason is None else f': {reason}' + + actor = event.actor + # Group Chat: You have been kicked by Alice + actor = '' if actor is None else _(' by {actor}').format( + actor=actor) + + # Group Chat: We have been removed from the room by Alice: reason + message = _('You have been removed from the ' + 'group chat{actor}{reason}') + + if StatusCode.REMOVED_ERROR in status_codes: + # Handle 333 before 307, some MUCs add both + # Group Chat: Server kicked us because of an server error + message = _('You have left due ' + 'to an error{reason}').format(reason=reason) + + elif StatusCode.REMOVED_KICKED in status_codes: + # Group Chat: We have been kicked by Alice: reason + message = _('You have been ' + 'kicked{actor}{reason}').format(actor=actor, + reason=reason) + + elif StatusCode.REMOVED_BANNED in status_codes: + # Group Chat: We have been banned by Alice: reason + message = _('You have been ' + 'banned{actor}{reason}').format(actor=actor, + reason=reason) + + elif StatusCode.REMOVED_AFFILIATION_CHANGE in status_codes: + # Group Chat: We were removed because of an affiliation change + reason = _(': Affiliation changed') + message = message.format(actor=actor, reason=reason) + + elif StatusCode.REMOVED_NONMEMBER_IN_MEMBERS_ONLY in status_codes: + # Group Chat: Room configuration changed + reason = _(': Group chat configuration changed to members-only') + message = message.format(actor=actor, reason=reason) + + elif StatusCode.REMOVED_SERVICE_SHUTDOWN in status_codes: + # Group Chat: Kicked because of server shutdown + reason = ': System shutdown' + message = message.format(actor=actor, reason=reason) + + else: + # No formatted message available return - self.conversation_view.add_user_status(user_contact.name, - user_contact.show.value, - user_contact.status) + self.add_info_message(message, event.timestamp) - def _on_user_status_show_changed(self, - _user_contact: GroupchatParticipant, + def _on_user_affiliation_changed(self, + _contact: GroupchatContact, _signal_name: str, - properties: PresenceProperties + user_contact: GroupchatParticipant, + event: events.MUCUserAffiliationChanged ) -> None: - nick = properties.muc_nickname - status = properties.status - status = '' if not status else f' - {status}' - assert properties.show is not None - show = helpers.get_uf_show(properties.show.value) + self._process_muc_user_affiliation_changed(event) - assert isinstance(self.contact, GroupchatParticipant) - if not self.contact.room.settings.get('print_status'): - return + def _process_muc_user_affiliation_changed( + self, + event: events.MUCUserAffiliationChanged) -> None: - if properties.is_muc_self_presence: - message = _('You are now {show}{status}').format(show=show, - status=status) + affiliation = helpers.get_uf_affiliation(event.affiliation) + + reason = event.reason + reason = '' if reason is None else f': {reason}' + + actor = event.actor + # Group Chat: You have been kicked by Alice + actor = '' if actor is None else _(' by {actor}').format( + actor=actor) + if event.is_self: + message = _('** Your Affiliation has been set to ' + '{affiliation}{actor}{reason}').format( + affiliation=affiliation, + actor=actor, + reason=reason) else: - message = _('{nick} is now {show}{status}').format(nick=nick, - show=show, - status=status) - self.add_info_message(message) + message = _('** Affiliation of {nick} has been set to ' + '{affiliation}{actor}{reason}').format( + nick=event.nick, + affiliation=affiliation, + actor=actor, + reason=reason) - def _on_room_voice_request(self, - _contact: GroupchatContact, - _signal_name: str, - properties: MessageProperties - ) -> None: - voice_request = properties.voice_request - assert voice_request is not None + self.add_info_message(message, event.timestamp) - def on_approve() -> None: - self.client.get_module('MUC').approve_voice_request( - self.contact.jid, voice_request) + def _on_user_role_changed(self, + _contact: GroupchatContact, + _signal_name: str, + user_contact: GroupchatParticipant, + event: events.MUCUserRoleChanged + ) -> None: - ConfirmationDialog( - _('Voice Request'), - _('Voice Request'), - _('<b>%(nick)s</b> from <b>%(room_name)s</b> requests voice') % { - 'nick': voice_request.nick, 'room_name': self.contact.name}, - [DialogButton.make('Cancel'), - DialogButton.make('Accept', - text=_('_Approve'), - callback=on_approve)], - modal=False).show() + self._process_muc_user_role_changed(event) - def add_muc_message(self, - text: str, - tim: float, - contact: str = '', - displaymarking: Optional[Displaymarking] = None, - message_id: Optional[str] = None, - stanza_id: Optional[str] = None, - additional_data: Optional[AdditionalDataDict] = None, - ) -> None: + def _process_muc_user_role_changed(self, + event: events.MUCUserRoleChanged + ) -> None: - assert isinstance(self._contact, GroupchatContact) + role = helpers.get_uf_role(event.role) + nick = event.nick - if contact == self._contact.nickname: - kind = 'outgoing' + reason = event.reason + reason = '' if reason is None else f': {reason}' + + actor = event.actor + # Group Chat: You have been kicked by Alice + actor = '' if actor is None else _(' by {actor}').format(actor=actor) + + if event.is_self: + message = _('** Your Role has been set to ' + '{role}{actor}{reason}').format(role=role, + actor=actor, + reason=reason) else: - kind = 'incoming' - # muc-specific chatstate + message = _('** Role of {nick} has been set to ' + '{role}{actor}{reason}').format(nick=nick, + role=role, + actor=actor, + reason=reason) + self.add_info_message(message, event.timestamp) - self._add_message(text, - kind, - contact, - tim, - displaymarking=displaymarking, - message_id=message_id, - stanza_id=stanza_id, - additional_data=additional_data) + def _on_user_status_show_changed(self, + _user_contact: GroupchatParticipant, + _signal_name: str, + event: events.MUCUserStatusShowChanged + ) -> None: - def _on_room_subject(self, - contact: GroupchatContact, - _signal_name: str, - subject: Optional[MucSubject] - ) -> None: + self._process_muc_user_status_show_changed(event) - if subject is None: - return + def _process_muc_user_status_show_changed( + self, + event: events.MUCUserStatusShowChanged) -> None: - _timestamp, old_subject = self._muc_subjects.get(contact, (None, None)) - if old_subject is not None and old_subject.text == subject.text: - # Probably a rejoin, we already showed that subject + nick = event.nick + status = event.status + status = '' if not status else f' - {status}' + show = helpers.get_uf_show(event.show_value) + + assert isinstance(self.contact, GroupchatParticipant) + if not self.contact.room.settings.get('print_status'): return - self._muc_subjects[contact] = (time.time(), subject) + if event.is_self: + message = _('You are now {show}{status}').format(show=show, + status=status) - if (app.settings.get('show_subject_on_join') or - not contact.is_joining): - self.conversation_view.add_muc_subject(subject) + else: + message = _('{nick} is now {show}{status}').format( + nick=nick, + show=show, + status=status) + + self.add_info_message(message, event.timestamp) def _on_room_config_changed(self, _contact: GroupchatContact, _signal_name: str, - properties: MessageProperties + event: events.MUCRoomConfigChanged ) -> None: - # http://www.xmpp.org/extensions/xep-0045.html#roomconfig-notify - status_codes = properties.muc_status_codes - assert status_codes is not None + self._process_muc_room_config_changed(event) + + def _process_muc_room_config_changed(self, + event: events.MUCRoomConfigChanged + ) -> None: + # http://www.xmpp.org/extensions/xep-0045.html#roomconfig-notify + status_codes = event.status_codes changes: list[str] = [] + if StatusCode.SHOWING_UNAVAILABLE in status_codes: changes.append(_('Group chat now shows unavailable members')) @@ -873,7 +963,8 @@ class ChatControl(EventHelper): self.client.get_module('Discovery').disco_muc(self.contact.jid) if StatusCode.CONFIG_ROOM_LOGGING in status_codes: - # Can be a presence (see chg_contact_status in groupchat_control.py) + # Can be a presence + # (see chg_contact_status in groupchat_control.py) changes.append(_('Conversations are stored on the server')) if StatusCode.CONFIG_NO_ROOM_LOGGING in status_codes: @@ -888,194 +979,130 @@ class ChatControl(EventHelper): if StatusCode.CONFIG_FULL_ANONYMOUS in status_codes: changes.append(_('Group chat is now fully anonymous')) - for change in changes: - self.add_info_message(change) - - def rejoin(self) -> None: - self.client.get_module('MUC').join(self.contact.jid) - - def _on_user_joined(self, - contact: GroupchatContact, - _signal_name: str, - user_contact: GroupchatParticipant, - properties: PresenceProperties - ) -> None: - - nick = user_contact.name - if not properties.is_muc_self_presence: - if contact.is_joined: - self.conversation_view.add_muc_user_joined(nick) - return - - status_codes = properties.muc_status_codes or [] - - if not contact.is_joined: - # We just joined the room - self.add_info_message(_('You (%s) joined the group chat') % nick) - - if StatusCode.NON_ANONYMOUS in status_codes: - self.add_info_message( - _('Any participant is allowed to see your full XMPP Address')) - - if StatusCode.CONFIG_ROOM_LOGGING in status_codes: - self.add_info_message(_('Conversations are stored on the server')) - - if StatusCode.NICKNAME_MODIFIED in status_codes: - self.add_info_message( - _('The server has assigned or modified your nickname in this ' - 'group chat')) + for message in changes: + self.add_info_message(message, event.timestamp) def _on_room_config_finished(self, _contact: GroupchatContact, - _signal_name: str + _signal_name: str, + event: events.MUCRoomConfigFinished ) -> None: + self._process_muc_room_config_finished(event) + + def _process_muc_room_config_finished(self, + event: events.MUCRoomConfigFinished + ) -> None: + self.add_info_message(_('A new group chat has been created')) - def _on_user_affiliation_changed(self, - _contact: GroupchatContact, - _signal_name: str, - user_contact: GroupchatParticipant, - properties: PresenceProperties - ) -> None: - affiliation = helpers.get_uf_affiliation(user_contact.affiliation) - nick = user_contact.name + def _on_room_presence_error(self, + _contact: GroupchatContact, + _signal_name: str, + event: events.MUCRoomPresenceError + ) -> None: - assert properties.muc_user is not None - reason = properties.muc_user.reason - reason = '' if reason is None else f': {reason}' + self._process_muc_room_presence_error(event) - actor = properties.muc_user.actor - # Group Chat: You have been kicked by Alice - actor = '' if actor is None else _(' by {actor}').format(actor=actor) + def _process_muc_room_presence_error(self, + event: events.MUCRoomPresenceError + ) -> None: - if properties.is_muc_self_presence: - message = _('** Your Affiliation has been set to ' - '{affiliation}{actor}{reason}').format( - affiliation=affiliation, - actor=actor, - reason=reason) - else: - message = _('** Affiliation of {nick} has been set to ' - '{affiliation}{actor}{reason}').format( - nick=nick, - affiliation=affiliation, - actor=actor, - reason=reason) + error_message = to_user_string(event.error) + self.add_info_message(_('Error: %s') % error_message) - self.add_info_message(message) + def _on_room_destroyed(self, + _contact: GroupchatContact, + _signal_name: str, + event: events.MUCRoomDestroyed + ) -> None: - def _on_user_role_changed(self, - _contact: GroupchatContact, - _signal_name: str, - user_contact: GroupchatParticipant, - properties: PresenceProperties - ) -> None: - role = helpers.get_uf_role(user_contact.role) - nick = user_contact.name + self._process_muc_room_destroyed(event) + + def _process_muc_room_destroyed(self, + event: events.MUCRoomDestroyed + ) -> None: - assert properties.muc_user is not None - reason = properties.muc_user.reason + reason = event.reason reason = '' if reason is None else f': {reason}' - actor = properties.muc_user.actor - # Group Chat: You have been kicked by Alice - actor = '' if actor is None else _(' by {actor}').format(actor=actor) + message = _('Group chat has been destroyed') - if properties.is_muc_self_presence: - message = _('** Your Role has been set to ' - '{role}{actor}{reason}').format(role=role, - actor=actor, - reason=reason) - else: - message = _('** Role of {nick} has been set to ' - '{role}{actor}{reason}').format(nick=nick, - role=role, - actor=actor, - reason=reason) + if event.alternate is not None: + message += '\n' + _('You can join this group chat instead: ' + 'xmpp:%s?join') % str(event.alternate) - self.add_info_message(message) + self.add_info_message(message, event.timestamp) - def _on_room_kicked(self, + def _on_user_joined(self, _contact: GroupchatContact, _signal_name: str, - properties: MessageProperties + _user_contact: GroupchatParticipant, + event: events.MUCUserJoined ) -> None: - status_codes = properties.muc_status_codes or [] - assert properties.muc_user is not None - reason = properties.muc_user.reason - reason = '' if reason is None else f': {reason}' + self._process_muc_user_joined(event) - actor = properties.muc_user.actor - # Group Chat: You have been kicked by Alice - actor = '' if actor is None else _(' by {actor}').format(actor=actor) + def _process_muc_user_joined(self, event: events.MUCUserJoined) -> None: + assert isinstance(self.contact, GroupchatContact) - # Group Chat: We have been removed from the room by Alice: reason - message = _('You have been removed from the group chat{actor}{reason}') + if not event.is_self: + if self.contact.is_joined: + self.conversation_view.add_muc_user_joined(event.nick) + return - if StatusCode.REMOVED_ERROR in status_codes: - # Handle 333 before 307, some MUCs add both - # Group Chat: Server kicked us because of an server error - message = _('You have left due ' - 'to an error{reason}').format(reason=reason) - self.add_info_message(message) + status_codes = event.status_codes or [] - elif StatusCode.REMOVED_KICKED in status_codes: - # Group Chat: We have been kicked by Alice: reason - message = _('You have been ' - 'kicked{actor}{reason}').format(actor=actor, - reason=reason) - self.add_info_message(message) + message = None + if not self.contact.is_joined: + # We just joined the room + message = _('You (%s) joined the group chat') % event.nick - elif StatusCode.REMOVED_BANNED in status_codes: - # Group Chat: We have been banned by Alice: reason - message = _('You have been ' - 'banned{actor}{reason}').format(actor=actor, - reason=reason) - self.add_info_message(message) + if StatusCode.NON_ANONYMOUS in status_codes: + message = _('Any participant is allowed to see your full ' + 'XMPP Address') - elif StatusCode.REMOVED_AFFILIATION_CHANGE in status_codes: - # Group Chat: We were removed because of an affiliation change - reason = _(': Affiliation changed') - message = message.format(actor=actor, reason=reason) - self.add_info_message(message) + if StatusCode.CONFIG_ROOM_LOGGING in status_codes: + message = _('Conversations are stored on the server') - elif StatusCode.REMOVED_NONMEMBER_IN_MEMBERS_ONLY in status_codes: - # Group Chat: Room configuration changed - reason = _(': Group chat configuration changed to members-only') - message = message.format(actor=actor, reason=reason) - self.add_info_message(message) + if StatusCode.NICKNAME_MODIFIED in status_codes: + message = _('The server has assigned or modified your ' + 'nickname in this group chat') - elif StatusCode.REMOVED_SERVICE_SHUTDOWN in status_codes: - # Group Chat: Kicked because of server shutdown - reason = ': System shutdown' - message = message.format(actor=actor, reason=reason) - self.add_info_message(message) + if message is not None: + self.add_info_message(message, event.timestamp) def _on_user_left(self, _contact: GroupchatContact, _signal_name: str, - user_contact: GroupchatParticipant, - properties: MessageProperties + _user_contact: GroupchatParticipant, + event: events.MUCUserLeft ) -> None: - status_codes = properties.muc_status_codes or [] - nick = user_contact.name - assert properties.muc_user is not None - reason = properties.muc_user.reason + self._process_muc_user_left(event) + + def _process_muc_user_left(self, event: events.MUCUserLeft) -> None: + if event.is_self: + return + + status_codes = event.status_codes or [] + nick = event.nick + + reason = event.reason reason = '' if reason is None else f': {reason}' - actor = properties.muc_user.actor + actor = event.actor # Group Chat: You have been kicked by Alice - actor = '' if actor is None else _(' by {actor}').format(actor=actor) + actor = '' if actor is None else _(' by {actor}').format( + actor=actor) # Group Chat: We have been removed from the room - message = _('{nick} has been removed from the group chat{by}{reason}') + message = _('{nick} has been removed from the group ' + 'chat{by}{reason}') if StatusCode.REMOVED_ERROR in status_codes: # Handle 333 before 307, some MUCs add both self.conversation_view.add_muc_user_left( - nick, properties.muc_user.reason, error=True) + nick, event.reason, error=True) elif StatusCode.REMOVED_KICKED in status_codes: # Group Chat: User was kicked by Alice: reason @@ -1083,7 +1110,6 @@ class ChatControl(EventHelper): 'kicked{actor}{reason}').format(nick=nick, actor=actor, reason=reason) - self.add_info_message(message) elif StatusCode.REMOVED_BANNED in status_codes: # Group Chat: User was banned by Alice: reason @@ -1091,49 +1117,104 @@ class ChatControl(EventHelper): 'banned{actor}{reason}').format(nick=nick, actor=actor, reason=reason) - self.add_info_message(message) elif StatusCode.REMOVED_AFFILIATION_CHANGE in status_codes: reason = _(': Affiliation changed') message = message.format(nick=nick, by=actor, reason=reason) - self.add_info_message(message) elif StatusCode.REMOVED_NONMEMBER_IN_MEMBERS_ONLY in status_codes: reason = _(': Group chat configuration changed to members-only') message = message.format(nick=nick, by=actor, reason=reason) - self.add_info_message(message) else: - self.conversation_view.add_muc_user_left( - nick, properties.muc_user.reason) + self.conversation_view.add_muc_user_left(nick, event.reason) + return - def _on_room_presence_error(self, - _contact: GroupchatContact, - _signal_name: str, - properties: PresenceProperties - ) -> None: + self.add_info_message(message, event.timestamp) - assert properties.error is not None - error_message = to_user_string(properties.error) - self.add_info_message(_('Error: %s') % error_message) + def _on_muc_user_status_show_changed(self, + contact: GroupchatContact, + _signal_name: str, + user_contact: GroupchatParticipant, + properties: PresenceProperties + ) -> None: - def _on_room_destroyed(self, - _contact: GroupchatContact, - _signal_name: str, - properties: PresenceProperties - ) -> None: + if not contact.settings.get('print_status'): + return - destroyed = properties.muc_destroyed - assert destroyed is not None + self.conversation_view.add_user_status(user_contact.name, + user_contact.show.value, + user_contact.status) - reason = destroyed.reason - reason = '' if reason is None else f': {reason}' + def _on_room_voice_request(self, + _contact: GroupchatContact, + _signal_name: str, + properties: MessageProperties + ) -> None: + voice_request = properties.voice_request + assert voice_request is not None - message = _('Group chat has been destroyed') - self.add_info_message(message) + def on_approve() -> None: + self.client.get_module('MUC').approve_voice_request( + self.contact.jid, voice_request) + + ConfirmationDialog( + _('Voice Request'), + _('Voice Request'), + _('<b>%(nick)s</b> from <b>%(room_name)s</b> requests voice') % { + 'nick': voice_request.nick, 'room_name': self.contact.name}, + [DialogButton.make('Cancel'), + DialogButton.make('Accept', + text=_('_Approve'), + callback=on_approve)], + modal=False).show() - alternate = destroyed.alternate - if alternate is not None: - join_message = _('You can join this group chat ' - 'instead: xmpp:%s?join') % str(alternate) - self.add_info_message(join_message) + def add_muc_message(self, + text: str, + tim: float, + contact: str = '', + displaymarking: Optional[Displaymarking] = None, + message_id: Optional[str] = None, + stanza_id: Optional[str] = None, + additional_data: Optional[AdditionalDataDict] = None, + ) -> None: + + assert isinstance(self._contact, GroupchatContact) + + if contact == self._contact.nickname: + kind = 'outgoing' + else: + kind = 'incoming' + # muc-specific chatstate + + self._add_message(text, + kind, + contact, + tim, + displaymarking=displaymarking, + message_id=message_id, + stanza_id=stanza_id, + additional_data=additional_data) + + def _on_room_subject(self, + contact: GroupchatContact, + _signal_name: str, + subject: Optional[MucSubject] + ) -> None: + + if subject is None: + return + + _timestamp, old_subject = self._muc_subjects.get(contact, (None, None)) + if old_subject is not None and old_subject.text == subject.text: + # Probably a rejoin, we already showed that subject + return + + self._muc_subjects[contact] = (time.time(), subject) + + if (app.settings.get('show_subject_on_join') or + not contact.is_joining): + self.conversation_view.add_muc_subject(subject) + + def rejoin(self) -> None: + self.client.get_module('MUC').join(self.contact.jid) diff --git a/gajim/gtk/conversation/view.py b/gajim/gtk/conversation/view.py index 8c2f170f0..7808c3bc1 100644 --- a/gajim/gtk/conversation/view.py +++ b/gajim/gtk/conversation/view.py @@ -176,7 +176,7 @@ class ConversationView(Gtk.ListBox): def add_muc_user_left(self, nick: str, - reason: str, + reason: Optional[str], error: bool = False) -> None: assert isinstance(self._contact, GroupchatContact) if not self._contact.settings.get('print_join_left'): diff --git a/gajim/gtk/groupchat_nick_completion.py b/gajim/gtk/groupchat_nick_completion.py index fceb49bc4..89513aca1 100644 --- a/gajim/gtk/groupchat_nick_completion.py +++ b/gajim/gtk/groupchat_nick_completion.py @@ -18,7 +18,6 @@ from typing import Optional import logging -from nbxmpp.structs import PresenceProperties from gi.repository import Gtk from gi.repository import Gdk @@ -27,6 +26,7 @@ from gajim.common import app from gajim.common import ged from gajim.common import types from gajim.common.events import GcMessageReceived +from gajim.common.events import MUCNicknameChanged from gajim.common.ged import EventHelper from gajim.common.helpers import jid_is_blocked from gajim.common.helpers import message_needs_highlight @@ -76,12 +76,12 @@ class GroupChatNickCompletion(EventHelper): def _on_user_nickname_changed(self, _contact: types.GroupchatContact, _signal_name: str, + event: MUCNicknameChanged, old_contact: types.GroupchatParticipant, - new_contact: types.GroupchatParticipant, - properties: PresenceProperties + new_contact: types.GroupchatParticipant ) -> None: - if properties.is_muc_self_presence: + if event.is_self: return old_name = old_contact.name diff --git a/gajim/gtk/groupchat_roster.py b/gajim/gtk/groupchat_roster.py index 37365aa44..6a067d62c 100644 --- a/gajim/gtk/groupchat_roster.py +++ b/gajim/gtk/groupchat_roster.py @@ -25,7 +25,6 @@ from gi.repository import Gdk from gi.repository import Gtk from gi.repository import GLib from nbxmpp.const import Affiliation -from nbxmpp.structs import PresenceProperties from gajim.common import app from gajim.common import ged @@ -36,6 +35,7 @@ from gajim.common.helpers import jid_is_blocked from gajim.common.const import AvatarSize from gajim.common.const import StyleAttr from gajim.common.events import ApplicationEvent +from gajim.common.events import MUCNicknameChanged from gajim.common.modules.contacts import GroupchatContact from .menus import get_groupchat_roster_menu @@ -281,11 +281,11 @@ class GroupchatRoster(Gtk.Revealer, EventHelper): self._add_contact(user_contact) def _on_user_nickname_changed(self, - contact: types.GroupchatContact, + _contact: types.GroupchatContact, _signal_name: str, + _event: MUCNicknameChanged, old_contact: types.GroupchatParticipant, - new_contact: types.GroupchatParticipant, - properties: PresenceProperties + new_contact: types.GroupchatParticipant ) -> None: self._remove_contact(old_contact) |