Mercurial > libervia-backend
view frontends/src/quick_frontend/quick_chat.py @ 2023:224c8b0886bc
plugin XEP-0184: Implementation of XEP-0184 (Message Delivery Receipts)
author | Geoffrey POUZET <chteufleur@kingpenguin.tk> |
---|---|
date | Thu, 28 Jul 2016 19:11:31 +0200 |
parents | 88c41a195728 |
children | 01aff34e8873 |
line wrap: on
line source
#!/usr/bin/env python2 # -*- coding: utf-8 -*- # helper class for making a SAT frontend # Copyright (C) 2009-2016 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 sat.core.i18n import _ from sat.core.log import getLogger log = getLogger(__name__) from sat.core import exceptions from sat_frontends.quick_frontend import quick_widgets from sat_frontends.quick_frontend.constants import Const as C from collections import OrderedDict from sat_frontends.tools import jid 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 try: # FIXME: to be removed when an acceptable solution is here unicode('') # XXX: unicode doesn't exist in pyjamas except (TypeError, AttributeError): # Error raised is not the same depending on pyjsbuild options unicode = str # FIXME: day_format need to be settable (i18n) class Message(object): """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.getNick(from_jid) # 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.itervalues(): if self.parent.nick.lower() in m.lower(): self._mention = True break self.widgets = set() # widgets linked to this message @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 main_message(self): """Return 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 = self.message.iteritems().next() self.selected_lang = lang return mess except StopIteration: log.error(u"Can't find message for uid {}".format(self.mess_data.uid)) def getNick(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(u"extra data is missing user nick for uid {}".format(self.uid)) return "" if self.parent.type == C.CHAT_GROUP or entity in contact_list.getSpecials(C.CONTACT_SPECIAL_GROUP): return entity.resource or "" if entity.bare in contact_list: return contact_list.getCache(entity, 'nick') or contact_list.getCache(entity, 'name') or entity.node or entity return entity.node or entity class Occupant(object): """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 jid(self): """jid in the room""" return jid.JID(u"{}/{}".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): self._state = new_state for w in self.widgets: w.updated(["state"]) class QuickChat(quick_widgets.QuickWidget): visible_states = ['chat_state'] def __init__(self, host, target, type_=C.CHAT_ONE2ONE, occupants=None, subject=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) self._locked = True # True when we are waiting for history/search # messageNew signals are cached when locked self._cache = OrderedDict() assert type_ in (C.CHAT_ONE2ONE, C.CHAT_GROUP) self.current_target = target self.type = type_ if type_ == C.CHAT_GROUP: if target.resource: raise exceptions.InternalError(u"a group chat entity can't have a resource") self.nick = None self.occupants = {} self.setOccupants(occupants) else: if occupants is not None: raise exceptions.InternalError(u"only group chat can have occupants") self.messages = OrderedDict() # key: uid, value: Message instance self.games = {} # key=game name (unicode), value=instance of quick_games.RoomGame self.subject = subject def postInit(self): """Method to be called by frontend after widget is initialised handle the display of history and subject """ self.historyPrint(profile=self.profile) if self.subject is not None: self.setSubject(self.subject) ## Widget management ## def __str__(self): return u"Chat Widget [target: {}, type: {}, profile: {}]".format(self.target, self.type, self.profile) @staticmethod def getWidgetHash(target, profiles): profile = profiles[0] return (profile, target.bare) @staticmethod def getPrivateHash(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 (unicode(profile), target) def addTarget(self, target): super(QuickChat, self).addTarget(target) if target.resource: self.current_target = target # FIXME: tmp, must use resource priority throught contactList instead def onPrivateCreated(self, widget): """Method called when a new widget for private conversation (MUC) is created""" raise NotImplementedError def getOrCreatePrivateWidget(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.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 @property def target(self): if self.type == C.CHAT_GROUP: return self.current_target.bare return self.current_target ## occupants ## def setOccupants(self, occupants): """set the whole list of occupants""" assert len(self.occupants) == 0 for nick, data in occupants.iteritems(): self.occupants[nick] = Occupant( self, data, self.profile ) 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(u"Trying to remove an unknown occupant: {}".format(nick)) else: return occupant def setUserNick(self, nick): """Set the nick of the user, usefull for e.g. change the color of the user""" self.nick = nick def changeUserNick(self, old_nick, new_nick): """Change nick of a user in group list""" self.printInfo("%s is now known as %s" % (old_nick, new_nick)) ## Messages ## def manageMessage(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 messageNew @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 updateHistory(self, size=C.HISTORY_LIMIT_DEFAULT, filters=None, profile='@NONE@'): """Called when history need to be recreated Remove all message from history then call historyPrint 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._locked = True self._cache = OrderedDict() self.messages.clear() self.historyPrint(size, filters, profile) def _onHistoryPrinted(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._locked = False for data in self._cache.itervalues(): self.messageNew(*data) del self._cache def historyPrint(self, size=C.HISTORY_LIMIT_DEFAULT, filters=None, profile='@NONE@'): """Print the current history @param size (int): number of messages @param search (str): pattern to filter the history results @param profile (str): %(doc_profile)s """ if filters is None: filters = {} if size == 0: log.debug(u"Empty history requested, skipping") self._onHistoryPrinted() return log_msg = _(u"now we print the history") if size != C.HISTORY_LIMIT_DEFAULT: log_msg += _(u" ({} 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) 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 def _historyGetCb(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.printDayChange(message_day) # previous_day = message_day for data in history: uid, timestamp, from_jid, to_jid, message, subject, type_, extra = data # cached messages may already be in history # so we check it to avoid duplicates, they'll be added later if uid in self._cache: continue from_jid = jid.JID(from_jid) to_jid = jid.JID(to_jid) # 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 self.messages[uid] = Message(self, uid, timestamp, from_jid, to_jid, message, subject, type_, extra, profile) self._onHistoryPrinted() def _historyGetEb(err): log.error(_(u"Can't get history")) self._onHistoryPrinted() self.host.bridge.historyGet(unicode(self.host.profiles[profile].whoami.bare), unicode(target), size, True, filters, profile, callback=_historyGetCb, errback=_historyGetEb) def messageNew(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 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.getOrCreatePrivateWidget(to_jid) chat_widget.messageNew(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.iteritems() 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(u"Delayed message received after history, this should not happen") self.createMessage(message) def createMessage(self, message, append=False): """Must be implemented by frontend to create and show a new message widget This is only called on messageNew, not on history. You need to override historyPrint to handle the later @param message(Message): message data """ raise NotImplementedError def printDayChange(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 setSubject(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 changeSubject(self, new_subject): """Change the subject of the room This change the subject on the room itself (i.e. via XMPP), while setSubject change the subject of this widget """ self.host.bridge.mucSubject(unicode(self.target), new_subject, self.profile) def addGamePanel(self, widget): """Insert a game panel to this Chat dialog. @param widget (Widget): the game panel """ raise NotImplementedError def removeGamePanel(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 """ raise NotImplementedError ## events ## def onChatState(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(u"{nick} not found in {room}, ignoring new chat state".format( nick=nick, room=self.target.bare)) quick_widgets.register(QuickChat)