Mercurial > libervia-backend
comparison sat_frontends/quick_frontend/quick_chat.py @ 2562:26edcf3a30eb
core, setup: huge cleaning:
- moved directories from src and frontends/src to sat and sat_frontends, which is the recommanded naming convention
- move twisted directory to root
- removed all hacks from setup.py, and added missing dependencies, it is now clean
- use https URL for website in setup.py
- removed "Environment :: X11 Applications :: GTK", as wix is deprecated and removed
- renamed sat.sh to sat and fixed its installation
- added python_requires to specify Python version needed
- replaced glib2reactor which use deprecated code by gtk3reactor
sat can now be installed directly from virtualenv without using --system-site-packages anymore \o/
author | Goffi <goffi@goffi.org> |
---|---|
date | Mon, 02 Apr 2018 19:44:50 +0200 |
parents | frontends/src/quick_frontend/quick_chat.py@0046283a285d |
children | 5e54afd17321 |
comparison
equal
deleted
inserted
replaced
2561:bd30dc3ffe5a | 2562:26edcf3a30eb |
---|---|
1 #!/usr/bin/env python2 | |
2 # -*- coding: utf-8 -*- | |
3 | |
4 # helper class for making a SAT frontend | |
5 # Copyright (C) 2009-2018 Jérôme Poisson (goffi@goffi.org) | |
6 | |
7 # This program is free software: you can redistribute it and/or modify | |
8 # it under the terms of the GNU Affero General Public License as published by | |
9 # the Free Software Foundation, either version 3 of the License, or | |
10 # (at your option) any later version. | |
11 | |
12 # This program is distributed in the hope that it will be useful, | |
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of | |
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
15 # GNU Affero General Public License for more details. | |
16 | |
17 # You should have received a copy of the GNU Affero General Public License | |
18 # along with this program. If not, see <http://www.gnu.org/licenses/>. | |
19 | |
20 from sat.core.i18n import _ | |
21 from sat.core.log import getLogger | |
22 log = getLogger(__name__) | |
23 from sat.core import exceptions | |
24 from sat_frontends.quick_frontend import quick_widgets | |
25 from sat_frontends.quick_frontend.constants import Const as C | |
26 from collections import OrderedDict | |
27 from sat_frontends.tools import jid | |
28 import time | |
29 try: | |
30 from locale import getlocale | |
31 except ImportError: | |
32 # FIXME: pyjamas workaround | |
33 getlocale = lambda x: (None, 'utf-8') | |
34 | |
35 | |
36 ROOM_USER_JOINED = 'ROOM_USER_JOINED' | |
37 ROOM_USER_LEFT = 'ROOM_USER_LEFT' | |
38 ROOM_USER_MOVED = (ROOM_USER_JOINED, ROOM_USER_LEFT) | |
39 | |
40 # from datetime import datetime | |
41 | |
42 try: | |
43 # FIXME: to be removed when an acceptable solution is here | |
44 unicode('') # XXX: unicode doesn't exist in pyjamas | |
45 except (TypeError, AttributeError): # Error raised is not the same depending on pyjsbuild options | |
46 unicode = str | |
47 | |
48 # FIXME: day_format need to be settable (i18n) | |
49 | |
50 class Message(object): | |
51 """Message metadata""" | |
52 | |
53 def __init__(self, parent, uid, timestamp, from_jid, to_jid, msg, subject, type_, extra, profile): | |
54 self.parent = parent | |
55 self.profile = profile | |
56 self.uid = uid | |
57 self.timestamp = timestamp | |
58 self.from_jid = from_jid | |
59 self.to_jid = to_jid | |
60 self.message = msg | |
61 self.subject = subject | |
62 self.type = type_ | |
63 self.extra = extra | |
64 self.nick = self.getNick(from_jid) | |
65 self._status = None | |
66 # own_mess is True if message was sent by profile's jid | |
67 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) | |
68 # is user mentioned here ? | |
69 if self.parent.type == C.CHAT_GROUP and not self.own_mess: | |
70 for m in msg.itervalues(): | |
71 if self.parent.nick.lower() in m.lower(): | |
72 self._mention = True | |
73 break | |
74 self.handleMe() | |
75 self.widgets = set() # widgets linked to this message | |
76 | |
77 @property | |
78 def host(self): | |
79 return self.parent.host | |
80 | |
81 @property | |
82 def info_type(self): | |
83 return self.extra.get('info_type') | |
84 | |
85 @property | |
86 def mention(self): | |
87 try: | |
88 return self._mention | |
89 except AttributeError: | |
90 return False | |
91 | |
92 @property | |
93 def main_message(self): | |
94 """currently displayed message""" | |
95 if self.parent.lang in self.message: | |
96 self.selected_lang = self.parent.lang | |
97 return self.message[self.parent.lang] | |
98 try: | |
99 self.selected_lang = '' | |
100 return self.message[''] | |
101 except KeyError: | |
102 try: | |
103 lang, mess = self.message.iteritems().next() | |
104 self.selected_lang = lang | |
105 return mess | |
106 except StopIteration: | |
107 log.error(u"Can't find message for uid {}".format(self.uid)) | |
108 return '' | |
109 | |
110 @property | |
111 def main_message_xhtml(self): | |
112 """rich message""" | |
113 xhtml = {k:v for k,v in self.extra.iteritems() if 'html' in k} | |
114 if xhtml: | |
115 # FIXME: we only return first found value for now | |
116 return next(xhtml.itervalues()) | |
117 | |
118 | |
119 @property | |
120 def time_text(self): | |
121 """Return timestamp in a nicely formatted way""" | |
122 # if the message was sent before today, we print the full date | |
123 timestamp = time.localtime(self.timestamp) | |
124 time_format = u"%c" if timestamp < self.parent.day_change else u"%H:%M" | |
125 return time.strftime(time_format, timestamp).decode(getlocale()[1] or 'utf-8') | |
126 | |
127 @property | |
128 def avatar(self): | |
129 """avatar full path or None if no avatar is found""" | |
130 ret = self.host.getAvatar(self.from_jid, profile=self.profile) | |
131 return ret | |
132 | |
133 def getNick(self, entity): | |
134 """Return nick of an entity when possible""" | |
135 contact_list = self.host.contact_lists[self.profile] | |
136 if self.type == C.MESS_TYPE_INFO and self.info_type in ROOM_USER_MOVED: | |
137 try: | |
138 return self.extra['user_nick'] | |
139 except KeyError: | |
140 log.error(u"extra data is missing user nick for uid {}".format(self.uid)) | |
141 return "" | |
142 # FIXME: converted getSpecials to list for pyjamas | |
143 if self.parent.type == C.CHAT_GROUP or entity in list(contact_list.getSpecials(C.CONTACT_SPECIAL_GROUP)): | |
144 return entity.resource or "" | |
145 if entity.bare in contact_list: | |
146 return contact_list.getCache(entity, 'nick') or contact_list.getCache(entity, 'name') or entity.node or entity | |
147 return entity.node or entity | |
148 | |
149 @property | |
150 def status(self): | |
151 return self._status | |
152 | |
153 @status.setter | |
154 def status(self, status): | |
155 if status != self._status: | |
156 self._status = status | |
157 for w in self.widgets: | |
158 w.update({"status": status}) | |
159 | |
160 def handleMe(self): | |
161 """Check if messages starts with "/me " and change them if it is the case | |
162 | |
163 if several messages (different languages) are presents, they all need to start with "/me " | |
164 """ | |
165 # TODO: XHTML-IM /me are not handled | |
166 me = False | |
167 # we need to check /me for every message | |
168 for m in self.message.itervalues(): | |
169 if m.startswith(u"/me "): | |
170 me = True | |
171 else: | |
172 me = False | |
173 break | |
174 if me: | |
175 self.type = C.MESS_TYPE_INFO | |
176 self.extra['info_type'] = 'me' | |
177 nick = self.nick | |
178 for lang, mess in self.message.iteritems(): | |
179 self.message[lang] = u"* " + nick + mess[3:] | |
180 | |
181 | |
182 class Occupant(object): | |
183 """Occupant metadata""" | |
184 | |
185 def __init__(self, parent, data, profile): | |
186 self.parent = parent | |
187 self.profile = profile | |
188 self.nick = data['nick'] | |
189 self._entity = data.get('entity') | |
190 self.affiliation = data['affiliation'] | |
191 self.role = data['role'] | |
192 self.widgets = set() # widgets linked to this occupant | |
193 self._state = None | |
194 | |
195 @property | |
196 def data(self): | |
197 """reconstruct data dict from attributes""" | |
198 data = {} | |
199 data['nick'] = self.nick | |
200 if self._entity is not None: | |
201 data['entity'] = self._entity | |
202 data['affiliation'] = self.affiliation | |
203 data['role'] = self.role | |
204 return data | |
205 | |
206 @property | |
207 def jid(self): | |
208 """jid in the room""" | |
209 return jid.JID(u"{}/{}".format(self.parent.target.bare, self.nick)) | |
210 | |
211 @property | |
212 def real_jid(self): | |
213 """real jid if known else None""" | |
214 return self._entity | |
215 | |
216 @property | |
217 def host(self): | |
218 return self.parent.host | |
219 | |
220 @property | |
221 def state(self): | |
222 return self._state | |
223 | |
224 @state.setter | |
225 def state(self, new_state): | |
226 if new_state != self._state: | |
227 self._state = new_state | |
228 for w in self.widgets: | |
229 w.update({"state": new_state}) | |
230 | |
231 def update(self, update_dict=None): | |
232 for w in self.widgets: | |
233 w.update(update_dict) | |
234 | |
235 | |
236 class QuickChat(quick_widgets.QuickWidget): | |
237 visible_states = ['chat_state'] # FIXME: to be removed, used only in quick_games | |
238 | |
239 def __init__(self, host, target, type_=C.CHAT_ONE2ONE, nick=None, occupants=None, subject=None, profiles=None): | |
240 """ | |
241 @param type_: can be C.CHAT_ONE2ONE for single conversation or C.CHAT_GROUP for chat à la IRC | |
242 """ | |
243 self.lang = '' # default language to use for messages | |
244 quick_widgets.QuickWidget.__init__(self, host, target, profiles=profiles) | |
245 self._locked = True # True when we are waiting for history/search | |
246 # messageNew signals are cached when locked | |
247 self._cache = OrderedDict() | |
248 assert type_ in (C.CHAT_ONE2ONE, C.CHAT_GROUP) | |
249 self.current_target = target | |
250 self.type = type_ | |
251 if type_ == C.CHAT_GROUP: | |
252 if target.resource: | |
253 raise exceptions.InternalError(u"a group chat entity can't have a resource") | |
254 if nick is None: | |
255 raise exceptions.InternalError(u"nick must not be None for group chat") | |
256 | |
257 self.nick = nick | |
258 self.occupants = {} | |
259 self.setOccupants(occupants) | |
260 else: | |
261 if occupants is not None or nick is not None: | |
262 raise exceptions.InternalError(u"only group chat can have occupants or nick") | |
263 self.messages = OrderedDict() # key: uid, value: Message instance | |
264 self.games = {} # key=game name (unicode), value=instance of quick_games.RoomGame | |
265 self.subject = subject | |
266 lt = time.localtime() | |
267 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 | |
268 if self.host.AVATARS_HANDLER: | |
269 self.host.addListener('avatar', self.onAvatar, profiles) | |
270 | |
271 def postInit(self): | |
272 """Method to be called by frontend after widget is initialised | |
273 | |
274 handle the display of history and subject | |
275 """ | |
276 self.historyPrint(profile=self.profile) | |
277 if self.subject is not None: | |
278 self.setSubject(self.subject) | |
279 | |
280 def onDelete(self): | |
281 if self.host.AVATARS_HANDLER: | |
282 self.host.removeListener('avatar', self.onAvatar) | |
283 | |
284 @property | |
285 def contact_list(self): | |
286 return self.host.contact_lists[self.profile] | |
287 | |
288 ## Widget management ## | |
289 | |
290 def __str__(self): | |
291 return u"Chat Widget [target: {}, type: {}, profile: {}]".format(self.target, self.type, self.profile) | |
292 | |
293 @staticmethod | |
294 def getWidgetHash(target, profiles): | |
295 profile = list(profiles)[0] | |
296 return profile + "\n" + unicode(target.bare) | |
297 | |
298 @staticmethod | |
299 def getPrivateHash(target, profile): | |
300 """Get unique hash for private conversations | |
301 | |
302 This method should be used with force_hash to get unique widget for private MUC conversations | |
303 """ | |
304 return (unicode(profile), target) | |
305 | |
306 def addTarget(self, target): | |
307 super(QuickChat, self).addTarget(target) | |
308 if target.resource: | |
309 self.current_target = target # FIXME: tmp, must use resource priority throught contactList instead | |
310 | |
311 def recreateArgs(self, args, kwargs): | |
312 """copy important attribute for a new widget""" | |
313 kwargs['type_'] = self.type | |
314 if self.type == C.CHAT_GROUP: | |
315 kwargs['occupants'] = {o.nick: o.data for o in self.occupants.itervalues()} | |
316 kwargs['subject'] = self.subject | |
317 try: | |
318 kwargs['nick'] = self.nick | |
319 except AttributeError: | |
320 pass | |
321 | |
322 def onPrivateCreated(self, widget): | |
323 """Method called when a new widget for private conversation (MUC) is created""" | |
324 raise NotImplementedError | |
325 | |
326 def getOrCreatePrivateWidget(self, entity): | |
327 """Create a widget for private conversation, or get it if it already exists | |
328 | |
329 @param entity: full jid of the target | |
330 """ | |
331 return self.host.widgets.getOrCreateWidget(QuickChat, entity, type_=C.CHAT_ONE2ONE, force_hash=self.getPrivateHash(self.profile, entity), on_new_widget=self.onPrivateCreated, profile=self.profile) # we force hash to have a new widget, not this one again | |
332 | |
333 @property | |
334 def target(self): | |
335 if self.type == C.CHAT_GROUP: | |
336 return self.current_target.bare | |
337 return self.current_target | |
338 | |
339 ## occupants ## | |
340 | |
341 def setOccupants(self, occupants): | |
342 """set the whole list of occupants""" | |
343 assert len(self.occupants) == 0 | |
344 for nick, data in occupants.iteritems(): | |
345 self.occupants[nick] = Occupant( | |
346 self, | |
347 data, | |
348 self.profile | |
349 ) | |
350 | |
351 def addUser(self, occupant_data): | |
352 """Add user if it is not in the group list""" | |
353 occupant = Occupant( | |
354 self, | |
355 occupant_data, | |
356 self.profile | |
357 ) | |
358 self.occupants[occupant.nick] = occupant | |
359 return occupant | |
360 | |
361 def removeUser(self, occupant_data): | |
362 """Remove a user from the group list""" | |
363 nick = occupant_data['nick'] | |
364 try: | |
365 occupant = self.occupants.pop(nick) | |
366 except KeyError: | |
367 log.warning(u"Trying to remove an unknown occupant: {}".format(nick)) | |
368 else: | |
369 return occupant | |
370 | |
371 def setUserNick(self, nick): | |
372 """Set the nick of the user, usefull for e.g. change the color of the user""" | |
373 self.nick = nick | |
374 | |
375 def changeUserNick(self, old_nick, new_nick): | |
376 """Change nick of a user in group list""" | |
377 self.printInfo("%s is now known as %s" % (old_nick, new_nick)) | |
378 | |
379 ## Messages ## | |
380 | |
381 def manageMessage(self, entity, mess_type): | |
382 """Tell if this chat widget manage an entity and message type couple | |
383 | |
384 @param entity (jid.JID): (full) jid of the sending entity | |
385 @param mess_type (str): message type as given by messageNew | |
386 @return (bool): True if this Chat Widget manage this couple | |
387 """ | |
388 if self.type == C.CHAT_GROUP: | |
389 if mess_type in (C.MESS_TYPE_GROUPCHAT, C.MESS_TYPE_INFO) and self.target == entity.bare: | |
390 return True | |
391 else: | |
392 if mess_type != C.MESS_TYPE_GROUPCHAT and entity in self.targets: | |
393 return True | |
394 return False | |
395 | |
396 def updateHistory(self, size=C.HISTORY_LIMIT_DEFAULT, filters=None, profile='@NONE@'): | |
397 """Called when history need to be recreated | |
398 | |
399 Remove all message from history then call historyPrint | |
400 Must probably be overriden by frontend to clear widget | |
401 @param size (int): number of messages | |
402 @param filters (str): patterns to filter the history results | |
403 @param profile (str): %(doc_profile)s | |
404 """ | |
405 self._locked = True | |
406 self._cache = OrderedDict() | |
407 self.messages.clear() | |
408 self.historyPrint(size, filters, profile) | |
409 | |
410 def _onHistoryPrinted(self): | |
411 """Method called when history is printed (or failed) | |
412 | |
413 unlock the widget, and can be used to refresh or scroll down | |
414 the focus after the history is printed | |
415 """ | |
416 self._locked = False | |
417 for data in self._cache.itervalues(): | |
418 self.messageNew(*data) | |
419 del self._cache | |
420 | |
421 def historyPrint(self, size=C.HISTORY_LIMIT_DEFAULT, filters=None, profile='@NONE@'): | |
422 """Print the current history | |
423 | |
424 @param size (int): number of messages | |
425 @param search (str): pattern to filter the history results | |
426 @param profile (str): %(doc_profile)s | |
427 """ | |
428 if filters is None: | |
429 filters = {} | |
430 if size == 0: | |
431 log.debug(u"Empty history requested, skipping") | |
432 self._onHistoryPrinted() | |
433 return | |
434 log_msg = _(u"now we print the history") | |
435 if size != C.HISTORY_LIMIT_DEFAULT: | |
436 log_msg += _(u" ({} messages)".format(size)) | |
437 log.debug(log_msg) | |
438 | |
439 if self.type == C.CHAT_ONE2ONE: | |
440 special = self.host.contact_lists[self.profile].getCache(self.target, C.CONTACT_SPECIAL) | |
441 if special == C.CONTACT_SPECIAL_GROUP: | |
442 # we have a private conversation | |
443 # so we need full jid for the history | |
444 # (else we would get history from group itself) | |
445 # and to filter out groupchat message | |
446 target = self.target | |
447 filters['not_types'] = C.MESS_TYPE_GROUPCHAT | |
448 else: | |
449 target = self.target.bare | |
450 else: | |
451 # groupchat | |
452 target = self.target.bare | |
453 # FIXME: info not handled correctly | |
454 filters['types'] = C.MESS_TYPE_GROUPCHAT | |
455 | |
456 def _historyGetCb(history): | |
457 # day_format = "%A, %d %b %Y" # to display the day change | |
458 # previous_day = datetime.now().strftime(day_format) | |
459 # message_day = datetime.fromtimestamp(timestamp).strftime(self.day_format) | |
460 # if previous_day != message_day: | |
461 # self.printDayChange(message_day) | |
462 # previous_day = message_day | |
463 for data in history: | |
464 uid, timestamp, from_jid, to_jid, message, subject, type_, extra = data | |
465 # cached messages may already be in history | |
466 # so we check it to avoid duplicates, they'll be added later | |
467 if uid in self._cache: | |
468 continue | |
469 from_jid = jid.JID(from_jid) | |
470 to_jid = jid.JID(to_jid) | |
471 # if ((self.type == C.CHAT_GROUP and type_ != C.MESS_TYPE_GROUPCHAT) or | |
472 # (self.type == C.CHAT_ONE2ONE and type_ == C.MESS_TYPE_GROUPCHAT)): | |
473 # continue | |
474 self.messages[uid] = Message(self, uid, timestamp, from_jid, to_jid, message, subject, type_, extra, profile) | |
475 self._onHistoryPrinted() | |
476 | |
477 def _historyGetEb(err): | |
478 log.error(_(u"Can't get history: {}").format(err)) | |
479 self._onHistoryPrinted() | |
480 | |
481 self.host.bridge.historyGet(unicode(self.host.profiles[profile].whoami.bare), unicode(target), size, True, filters, profile, callback=_historyGetCb, errback=_historyGetEb) | |
482 | |
483 def messageNew(self, uid, timestamp, from_jid, to_jid, msg, subject, type_, extra, profile): | |
484 if self._locked: | |
485 self._cache[uid] = (uid, timestamp, from_jid, to_jid, msg, subject, type_, extra, profile) | |
486 return | |
487 if self.type == C.CHAT_GROUP: | |
488 if to_jid.resource and type_ != C.MESS_TYPE_GROUPCHAT: | |
489 # we have a private message, we forward it to a private conversation widget | |
490 chat_widget = self.getOrCreatePrivateWidget(to_jid) | |
491 chat_widget.messageNew(uid, timestamp, from_jid, to_jid, msg, subject, type_, extra, profile) | |
492 return | |
493 if type_ == C.MESS_TYPE_INFO: | |
494 try: | |
495 info_type = extra['info_type'] | |
496 except KeyError: | |
497 pass | |
498 else: | |
499 user_data = {k[5:]:v for k,v in extra.iteritems() if k.startswith('user_')} | |
500 if info_type == ROOM_USER_JOINED: | |
501 self.addUser(user_data) | |
502 elif info_type == ROOM_USER_LEFT: | |
503 self.removeUser(user_data) | |
504 | |
505 message = Message(self, uid, timestamp, from_jid, to_jid, msg, subject, type_, extra, profile) | |
506 self.messages[uid] = message | |
507 | |
508 if 'received_timestamp' in extra: | |
509 log.warning(u"Delayed message received after history, this should not happen") | |
510 self.createMessage(message) | |
511 | |
512 def createMessage(self, message, append=False): | |
513 """Must be implemented by frontend to create and show a new message widget | |
514 | |
515 This is only called on messageNew, not on history. | |
516 You need to override historyPrint to handle the later | |
517 @param message(Message): message data | |
518 """ | |
519 raise NotImplementedError | |
520 | |
521 def printDayChange(self, day): | |
522 """Display the day on a new line. | |
523 | |
524 @param day(unicode): day to display (or not if this method is not overwritten) | |
525 """ | |
526 # FIXME: not called anymore after refactoring | |
527 pass | |
528 | |
529 ## Room ## | |
530 | |
531 def setSubject(self, subject): | |
532 """Set title for a group chat""" | |
533 if self.type != C.CHAT_GROUP: | |
534 raise exceptions.InternalError("trying to set subject for a non group chat window") | |
535 self.subject = subject | |
536 | |
537 def changeSubject(self, new_subject): | |
538 """Change the subject of the room | |
539 | |
540 This change the subject on the room itself (i.e. via XMPP), | |
541 while setSubject change the subject of this widget | |
542 """ | |
543 self.host.bridge.mucSubject(unicode(self.target), new_subject, self.profile) | |
544 | |
545 def addGamePanel(self, widget): | |
546 """Insert a game panel to this Chat dialog. | |
547 | |
548 @param widget (Widget): the game panel | |
549 """ | |
550 raise NotImplementedError | |
551 | |
552 def removeGamePanel(self, widget): | |
553 """Remove the game panel from this Chat dialog. | |
554 | |
555 @param widget (Widget): the game panel | |
556 """ | |
557 raise NotImplementedError | |
558 | |
559 def update(self, entity=None): | |
560 """Update one or all entities. | |
561 | |
562 @param entity (jid.JID): entity to update | |
563 """ | |
564 # FIXME: to remove ? | |
565 raise NotImplementedError | |
566 | |
567 ## events ## | |
568 | |
569 def onChatState(self, from_jid, state, profile): | |
570 """A chat state has been received""" | |
571 if self.type == C.CHAT_GROUP: | |
572 nick = from_jid.resource | |
573 try: | |
574 self.occupants[nick].state = state | |
575 except KeyError: | |
576 log.warning(u"{nick} not found in {room}, ignoring new chat state".format( | |
577 nick=nick, room=self.target.bare)) | |
578 | |
579 def onMessageState(self, uid, status, profile): | |
580 try: | |
581 mess_data = self.messages[uid] | |
582 except KeyError: | |
583 pass | |
584 else: | |
585 mess_data.status = status | |
586 | |
587 def onAvatar(self, entity, filename, profile): | |
588 if self.type == C.CHAT_GROUP: | |
589 if entity.bare == self.target: | |
590 try: | |
591 self.occupants[entity.resource].update({'avatar': filename}) | |
592 except KeyError: | |
593 # can happen for a message in history where the | |
594 # entity is not here anymore | |
595 pass | |
596 | |
597 for m in self.messages.values(): | |
598 if m.nick == entity.resource: | |
599 for w in m.widgets: | |
600 w.update({'avatar': filename}) | |
601 else: | |
602 if entity.bare == self.target.bare or entity.bare == self.host.profiles[profile].whoami.bare: | |
603 log.info(u"avatar updated for {}".format(entity)) | |
604 for m in self.messages.values(): | |
605 if m.from_jid.bare == entity.bare: | |
606 for w in m.widgets: | |
607 w.update({'avatar': filename}) | |
608 | |
609 | |
610 quick_widgets.register(QuickChat) |