comparison frontends/src/quick_frontend/quick_chat.py @ 1955:633b5c21aefd

backend, frontend: messages refactoring (huge commit, not finished): /!\ database schema has been modified, do a backup before updating message have been refactored, here are the main changes: - languages are now handled - all messages have an uid (internal to SàT) - message updating is anticipated - subject is now first class - new naming scheme is used newMessage => messageNew, getHistory => historyGet, sendMessage => messageSend - minimal compatibility refactoring in quick_frontend/Primitivus, better refactoring should follow - threads handling - delayed messages are saved into history - info messages may also be saved in history (e.g. to keep track of people joining/leaving a room) - duplicate messages should be avoided - historyGet return messages in right order, no need to sort again - plugins have been updated to follow new features, some of them need to be reworked (e.g. OTR) - XEP-0203 (Delayed Delivery) is now fully handled in core, the plugin just handle disco and creation of a delay element - /!\ jp and Libervia are currently broken, as some features of Primitivus It has been put in one huge commit to avoid breaking messaging between changes. This is the main part of message refactoring, other commits will follow to take profit of the new features/behaviour.
author Goffi <goffi@goffi.org>
date Tue, 24 May 2016 22:11:04 +0200
parents 2daf7b4c6756
children a2bc5089c2eb
comparison
equal deleted inserted replaced
1943:ccfe45302a5c 1955:633b5c21aefd
23 from sat_frontends.tools import jid 23 from sat_frontends.tools import jid
24 from sat_frontends.quick_frontend import quick_widgets 24 from sat_frontends.quick_frontend import quick_widgets
25 from sat_frontends.quick_frontend.constants import Const as C 25 from sat_frontends.quick_frontend.constants import Const as C
26 from collections import OrderedDict 26 from collections import OrderedDict
27 from datetime import datetime 27 from datetime import datetime
28 from time import time
29 28
30 try: 29 try:
31 # FIXME: to be removed when an acceptable solution is here 30 # FIXME: to be removed when an acceptable solution is here
32 unicode('') # XXX: unicode doesn't exist in pyjamas 31 unicode('') # XXX: unicode doesn't exist in pyjamas
33 except (TypeError, AttributeError): # Error raised is not the same depending on pyjsbuild options 32 except (TypeError, AttributeError): # Error raised is not the same depending on pyjsbuild options
50 self.type = type_ 49 self.type = type_
51 self.id = "" # FIXME: to be removed 50 self.id = "" # FIXME: to be removed
52 self.nick = None 51 self.nick = None
53 self.games = {} # key=game name (unicode), value=instance of quick_games.RoomGame 52 self.games = {} # key=game name (unicode), value=instance of quick_games.RoomGame
54 53
55 if type_ == C.CHAT_ONE2ONE: 54 self.historyPrint(profile=self.profile)
56 self.historyPrint(profile=self.profile)
57
58 # FIXME: has been introduced to temporarily fix http://bugs.goffi.org/show_bug.cgi?id=12
59 self.initialised = False
60 55
61 def __str__(self): 56 def __str__(self):
62 return u"Chat Widget [target: {}, type: {}, profile: {}]".format(self.target, self.type, self.profile) 57 return u"Chat Widget [target: {}, type: {}, profile: {}]".format(self.target, self.type, self.profile)
63 58
64 @staticmethod 59 @staticmethod
65 def getWidgetHash(target, profile): 60 def getWidgetHash(target, profiles):
66 return (unicode(profile), target.bare) 61 profile = profiles[0]
62 return (profile, target.bare)
67 63
68 @staticmethod 64 @staticmethod
69 def getPrivateHash(target, profile): 65 def getPrivateHash(target, profile):
70 """Get unique hash for private conversations 66 """Get unique hash for private conversations
71 67
97 93
98 def manageMessage(self, entity, mess_type): 94 def manageMessage(self, entity, mess_type):
99 """Tell if this chat widget manage an entity and message type couple 95 """Tell if this chat widget manage an entity and message type couple
100 96
101 @param entity (jid.JID): (full) jid of the sending entity 97 @param entity (jid.JID): (full) jid of the sending entity
102 @param mess_type (str): message type as given by newMessage 98 @param mess_type (str): message type as given by messageNew
103 @return (bool): True if this Chat Widget manage this couple 99 @return (bool): True if this Chat Widget manage this couple
104 """ 100 """
105 if self.type == C.CHAT_GROUP: 101 if self.type == C.CHAT_GROUP:
106 if mess_type == C.MESS_TYPE_GROUPCHAT and self.target == entity.bare: 102 if mess_type == C.MESS_TYPE_GROUPCHAT and self.target == entity.bare:
107 return True 103 return True
110 return True 106 return True
111 return False 107 return False
112 108
113 def addUser(self, nick): 109 def addUser(self, nick):
114 """Add user if it is not in the group list""" 110 """Add user if it is not in the group list"""
115 if not self.initialised:
116 return # FIXME: tmp fix for bug 12, do not flood the room with the messages when we've just entered it
117 self.printInfo("=> %s has joined the room" % nick) 111 self.printInfo("=> %s has joined the room" % nick)
118 112
119 def removeUser(self, nick): 113 def removeUser(self, nick):
120 """Remove a user from the group list""" 114 """Remove a user from the group list"""
121 self.printInfo("<= %s has left the room" % nick) 115 self.printInfo("<= %s has left the room" % nick)
139 """Refresh or scroll down the focus after the history is printed""" 133 """Refresh or scroll down the focus after the history is printed"""
140 pass 134 pass
141 135
142 def historyPrint(self, size=C.HISTORY_LIMIT_DEFAULT, search='', profile='@NONE@'): 136 def historyPrint(self, size=C.HISTORY_LIMIT_DEFAULT, search='', profile='@NONE@'):
143 """Print the current history 137 """Print the current history
138
144 @param size (int): number of messages 139 @param size (int): number of messages
145 @param search (str): pattern to filter the history results 140 @param search (str): pattern to filter the history results
146 @param profile (str): %(doc_profile)s 141 @param profile (str): %(doc_profile)s
147 """ 142 """
148 log_msg = _(u"now we print the history") 143 log_msg = _(u"now we print the history")
150 log_msg += _(u" (%d messages)" % size) 145 log_msg += _(u" (%d messages)" % size)
151 log.debug(log_msg) 146 log.debug(log_msg)
152 147
153 target = self.target.bare 148 target = self.target.bare
154 149
155 def onHistory(history): 150 def _historyGetCb(history):
156 self.initialised = True # FIXME: tmp fix for bug 12
157 day_format = "%A, %d %b %Y" # to display the day change 151 day_format = "%A, %d %b %Y" # to display the day change
158 previous_day = datetime.now().strftime(day_format) 152 previous_day = datetime.now().strftime(day_format)
159 for line in history: 153 for data in history:
160 timestamp, from_jid, to_jid, message, type_, extra = line # FIXME: extra is unused ! 154 uid, timestamp, from_jid, to_jid, message, subject, type_, extra = data # FIXME: extra is unused !
161 if ((self.type == C.CHAT_GROUP and type_ != C.MESS_TYPE_GROUPCHAT) or 155 if ((self.type == C.CHAT_GROUP and type_ != C.MESS_TYPE_GROUPCHAT) or
162 (self.type == C.CHAT_ONE2ONE and type_ == C.MESS_TYPE_GROUPCHAT)): 156 (self.type == C.CHAT_ONE2ONE and type_ == C.MESS_TYPE_GROUPCHAT)):
163 continue 157 continue
164 message_day = datetime.fromtimestamp(float(timestamp or time())).strftime(day_format) 158 message_day = datetime.fromtimestamp(timestamp).strftime(day_format)
165 if previous_day != message_day: 159 if previous_day != message_day:
166 self.printDayChange(message_day) 160 self.printDayChange(message_day)
167 previous_day = message_day 161 previous_day = message_day
168 extra["timestamp"] = timestamp 162 extra["timestamp"] = timestamp
169 self.newMessage(jid.JID(from_jid), target, message, type_, extra, profile) 163 self.messageNew(uid, timestamp, jid.JID(from_jid), target, message, subject, type_, extra, profile)
170 self.afterHistoryPrint() 164 self.afterHistoryPrint()
171 165
172 def onHistoryError(err): 166 def _historyGetEb(err):
173 log.error(_("Can't get history")) 167 log.error(_("Can't get history"))
174 168
175 self.initialised = False # FIXME: tmp fix for bug 12, here needed for :history and :search commands 169 self.host.bridge.historyGet(unicode(self.host.profiles[profile].whoami.bare), unicode(target), size, True, search, profile, callback=_historyGetCb, errback=_historyGetEb)
176 self.host.bridge.getHistory(unicode(self.host.profiles[profile].whoami.bare), unicode(target), size, True, search, profile, callback=onHistory, errback=onHistoryError)
177 170
178 def _get_nick(self, entity): 171 def _get_nick(self, entity):
179 """Return nick of this entity when possible""" 172 """Return nick of this entity when possible"""
180 contact_list = self.host.contact_lists[self.profile] 173 contact_list = self.host.contact_lists[self.profile]
181 if self.type == C.CHAT_GROUP or entity in contact_list.getSpecialExtras(C.CONTACT_SPECIAL_GROUP): 174 if self.type == C.CHAT_GROUP or entity in contact_list.getSpecialExtras(C.CONTACT_SPECIAL_GROUP):
193 186
194 @param entity: full jid of the target 187 @param entity: full jid of the target
195 """ 188 """
196 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 189 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
197 190
198 def newMessage(self, from_jid, target, msg, type_, extra, profile): 191 def messageNew(self, uid, timestamp, from_jid, target, msg, subject, type_, extra, profile):
192 try:
193 msg = msg.itervalues().next() # FIXME: tmp fix until message refactoring is finished (msg is now a dict)
194 except StopIteration:
195 log.warning(u"No message found (uid: {})".format(uid))
196 msg = ''
199 if self.type == C.CHAT_GROUP and target.resource and type_ != C.MESS_TYPE_GROUPCHAT: 197 if self.type == C.CHAT_GROUP and target.resource and type_ != C.MESS_TYPE_GROUPCHAT:
200 # we have a private message, we forward it to a private conversation widget 198 # we have a private message, we forward it to a private conversation widget
201 chat_widget = self.getOrCreatePrivateWidget(target) 199 chat_widget = self.getOrCreatePrivateWidget(target)
202 chat_widget.newMessage(from_jid, target, msg, type_, extra, profile) 200 chat_widget.messageNew(uid, timestamp, from_jid, target, msg, subject, type_, extra, profile)
203 return 201 return
204 try:
205 timestamp = float(extra['timestamp'])
206 except KeyError:
207 timestamp = None
208
209 if not self.initialised and self.type == C.CHAT_ONE2ONE:
210 return # FIXME: tmp fix for bug 12, do not display the first one2one message which is already in the local history
211 202
212 if type_ == C.MESS_TYPE_INFO: 203 if type_ == C.MESS_TYPE_INFO:
213 self.printInfo(msg, extra=extra) 204 self.printInfo(msg, extra=extra)
214 else: 205 else:
215 self.initialised = True # FIXME: tmp fix for bug 12, do not discard any message from now
216
217 nick = self._get_nick(from_jid) 206 nick = self._get_nick(from_jid)
218 if msg.startswith('/me '): 207 if msg.startswith('/me '):
219 self.printInfo('* %s %s' % (nick, msg[4:]), type_='me', extra=extra) 208 self.printInfo('* {} {}'.format(nick, msg[4:]), type_='me', extra=extra)
220 else: 209 else:
221 # my_message is True if message comes from local user 210 # my_message is True if message comes from local user
222 my_message = (from_jid.resource == self.nick) if self.type == C.CHAT_GROUP else (from_jid.bare == self.host.profiles[profile].whoami.bare) 211 my_message = (from_jid.resource == self.nick) if self.type == C.CHAT_GROUP else (from_jid.bare == self.host.profiles[profile].whoami.bare)
223 self.printMessage(nick, my_message, msg, timestamp, extra, profile) 212 self.printMessage(nick, my_message, msg, timestamp, extra, profile)
224 if timestamp: 213 # FIXME: to be checked/removed after message refactoring
225 self.afterHistoryPrint() 214 # if timestamp:
215 self.afterHistoryPrint()
226 216
227 def printMessage(self, nick, my_message, message, timestamp, extra=None, profile=C.PROF_KEY_NONE): 217 def printMessage(self, nick, my_message, message, timestamp, extra=None, profile=C.PROF_KEY_NONE):
228 """Print message in chat window. 218 """Print message in chat window.
229 219
230 @param nick (unicode): author nick 220 @param nick (unicode): author nick
231 @param my_message (boolean): True if profile is the author 221 @param my_message (boolean): True if profile is the author
232 @param message (unicode): message content 222 @param message (unicode): message content
233 @param extra (dict): extra data 223 @param extra (dict): extra data
234 """ 224 """
235 if not timestamp: 225 # FIXME: check/remove this if necessary (message refactoring)
236 # XXX: do not send notifications for each line of the history being displayed 226 # if not timestamp:
237 # FIXME: this must be changed in the future if the timestamp is passed with 227 # # XXX: do not send notifications for each line of the history being displayed
238 # all messages and not only with the messages coming from the history. 228 # # FIXME: this must be changed in the future if the timestamp is passed with
239 self.notify(nick, message) 229 # # all messages and not only with the messages coming from the history.
230 self.notify(nick, message)
240 231
241 def printInfo(self, msg, type_='normal', extra=None): 232 def printInfo(self, msg, type_='normal', extra=None):
242 """Print general info. 233 """Print general info.
243 234
244 @param msg (unicode): message to print 235 @param msg (unicode): message to print