Mercurial > libervia-backend
diff libervia/frontends/quick_frontend/quick_chat.py @ 4074:26b7ed2817da
refactoring: rename `sat_frontends` to `libervia.frontends`
author | Goffi <goffi@goffi.org> |
---|---|
date | Fri, 02 Jun 2023 14:12:38 +0200 |
parents | sat_frontends/quick_frontend/quick_chat.py@4b842c1fb686 |
children | 0d7bb4df2343 |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/frontends/quick_frontend/quick_chat.py Fri Jun 02 14:12:38 2023 +0200 @@ -0,0 +1,941 @@ +#!/usr/bin/env python3 + +# helper class for making a SàT frontend +# Copyright (C) 2009-2021 Jérôme Poisson (goffi@goffi.org) + +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# This program 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 Affero General Public License for more details. + +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +from libervia.backend.core.i18n import _ +from libervia.backend.core.log import getLogger +from libervia.backend.tools.common import data_format +from libervia.backend.core import exceptions +from libervia.frontends.quick_frontend import quick_widgets +from libervia.frontends.quick_frontend.constants import Const as C +from collections import OrderedDict +from libervia.frontends.tools import jid +import time + + +log = getLogger(__name__) + + +ROOM_USER_JOINED = "ROOM_USER_JOINED" +ROOM_USER_LEFT = "ROOM_USER_LEFT" +ROOM_USER_MOVED = (ROOM_USER_JOINED, ROOM_USER_LEFT) + +# from datetime import datetime + +# FIXME: day_format need to be settable (i18n) + + +class Message: + """Message metadata""" + + def __init__( + self, parent, uid, timestamp, from_jid, to_jid, msg, subject, type_, extra, + profile): + self.parent = parent + self.profile = profile + self.uid = uid + self.timestamp = timestamp + self.from_jid = from_jid + self.to_jid = to_jid + self.message = msg + self.subject = subject + self.type = type_ + self.extra = extra + self.nick = self.get_nick(from_jid) + self._status = None + # own_mess is True if message was sent by profile's jid + self.own_mess = ( + (from_jid.resource == self.parent.nick) + if self.parent.type == C.CHAT_GROUP + else (from_jid.bare == self.host.profiles[profile].whoami.bare) + ) + # is user mentioned here ? + if self.parent.type == C.CHAT_GROUP and not self.own_mess: + for m in msg.values(): + if self.parent.nick.lower() in m.lower(): + self._mention = True + break + self.handle_me() + self.widgets = set() # widgets linked to this message + + def __str__(self): + return "Message<{mess_type}> [{time}]{nick}> {message}".format( + mess_type=self.type, + time=self.time_text, + nick=self.nick, + message=self.main_message) + + def __contains__(self, item): + return hasattr(self, item) or item in self.extra + + @property + def host(self): + return self.parent.host + + @property + def info_type(self): + return self.extra.get("info_type") + + @property + def mention(self): + try: + return self._mention + except AttributeError: + return False + + @property + def history(self): + """True if message come from history""" + return self.extra.get("history", False) + + @property + def main_message(self): + """currently displayed message""" + if self.parent.lang in self.message: + self.selected_lang = self.parent.lang + return self.message[self.parent.lang] + try: + self.selected_lang = "" + return self.message[""] + except KeyError: + try: + lang, mess = next(iter(self.message.items())) + self.selected_lang = lang + return mess + except StopIteration: + if not self.attachments: + # we may have empty messages if we have attachments + log.error("Can't find message for uid {}".format(self.uid)) + return "" + + @property + def main_message_xhtml(self): + """rich message""" + xhtml = {k: v for k, v in self.extra.items() if "html" in k} + if xhtml: + # FIXME: we only return first found value for now + return next(iter(xhtml.values())) + + @property + def time_text(self): + """Return timestamp in a nicely formatted way""" + # if the message was sent before today, we print the full date + timestamp = time.localtime(self.timestamp) + time_format = "%c" if timestamp < self.parent.day_change else "%H:%M" + return time.strftime(time_format, timestamp) + + @property + def avatar(self): + """avatar data or None if no avatar is found""" + entity = self.from_jid + contact_list = self.host.contact_lists[self.profile] + try: + return contact_list.getCache(entity, "avatar") + except (exceptions.NotFound, KeyError): + # we don't check the result as the avatar listener will be called + self.host.bridge.avatar_get(entity, True, self.profile) + return None + + @property + def encrypted(self): + return self.extra.get("encrypted", False) + + def get_nick(self, entity): + """Return nick of an entity when possible""" + contact_list = self.host.contact_lists[self.profile] + if self.type == C.MESS_TYPE_INFO and self.info_type in ROOM_USER_MOVED: + try: + return self.extra["user_nick"] + except KeyError: + log.error("extra data is missing user nick for uid {}".format(self.uid)) + return "" + # FIXME: converted get_specials to list for pyjamas + if self.parent.type == C.CHAT_GROUP or entity in list( + contact_list.get_specials(C.CONTACT_SPECIAL_GROUP) + ): + return entity.resource or "" + if entity.bare in contact_list: + + try: + nicknames = contact_list.getCache(entity, "nicknames") + except (exceptions.NotFound, KeyError): + # we check result as listener will be called + self.host.bridge.identity_get( + entity.bare, ["nicknames"], True, self.profile) + return entity.node or entity + + if nicknames: + return nicknames[0] + else: + return ( + contact_list.getCache(entity, "name", default=None) + or entity.node + or entity + ) + + return entity.node or entity + + @property + def status(self): + return self._status + + @status.setter + def status(self, status): + if status != self._status: + self._status = status + for w in self.widgets: + w.update({"status": status}) + + def handle_me(self): + """Check if messages starts with "/me " and change them if it is the case + + if several messages (different languages) are presents, they all need to start with "/me " + """ + # TODO: XHTML-IM /me are not handled + me = False + # we need to check /me for every message + for m in self.message.values(): + if m.startswith("/me "): + me = True + else: + me = False + break + if me: + self.type = C.MESS_TYPE_INFO + self.extra["info_type"] = "me" + nick = self.nick + for lang, mess in self.message.items(): + self.message[lang] = "* " + nick + mess[3:] + + @property + def attachments(self): + return self.extra.get(C.KEY_ATTACHMENTS) + + +class MessageWidget: + """Base classe for widgets""" + # This class does nothing and is only used to have a common ancestor + + pass + + +class Occupant: + """Occupant metadata""" + + def __init__(self, parent, data, profile): + self.parent = parent + self.profile = profile + self.nick = data["nick"] + self._entity = data.get("entity") + self.affiliation = data["affiliation"] + self.role = data["role"] + self.widgets = set() # widgets linked to this occupant + self._state = None + + @property + def data(self): + """reconstruct data dict from attributes""" + data = {} + data["nick"] = self.nick + if self._entity is not None: + data["entity"] = self._entity + data["affiliation"] = self.affiliation + data["role"] = self.role + return data + + @property + def jid(self): + """jid in the room""" + return jid.JID("{}/{}".format(self.parent.target.bare, self.nick)) + + @property + def real_jid(self): + """real jid if known else None""" + return self._entity + + @property + def host(self): + return self.parent.host + + @property + def state(self): + return self._state + + @state.setter + def state(self, new_state): + if new_state != self._state: + self._state = new_state + for w in self.widgets: + w.update({"state": new_state}) + + def update(self, update_dict=None): + for w in self.widgets: + w.update(update_dict) + + +class QuickChat(quick_widgets.QuickWidget): + visible_states = ["chat_state"] # FIXME: to be removed, used only in quick_games + + def __init__(self, host, target, type_=C.CHAT_ONE2ONE, nick=None, occupants=None, + subject=None, statuses=None, profiles=None): + """ + @param type_: can be C.CHAT_ONE2ONE for single conversation or C.CHAT_GROUP for + chat à la IRC + """ + self.lang = "" # default language to use for messages + quick_widgets.QuickWidget.__init__(self, host, target, profiles=profiles) + assert type_ in (C.CHAT_ONE2ONE, C.CHAT_GROUP) + self.current_target = target + self.type = type_ + self.encrypted = False # True if this session is currently encrypted + self._locked = False + # True when resync is in progress, avoid resynchronising twice when resync is called + # and history is still being updated. For internal use only + self._resync_lock = False + self.set_locked() + if type_ == C.CHAT_GROUP: + if target.resource: + raise exceptions.InternalError( + "a group chat entity can't have a resource" + ) + if nick is None: + raise exceptions.InternalError("nick must not be None for group chat") + + self.nick = nick + self.occupants = {} + self.set_occupants(occupants) + else: + if occupants is not None or nick is not None: + raise exceptions.InternalError( + "only group chat can have occupants or nick" + ) + self.messages = OrderedDict() # key: uid, value: Message instance + self.games = {} # key=game name (unicode), value=instance of quick_games.RoomGame + self.subject = subject + self.statuses = set(statuses or []) + lt = time.localtime() + self.day_change = ( + lt.tm_year, + lt.tm_mon, + lt.tm_mday, + 0, + 0, + 0, + lt.tm_wday, + lt.tm_yday, + lt.tm_isdst, + ) # struct_time of day changing time + if self.host.AVATARS_HANDLER: + self.host.addListener("avatar", self.on_avatar, profiles) + + def set_locked(self): + """Set locked flag + + To be set when we are waiting for history/search + """ + # FIXME: we don't use getter/setter here because of pyjamas + # TODO: use proper getter/setter once we get rid of pyjamas + if self._locked: + log.warning("{wid} is already locked!".format(wid=self)) + return + self._locked = True + # message_new signals are cached when locked + self._cache = OrderedDict() + log.debug("{wid} is now locked".format(wid=self)) + + def set_unlocked(self): + if not self._locked: + log.debug("{wid} was already unlocked".format(wid=self)) + return + self._locked = False + for uid, data in self._cache.items(): + if uid not in self.messages: + self.message_new(*data) + else: + log.debug("discarding message already in history: {data}, ".format(data=data)) + del self._cache + log.debug("{wid} is now unlocked".format(wid=self)) + + def post_init(self): + """Method to be called by frontend after widget is initialised + + handle the display of history and subject + """ + self.history_print(profile=self.profile) + if self.subject is not None: + self.set_subject(self.subject) + if self.host.ENCRYPTION_HANDLERS: + self.get_encryption_state() + + def on_delete(self): + if self.host.AVATARS_HANDLER: + self.host.removeListener("avatar", self.on_avatar) + + @property + def contact_list(self): + return self.host.contact_lists[self.profile] + + @property + def message_widgets_rev(self): + """Return the history of MessageWidget in reverse chronological order + + Must be implemented by frontend + """ + raise NotImplementedError + + ## synchornisation handling ## + + @quick_widgets.QuickWidget.sync.setter + def sync(self, state): + quick_widgets.QuickWidget.sync.fset(self, state) + if not state: + self.set_locked() + + def _resync_complete(self): + self.sync = True + self._resync_lock = False + + def occupants_clear(self): + """Remove all occupants + + Must be overridden by frontends to clear their own representations of occupants + """ + self.occupants.clear() + + def resync(self): + if self._resync_lock: + return + self._resync_lock = True + log.debug("resynchronising {self}".format(self=self)) + for mess in reversed(list(self.messages.values())): + if mess.type == C.MESS_TYPE_INFO: + continue + last_message = mess + break + else: + # we have no message yet, we can get normal history + self.history_print(callback=self._resync_complete, profile=self.profile) + return + if self.type == C.CHAT_GROUP: + self.occupants_clear() + self.host.bridge.muc_occupants_get( + str(self.target), self.profile, callback=self.update_occupants, + errback=log.error) + self.history_print( + size=C.HISTORY_LIMIT_NONE, + filters={'timestamp_start': last_message.timestamp}, + callback=self._resync_complete, + profile=self.profile) + + ## Widget management ## + + def __str__(self): + return "Chat Widget [target: {}, type: {}, profile: {}]".format( + self.target, self.type, self.profile + ) + + @staticmethod + def get_widget_hash(target, profiles): + profile = list(profiles)[0] + return profile + "\n" + str(target.bare) + + @staticmethod + def get_private_hash(target, profile): + """Get unique hash for private conversations + + This method should be used with force_hash to get unique widget for private MUC conversations + """ + return (str(profile), target) + + def add_target(self, target): + super(QuickChat, self).add_target(target) + if target.resource: + self.current_target = ( + target + ) # FIXME: tmp, must use resource priority throught contactList instead + + def recreate_args(self, args, kwargs): + """copy important attribute for a new widget""" + kwargs["type_"] = self.type + if self.type == C.CHAT_GROUP: + kwargs["occupants"] = {o.nick: o.data for o in self.occupants.values()} + kwargs["subject"] = self.subject + try: + kwargs["nick"] = self.nick + except AttributeError: + pass + + def on_private_created(self, widget): + """Method called when a new widget for private conversation (MUC) is created""" + raise NotImplementedError + + def get_or_create_private_widget(self, entity): + """Create a widget for private conversation, or get it if it already exists + + @param entity: full jid of the target + """ + return self.host.widgets.get_or_create_widget( + QuickChat, + entity, + type_=C.CHAT_ONE2ONE, + force_hash=self.get_private_hash(self.profile, entity), + on_new_widget=self.on_private_created, + profile=self.profile, + ) # we force hash to have a new widget, not this one again + + @property + def target(self): + if self.type == C.CHAT_GROUP: + return self.current_target.bare + return self.current_target + + ## occupants ## + + def set_occupants(self, occupants): + """Set the whole list of occupants""" + assert len(self.occupants) == 0 + for nick, data in occupants.items(): + # XXX: this log is disabled because it's really too verbose + # but kept commented as it may be useful for debugging + # log.debug(u"adding occupant {nick} to {room}".format( + # nick=nick, room=self.target)) + self.occupants[nick] = Occupant(self, data, self.profile) + + def update_occupants(self, occupants): + """Update occupants list + + In opposition to set_occupants, this only add missing occupants and remove + occupants who have left + """ + # FIXME: occupants with modified status are not handled + local_occupants = set(self.occupants) + updated_occupants = set(occupants) + left_occupants = local_occupants - updated_occupants + joined_occupants = updated_occupants - local_occupants + log.debug("updating occupants for {room}:\n" + "left: {left_occupants}\n" + "joined: {joined_occupants}" + .format(room=self.target, + left_occupants=", ".join(left_occupants), + joined_occupants=", ".join(joined_occupants))) + for nick in left_occupants: + self.removeUser(occupants[nick]) + for nick in joined_occupants: + self.addUser(occupants[nick]) + + def addUser(self, occupant_data): + """Add user if it is not in the group list""" + occupant = Occupant(self, occupant_data, self.profile) + self.occupants[occupant.nick] = occupant + return occupant + + def removeUser(self, occupant_data): + """Remove a user from the group list""" + nick = occupant_data["nick"] + try: + occupant = self.occupants.pop(nick) + except KeyError: + log.warning("Trying to remove an unknown occupant: {}".format(nick)) + else: + return occupant + + def set_user_nick(self, nick): + """Set the nick of the user, usefull for e.g. change the color of the user""" + self.nick = nick + + def change_user_nick(self, old_nick, new_nick): + """Change nick of a user in group list""" + log.info("{old} is now known as {new} in room {room_jid}".format( + old = old_nick, + new = new_nick, + room_jid = self.target)) + + ## Messages ## + + def manage_message(self, entity, mess_type): + """Tell if this chat widget manage an entity and message type couple + + @param entity (jid.JID): (full) jid of the sending entity + @param mess_type (str): message type as given by message_new + @return (bool): True if this Chat Widget manage this couple + """ + if self.type == C.CHAT_GROUP: + if ( + mess_type in (C.MESS_TYPE_GROUPCHAT, C.MESS_TYPE_INFO) + and self.target == entity.bare + ): + return True + else: + if mess_type != C.MESS_TYPE_GROUPCHAT and entity in self.targets: + return True + return False + + def update_history(self, size=C.HISTORY_LIMIT_DEFAULT, filters=None, profile="@NONE@"): + """Called when history need to be recreated + + Remove all message from history then call history_print + Must probably be overriden by frontend to clear widget + @param size (int): number of messages + @param filters (str): patterns to filter the history results + @param profile (str): %(doc_profile)s + """ + self.set_locked() + self.messages.clear() + self.history_print(size, filters, profile=profile) + + def _on_history_printed(self): + """Method called when history is printed (or failed) + + unlock the widget, and can be used to refresh or scroll down + the focus after the history is printed + """ + self.set_unlocked() + + def history_print(self, size=C.HISTORY_LIMIT_DEFAULT, filters=None, callback=None, + profile="@NONE@"): + """Print the current history + + Note: self.set_unlocked will be called once history is printed + @param size (int): number of messages + @param search (str): pattern to filter the history results + @param callback(callable, None): method to call when history has been printed + @param profile (str): %(doc_profile)s + """ + if filters is None: + filters = {} + if size == 0: + log.debug("Empty history requested, skipping") + self._on_history_printed() + return + log_msg = _("now we print the history") + if size != C.HISTORY_LIMIT_DEFAULT: + log_msg += _(" ({} messages)".format(size)) + log.debug(log_msg) + + if self.type == C.CHAT_ONE2ONE: + special = self.host.contact_lists[self.profile].getCache( + self.target, C.CONTACT_SPECIAL, create_if_not_found=True, default=None + ) + if special == C.CONTACT_SPECIAL_GROUP: + # we have a private conversation + # so we need full jid for the history + # (else we would get history from group itself) + # and to filter out groupchat message + target = self.target + filters["not_types"] = C.MESS_TYPE_GROUPCHAT + else: + target = self.target.bare + else: + # groupchat + target = self.target.bare + # FIXME: info not handled correctly + filters["types"] = C.MESS_TYPE_GROUPCHAT + + self.history_filters = filters + + def _history_get_cb(history): + # day_format = "%A, %d %b %Y" # to display the day change + # previous_day = datetime.now().strftime(day_format) + # message_day = datetime.fromtimestamp(timestamp).strftime(self.day_format) + # if previous_day != message_day: + # self.print_day_change(message_day) + # previous_day = message_day + for data in history: + uid, timestamp, from_jid, to_jid, message, subject, type_, extra_s = data + from_jid = jid.JID(from_jid) + to_jid = jid.JID(to_jid) + extra = data_format.deserialise(extra_s) + # if ((self.type == C.CHAT_GROUP and type_ != C.MESS_TYPE_GROUPCHAT) or + # (self.type == C.CHAT_ONE2ONE and type_ == C.MESS_TYPE_GROUPCHAT)): + # continue + extra["history"] = True + self.messages[uid] = Message( + self, + uid, + timestamp, + from_jid, + to_jid, + message, + subject, + type_, + extra, + profile, + ) + self._on_history_printed() + if callback is not None: + callback() + + def _history_get_eb(err): + log.error(_("Can't get history: {}").format(err)) + self._on_history_printed() + if callback is not None: + callback() + + self.host.bridge.history_get( + str(self.host.profiles[profile].whoami.bare), + str(target), + size, + True, + {k: str(v) for k,v in filters.items()}, + profile, + callback=_history_get_cb, + errback=_history_get_eb, + ) + + def message_encryption_get_cb(self, session_data): + if session_data: + session_data = data_format.deserialise(session_data) + self.message_encryption_started(session_data) + + def message_encryption_get_eb(self, failure_): + log.error(_("Can't get encryption state: {reason}").format(reason=failure_)) + + def get_encryption_state(self): + """Retrieve encryption state with current target. + + Once state is retrieved, default message_encryption_started will be called if + suitable + """ + if self.type == C.CHAT_GROUP: + return + self.host.bridge.message_encryption_get(str(self.target.bare), self.profile, + callback=self.message_encryption_get_cb, + errback=self.message_encryption_get_eb) + + + def message_new(self, uid, timestamp, from_jid, to_jid, msg, subject, type_, extra, + profile): + if self._locked: + self._cache[uid] = ( + uid, + timestamp, + from_jid, + to_jid, + msg, + subject, + type_, + extra, + profile, + ) + return + + if ((not msg and not subject and not extra[C.KEY_ATTACHMENTS] + and type_ != C.MESS_TYPE_INFO)): + log.warning("Received an empty message for uid {}".format(uid)) + return + + if self.type == C.CHAT_GROUP: + if to_jid.resource and type_ != C.MESS_TYPE_GROUPCHAT: + # we have a private message, we forward it to a private conversation + # widget + chat_widget = self.get_or_create_private_widget(to_jid) + chat_widget.message_new( + uid, timestamp, from_jid, to_jid, msg, subject, type_, extra, profile + ) + return + if type_ == C.MESS_TYPE_INFO: + try: + info_type = extra["info_type"] + except KeyError: + pass + else: + user_data = { + k[5:]: v for k, v in extra.items() if k.startswith("user_") + } + if info_type == ROOM_USER_JOINED: + self.addUser(user_data) + elif info_type == ROOM_USER_LEFT: + self.removeUser(user_data) + + message = Message( + self, uid, timestamp, from_jid, to_jid, msg, subject, type_, extra, profile + ) + self.messages[uid] = message + + if "received_timestamp" in extra: + log.warning("Delayed message received after history, this should not happen") + self.create_message(message) + + def message_encryption_started(self, session_data): + self.encrypted = True + log.debug(_("message encryption started with {target} using {encryption}").format( + target=self.target, encryption=session_data['name'])) + + def message_encryption_stopped(self, session_data): + self.encrypted = False + log.debug(_("message encryption stopped with {target} (was using {encryption})") + .format(target=self.target, encryption=session_data['name'])) + + def create_message(self, message, append=False): + """Must be implemented by frontend to create and show a new message widget + + This is only called on message_new, not on history. + You need to override history_print to handle the later + @param message(Message): message data + """ + raise NotImplementedError + + def is_user_moved(self, message): + """Return True if message is a user left/joined message + + @param message(Message): message to check + @return (bool): True is message is user moved info message + """ + if message.type != C.MESS_TYPE_INFO: + return False + try: + info_type = message.extra["info_type"] + except KeyError: + return False + else: + return info_type in ROOM_USER_MOVED + + def handle_user_moved(self, message): + """Check if this message is a UserMoved one, and merge it when possible + + "merge it" means that info message indicating a user joined/left will be + grouped if no other non-info messages has been sent since + @param message(Message): message to check + @return (bool): True if this message has been merged + if True, a new MessageWidget must not be created and appended to history + """ + if self.is_user_moved(message): + for wid in self.message_widgets_rev: + # we merge in/out messages if no message was sent meanwhile + if not isinstance(wid, MessageWidget): + continue + elif wid.mess_data.type != C.MESS_TYPE_INFO: + return False + elif ( + wid.info_type in ROOM_USER_MOVED + and wid.mess_data.nick == message.nick + ): + try: + count = wid.reentered_count + except AttributeError: + count = wid.reentered_count = 1 + nick = wid.mess_data.nick + if message.info_type == ROOM_USER_LEFT: + wid.message = _("<= {nick} has left the room ({count})").format( + nick=nick, count=count + ) + else: + wid.message = _( + "<=> {nick} re-entered the room ({count})" + ).format(nick=nick, count=count) + wid.reentered_count += 1 + return True + return False + + def print_day_change(self, day): + """Display the day on a new line. + + @param day(unicode): day to display (or not if this method is not overwritten) + """ + # FIXME: not called anymore after refactoring + pass + + ## Room ## + + def set_subject(self, subject): + """Set title for a group chat""" + if self.type != C.CHAT_GROUP: + raise exceptions.InternalError( + "trying to set subject for a non group chat window" + ) + self.subject = subject + + def change_subject(self, new_subject): + """Change the subject of the room + + This change the subject on the room itself (i.e. via XMPP), + while set_subject change the subject of this widget + """ + self.host.bridge.muc_subject(str(self.target), new_subject, self.profile) + + def add_game_panel(self, widget): + """Insert a game panel to this Chat dialog. + + @param widget (Widget): the game panel + """ + raise NotImplementedError + + def remove_game_panel(self, widget): + """Remove the game panel from this Chat dialog. + + @param widget (Widget): the game panel + """ + raise NotImplementedError + + def update(self, entity=None): + """Update one or all entities. + + @param entity (jid.JID): entity to update + """ + # FIXME: to remove ? + raise NotImplementedError + + ## events ## + + def on_chat_state(self, from_jid, state, profile): + """A chat state has been received""" + if self.type == C.CHAT_GROUP: + nick = from_jid.resource + try: + self.occupants[nick].state = state + except KeyError: + log.warning( + "{nick} not found in {room}, ignoring new chat state".format( + nick=nick, room=self.target.bare + ) + ) + + def on_message_state(self, uid, status, profile): + try: + mess_data = self.messages[uid] + except KeyError: + pass + else: + mess_data.status = status + + def on_avatar(self, entity, avatar_data, profile): + if self.type == C.CHAT_GROUP: + if entity.bare == self.target: + try: + self.occupants[entity.resource].update({"avatar": avatar_data}) + except KeyError: + # can happen for a message in history where the + # entity is not here anymore + pass + + for m in list(self.messages.values()): + if m.nick == entity.resource: + for w in m.widgets: + w.update({"avatar": avatar_data}) + else: + if ( + entity.bare == self.target.bare + or entity.bare == self.host.profiles[profile].whoami.bare + ): + log.info("avatar updated for {}".format(entity)) + for m in list(self.messages.values()): + if m.from_jid.bare == entity.bare: + for w in m.widgets: + w.update({"avatar": avatar_data}) + + +quick_widgets.register(QuickChat)