libervia-backend/libervia/frontends/quick_frontend/quick_chat.py
2024-06-19 18:44:57 +02:00

987 lines
33 KiB
Python

#!/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)