backend, frontends: message refactoring (huge commit):

/!\ several features are temporarily disabled, like notifications in frontends

next step in refactoring, with the following changes:
    - jp: updated jp message to follow changes in backend/bridge
    - jp: added --lang, --subject, --subject_lang, and --type options to jp message + fixed unicode handling for jid
    - quick_frontend (QuickApp, QuickChat):
        - follow backend changes
        - refactored chat, message are now handled in OrderedDict and uid are kept so they can be updated
        - Message and Occupant classes handle metadata, so frontend just have to display them
    - Primitivus (Chat):
        - follow backend/QuickFrontend changes
        - info & standard messages are handled in the same MessageWidget class
        - improved/simplified handling of messages, removed update() method
        - user joined/left messages are merged when next to each other
        - a separator is shown when message is received while widget is out of focus, so user can quickly see the new messages
        - affiliation/role are shown (in a basic way for now) in occupants panel
        - removed "/me" messages handling, as it will be done by a backend plugin
        - message language is displayed when available (only one language per message for now)
        - fixed :history and :search commands
    - core (constants): new constants for messages type, XML namespace, entity type
    - core: *Message methods renamed to follow new code sytle (e.g. sendMessageToBridge => messageSendToBridge)
    - core (messages handling): fixed handling of language
    - core (messages handling): mes_data['from'] and ['to'] are now jid.JID
    - core (core.xmpp): reorganised message methods, added getNick() method to client.roster
    - plugin text commands: fixed plugin and adapted to new messages behaviour. client is now used in arguments instead of profile
    - plugins: added information for cancellation reason in CancelError calls
    - plugin XEP-0045: various improvments, but this plugin still need work:
        - trigger is used to avoid message already handled by the plugin to be handled a second time
        - changed the way to handle history, the last message from DB is checked and we request only messages since this one, in seconds (thanks Poezio folks :))
        - subject reception is waited before sending the roomJoined signal, this way we are sure that everything including history is ready
        - cmd_* method now follow the new convention with client instead of profile
        - roomUserJoined and roomUserLeft messages are removed, the events are now handled with info message with a "ROOM_USER_JOINED" info subtype
    - probably other forgotten stuffs :p
This commit is contained in:
Goffi 2016-06-20 18:41:53 +02:00
commit a7de473107
10 changed files with 987 additions and 598 deletions

View file

@ -103,14 +103,14 @@ class CommandExport(object):
except ValueError:
pass
def MessageReceivedTrigger(self, client, message, post_treat):
def MessageReceivedTrigger(self, client, message_elt, post_treat):
""" Check if source is linked and repeat message, else do nothing """
from_jid = jid.JID(message["from"])
from_jid = jid.JID(message_elt["from"])
spawned_key = (from_jid.userhostJID(), client.profile)
if spawned_key in self.spawned:
try:
body = message.elements(C.NS_CLIENT, 'body').next()
body = message_elt.elements(C.NS_CLIENT, 'body').next()
except StopIteration:
# do not block message without body (chat state notification...)
return True

View file

@ -67,12 +67,12 @@ class Exp_Parrot(object):
# log.debug("Parrot link detected, skipping other triggers")
# raise trigger.SkipOtherTriggers
def MessageReceivedTrigger(self, client, message, post_treat):
def MessageReceivedTrigger(self, client, message_elt, post_treat):
""" Check if source is linked and repeat message, else do nothing """
# TODO: many things are not repeated (subject, thread, etc)
profile = client.profile
client = self.host.getClient(profile)
from_jid = jid.JID(message["from"])
from_jid = message_elt["from"]
try:
_links = client.parrot_links
@ -83,7 +83,7 @@ class Exp_Parrot(object):
return True
message = {}
for e in message.elements(C.NS_CLIENT, 'body'):
for e in message_elt.elements(C.NS_CLIENT, 'body'):
body = unicode(e)
lang = e.getAttribute('lang') or ''
@ -106,12 +106,12 @@ class Exp_Parrot(object):
return True
def addParrot(self, source_jid, dest_jid, profile):
def addParrot(self, client, source_jid, dest_jid):
"""Add a parrot link from one entity to another one
@param source_jid: entity from who messages will be repeated
@param dest_jid: entity where the messages will be repeated
@param profile: %(doc_profile_key)s"""
client = self.host.getClient(profile)
"""
try:
_links = client.parrot_links
except AttributeError:
@ -120,17 +120,17 @@ class Exp_Parrot(object):
_links[source_jid.userhostJID()] = dest_jid
log.info(u"Parrot mode: %s will be repeated to %s" % (source_jid.userhost(), unicode(dest_jid)))
def removeParrot(self, source_jid, profile):
def removeParrot(self, client, source_jid):
"""Remove parrot link
@param source_jid: this entity will no more be repeated
@param profile: %(doc_profile_key)s"""
client = self.host.getClient(profile)
"""
try:
del client.parrot_links[source_jid.userhostJID()]
except (AttributeError, KeyError):
pass
def cmd_parrot(self, mess_data, profile):
def cmd_parrot(self, client, mess_data):
"""activate Parrot mode between 2 entities, in both directions."""
log.debug("Catched parrot command")
txt_cmd = self.host.plugins[C.TEXT_CMDS]
@ -140,19 +140,19 @@ class Exp_Parrot(object):
if not link_left_jid.user or not link_left_jid.host:
raise jid.InvalidFormat
except (RuntimeError, jid.InvalidFormat, AttributeError):
txt_cmd.feedBack("Can't activate Parrot mode for invalid jid", mess_data, profile)
txt_cmd.feedBack(client, "Can't activate Parrot mode for invalid jid", mess_data)
return False
link_right_jid = mess_data['to']
self.addParrot(link_left_jid, link_right_jid, profile)
self.addParrot(link_right_jid, link_left_jid, profile)
self.addParrot(client, link_left_jid, link_right_jid)
self.addParrot(client, link_right_jid, link_left_jid)
txt_cmd.feedBack("Parrot mode activated for %s" % (unicode(link_left_jid), ), mess_data, profile)
txt_cmd.feedBack(client, "Parrot mode activated for {}".format(unicode(link_left_jid)), mess_data)
return False
def cmd_unparrot(self, mess_data, profile):
def cmd_unparrot(self, client, mess_data):
"""remove Parrot mode between 2 entities, in both directions."""
log.debug("Catched unparrot command")
txt_cmd = self.host.plugins[C.TEXT_CMDS]
@ -162,14 +162,14 @@ class Exp_Parrot(object):
if not link_left_jid.user or not link_left_jid.host:
raise jid.InvalidFormat
except jid.InvalidFormat:
txt_cmd.feedBack("Can't deactivate Parrot mode for invalid jid", mess_data, profile)
txt_cmd.feedBack(client, u"Can't deactivate Parrot mode for invalid jid", mess_data)
return False
link_right_jid = mess_data['to']
self.removeParrot(link_left_jid, profile)
self.removeParrot(link_right_jid, profile)
self.removeParrot(client, link_left_jid)
self.removeParrot(client, link_right_jid)
txt_cmd.feedBack("Parrot mode deactivated for %s and %s" % (unicode(link_left_jid), unicode(link_right_jid)), mess_data, profile)
txt_cmd.feedBack(client, u"Parrot mode deactivated for {} and {}".format(unicode(link_left_jid), unicode(link_right_jid)), mess_data)
return False

View file

@ -46,6 +46,7 @@ class InvalidCommandSyntax(Exception):
CMD_KEY = "@command"
CMD_TYPES = ('group', 'one2one', 'all')
FEEDBACK_INFO_TYPE = "TEXT_CMD"
class TextCommands(object):
@ -148,7 +149,7 @@ class TextCommands(object):
while (cmd_name + str(suff)) in self._commands:
suff+=1
new_name = cmd_name + str(suff)
log.warning(_(u"Conflict for command [%(old_name)s], renaming it to [%(new_name)s]") % {'old_name': cmd_name, 'new_name': new_name})
log.warning(_(u"Conflict for command [{old_name}], renaming it to [{new_name}]").format(old_name=cmd_name, new_name=new_name))
cmd_name = new_name
self._commands[cmd_name] = cmd_data = OrderedDict({'callback':cmd}) # We use an Ordered dict to keep documenation order
cmd_data.update(self._parseDocString(cmd, cmd_name))
@ -182,7 +183,6 @@ class TextCommands(object):
@param mess_data(dict): data comming from messageSend trigger
@param profile: %(doc_profile)s
"""
profile = client.profile
try:
msg = mess_data["message"]['']
msg_lang = ''
@ -217,7 +217,7 @@ class TextCommands(object):
if ret:
return mess_data
else:
log.debug("text commands took over")
log.debug(u"text command detected ({})".format(command))
raise failure.Failure(exceptions.CancelError())
def genericErrback(failure):
@ -225,26 +225,26 @@ class TextCommands(object):
msg = u"with condition {}".format(failure.value.condition)
except AttributeError:
msg = u"with error {}".format(failure.value)
self.feedBack(u"Command failed {}".format(msg), mess_data, profile)
self.feedBack(client, u"Command failed {}".format(msg), mess_data)
return False
mess_data["unparsed"] = msg[1 + len(command):] # part not yet parsed of the message
try:
cmd_data = self._commands[command]
except KeyError:
self.feedBack(_("Unknown command /%s. ") % command + self.HELP_SUGGESTION, mess_data, profile)
log.debug("text commands took over")
self.feedBack(client, _("Unknown command /%s. ") % command + self.HELP_SUGGESTION, mess_data)
log.debug("text command help message")
raise failure.Failure(exceptions.CancelError())
else:
if not self._contextValid(mess_data, cmd_data):
# The command is not launched in the right context, we throw a message with help instructions
context_txt = _("group discussions") if cmd_data["type"] == "group" else _("one to one discussions")
feedback = _("/{command} command only applies in {context}.").format(command=command, context=context_txt)
self.feedBack(u"{} {}".format(feedback, self.HELP_SUGGESTION), mess_data, profile)
log.debug("text commands took over")
self.feedBack(client, u"{} {}".format(feedback, self.HELP_SUGGESTION), mess_data)
log.debug("text command invalid message")
raise failure.Failure(exceptions.CancelError())
else:
d = defer.maybeDeferred(cmd_data["callback"], mess_data, profile)
d = defer.maybeDeferred(cmd_data["callback"], client, mess_data)
d.addErrback(genericErrback)
d.addCallback(retHandling)
@ -276,16 +276,22 @@ class TextCommands(object):
return jid.JID(arg + service_jid)
return jid.JID(u"%s@%s" % (arg, service_jid))
def feedBack(self, message, mess_data, profile):
def feedBack(self, client, message, mess_data, info_type=FEEDBACK_INFO_TYPE):
"""Give a message back to the user"""
if mess_data["type"] == 'groupchat':
_from = mess_data["to"].userhostJID()
to_ = mess_data["to"].userhostJID()
else:
_from = self.host.getJidNStream(profile)[0]
to_ = client.jid
self.host.bridge.messageNew(unicode(mess_data["to"]), {'': message}, {}, C.MESS_TYPE_INFO, unicode(_from), {}, profile=profile)
# we need to invert send message back, so sender need to original destinee
mess_data["from"] = mess_data["to"]
mess_data["to"] = to_
mess_data["type"] = C.MESS_TYPE_INFO
mess_data["message"] = {'': message}
mess_data["extra"]["info_type"] = info_type
self.host.messageSendToBridge(mess_data, client)
def cmd_whois(self, mess_data, profile):
def cmd_whois(self, client, mess_data):
"""show informations on entity
@command: [JID|ROOM_NICK]
@ -299,7 +305,7 @@ class TextCommands(object):
if mess_data['type'] == "groupchat":
room = mess_data["to"].userhostJID()
try:
if self.host.plugins["XEP-0045"].isNickInRoom(room, entity, profile):
if self.host.plugins["XEP-0045"].isNickInRoom(room, entity, client.profile):
entity = u"%s/%s" % (room, entity)
except KeyError:
log.warning("plugin XEP-0045 is not present")
@ -312,20 +318,20 @@ class TextCommands(object):
if not target_jid.user or not target_jid.host:
raise jid.InvalidFormat
except (RuntimeError, jid.InvalidFormat, AttributeError):
self.feedBack(_("Invalid jid, can't whois"), mess_data, profile)
self.feedBack(client, _("Invalid jid, can't whois"), mess_data)
return False
if not target_jid.resource:
target_jid.resource = self.host.memory.getMainResource(target_jid, profile)
target_jid.resource = self.host.memory.getMainResource(target_jid, client.profile)
whois_msg = [_(u"whois for %(jid)s") % {'jid': target_jid}]
d = defer.succeed(None)
for ignore, callback in self._whois:
d.addCallback(lambda ignore: callback(whois_msg, mess_data, target_jid, profile))
d.addCallback(lambda ignore: callback(client, whois_msg, mess_data, target_jid))
def feedBack(ignore):
self.feedBack(u"\n".join(whois_msg), mess_data, profile)
self.feedBack(client, u"\n".join(whois_msg), mess_data)
return False
d.addCallback(feedBack)
@ -345,7 +351,7 @@ class TextCommands(object):
return strings
def cmd_help(self, mess_data, profile):
def cmd_help(self, client, mess_data):
"""show help on available commands
@command: [cmd_name]
@ -355,7 +361,7 @@ class TextCommands(object):
if cmd_name and cmd_name[0] == "/":
cmd_name = cmd_name[1:]
if cmd_name and cmd_name not in self._commands:
self.feedBack(_(u"Invalid command name [{}]\n".format(cmd_name)), mess_data, profile)
self.feedBack(client, _(u"Invalid command name [{}]\n".format(cmd_name)), mess_data)
cmd_name = ""
if not cmd_name:
# we show the global help
@ -384,13 +390,4 @@ class TextCommands(object):
syntax=_(" "*4+"syntax: {}\n").format(syntax) if syntax else "",
args_help=u'\n'.join([u" "*8+"{}".format(line) for line in self._getArgsHelp(cmd_data)]))
self.feedBack(help_mess, mess_data, profile)
def cmd_me(self, mess_data, profile):
"""Display a message at third person
@command: message
- message: message to display at the third person
"""
# We just catch the method and continue it as the frontends should manage /me display
return True
self.feedBack(client, help_mess, mess_data)

View file

@ -209,7 +209,7 @@ class OTR(object):
self.skipped_profiles = set()
host.trigger.add("MessageReceived", self.MessageReceivedTrigger, priority=100000)
host.trigger.add("messageSend", self.messageSendTrigger, priority=100000)
host.bridge.addMethod("skipOTR", ".plugin", in_sign='s', out_sign='', method=self._skipOTR)
host.bridge.addMethod("skipOTR", ".plugin", in_sign='s', out_sign='', method=self._skipOTR) # FIXME: must be removed, must be done on per-message basis
host.importMenu((MAIN_MENU, D_("Start/Refresh")), self._startRefresh, security_limit=0, help_string=D_("Start or refresh an OTR session"), type_=C.MENU_SINGLE)
host.importMenu((MAIN_MENU, D_("End session")), self._endSession, security_limit=0, help_string=D_("Finish an OTR session"), type_=C.MENU_SINGLE)
host.importMenu((MAIN_MENU, D_("Authenticate")), self._authenticate, security_limit=0, help_string=D_("Authenticate user/see your fingerprint"), type_=C.MENU_SINGLE)
@ -237,6 +237,9 @@ class OTR(object):
@param profile (str): %(doc_profile)s
"""
# FIXME: should not be done per profile but per message, using extra data
# for message received, profile wide hook may be need, but client
# should be used anyway instead of a class attribute
self.skipped_profiles.add(profile)
@defer.inlineCallbacks
@ -406,7 +409,7 @@ class OTR(object):
return {'xmlui': confirm.toXml()}
def _receivedTreatment(self, data, profile):
from_jid = jid.JID(data['from'])
from_jid = data['from']
log.debug(u"_receivedTreatment [from_jid = %s]" % from_jid)
otrctx = self.context_managers[profile].getContextForUser(from_jid)
encrypted = True
@ -437,7 +440,7 @@ class OTR(object):
data['message'] = {'':res[0].decode('utf-8')} # FIXME: Q&D fix for message refactoring, message is now a dict
raise failure.Failure(exceptions.SkipHistory()) # we send the decrypted message to frontends, but we don't want it in history
else:
raise failure.Failure(exceptions.CancelError()) # no message at all (no history, no signal)
raise failure.Failure(exceptions.CancelError('Cancelled by OTR')) # no message at all (no history, no signal)
def _receivedTreatmentForSkippedProfiles(self, data, profile):
"""This profile must be skipped because the frontend manages OTR itself,
@ -450,7 +453,7 @@ class OTR(object):
raise failure.Failure(exceptions.SkipHistory())
return data
def MessageReceivedTrigger(self, client, message, post_treat):
def MessageReceivedTrigger(self, client, message_elt, post_treat):
profile = client.profile
if profile in self.skipped_profiles:
post_treat.addCallback(self._receivedTreatmentForSkippedProfiles, profile)
@ -478,7 +481,7 @@ class OTR(object):
log.warning(u"No message found")
return False
otrctx.sendMessage(0, msg.encode('utf-8'))
self.host.sendMessageToBridge(mess_data, client)
self.host.messageSendToBridge(mess_data, client)
else:
feedback = D_("Your message was not sent because your correspondent closed the encrypted conversation on his/her side. Either close your own side, or refresh the session.")
self.host.bridge.messageNew(to_jid.full(),

View file

@ -86,7 +86,7 @@ class XEP_0033(object):
def discoCallback(entities):
if not entities:
log.warning(_("XEP-0033 is being used but the server doesn't support it!"))
raise failure.Failure(exceptions.CancelError())
raise failure.Failure(exceptions.CancelError(u'Cancelled by XEP-0033'))
if mess_data["to"] not in entities:
expected = _(' or ').join([entity.userhost() for entity in entities])
log.warning(_(u"Stanzas using XEP-0033 should be addressed to %(expected)s, not %(current)s!") % {'expected': expected, 'current': mess_data["to"]})
@ -99,7 +99,7 @@ class XEP_0033(object):
# when the prosody plugin is completed, we can immediately return mess_data from here
self.sendAndStoreMessage(mess_data, entries, profile)
log.debug("XEP-0033 took over")
raise failure.Failure(exceptions.CancelError())
raise failure.Failure(exceptions.CancelError(u'Cancelled by XEP-0033'))
d = self.host.findFeaturesSet([NS_ADDRESS], profile=profile)
d.addCallbacks(discoCallback, lambda dummy: discoCallback(None))
return d
@ -124,9 +124,9 @@ class XEP_0033(object):
client = self.host.profiles[profile]
d = defer.Deferred()
if not skip_send:
d.addCallback(self.host._sendMessageToStream, client)
d.addCallback(self.host._storeMessage, client)
d.addCallback(self.host.sendMessageToBridge, client)
d.addCallback(self.host.messageSendToStream, client)
d.addCallback(self.host.messageAddToHistory, client)
d.addCallback(self.host.messageSendToBridge, client)
d.addErrback(lambda failure: failure.trap(exceptions.CancelError))
return d.callback(mess_data)

View file

@ -23,10 +23,13 @@ from sat.core.log import getLogger
log = getLogger(__name__)
from twisted.internet import defer
from twisted.words.protocols.jabber import jid
from dateutil.tz import tzutc
from sat.core import exceptions
from sat.memory import memory
import calendar
import time
import uuid
import copy
@ -50,6 +53,10 @@ PLUGIN_INFO = {
NS_MUC = 'http://jabber.org/protocol/muc'
AFFILIATIONS = ('owner', 'admin', 'member', 'none', 'outcast')
ROOM_USER_JOINED = 'ROOM_USER_JOINED'
ROOM_USER_LEFT = 'ROOM_USER_LEFT'
OCCUPANT_KEYS = ('nick', 'entity', 'affiliation', 'role')
ENTITY_TYPE_MUC = "MUC"
CONFIG_SECTION = u'plugin muc'
@ -73,20 +80,18 @@ class XEP_0045(object):
def __init__(self, host):
log.info(_("Plugin XEP_0045 initialization"))
self.host = host
self.clients = {}
self.clients = {} # FIXME: should be moved to profile's client
self._sessions = memory.Sessions()
host.bridge.addMethod("joinMUC", ".plugin", in_sign='ssa{ss}s', out_sign='s', method=self._join, async=True)
host.bridge.addMethod("mucJoin", ".plugin", in_sign='ssa{ss}s', out_sign='s', method=self._join, async=True)
host.bridge.addMethod("mucNick", ".plugin", in_sign='sss', out_sign='', method=self.mucNick)
host.bridge.addMethod("mucLeave", ".plugin", in_sign='ss', out_sign='', method=self.mucLeave, async=True)
host.bridge.addMethod("getRoomsJoined", ".plugin", in_sign='s', out_sign='a(sass)', method=self.getRoomsJoined)
host.bridge.addMethod("getRoomsJoined", ".plugin", in_sign='s', out_sign='a(sa{sa{ss}}ss)', method=self.getRoomsJoined)
host.bridge.addMethod("getRoomsSubjects", ".plugin", in_sign='s', out_sign='a(ss)', method=self.getRoomsSubjects)
host.bridge.addMethod("getUniqueRoomName", ".plugin", in_sign='ss', out_sign='s', method=self._getUniqueName)
host.bridge.addMethod("configureRoom", ".plugin", in_sign='ss', out_sign='s', method=self._configureRoom, async=True)
host.bridge.addMethod("getDefaultMUC", ".plugin", in_sign='', out_sign='s', method=self.getDefaultMUC)
host.bridge.addSignal("roomJoined", ".plugin", signature='sasss') # args: room_jid, room_nicks, user_nick, profile
host.bridge.addSignal("roomJoined", ".plugin", signature='sa{sa{ss}}sss') # args: room_jid, occupants, user_nick, subject, profile
host.bridge.addSignal("roomLeft", ".plugin", signature='ss') # args: room_jid, profile
host.bridge.addSignal("roomUserJoined", ".plugin", signature='ssa{ss}s') # args: room_jid, user_nick, user_data, profile
host.bridge.addSignal("roomUserLeft", ".plugin", signature='ssa{ss}s') # args: room_jid, user_nick, user_data, profile
host.bridge.addSignal("roomUserChangedNick", ".plugin", signature='ssss') # args: room_jid, old_nick, new_nick, profile
host.bridge.addSignal("roomNewSubject", ".plugin", signature='sss') # args: room_jid, subject, profile
self.__submit_conf_id = host.registerCallback(self._submitConfiguration, with_data=True)
@ -98,6 +103,7 @@ class XEP_0045(object):
log.info(_("Text commands not available"))
host.trigger.add("presence_available", self.presenceTrigger)
host.trigger.add("MessageReceived", self.MessageReceivedTrigger, priority=1000000)
def profileConnected(self, profile):
def assign_service(service):
@ -105,6 +111,23 @@ class XEP_0045(object):
client.muc_service = service
return self.getMUCService(profile=profile).addCallback(assign_service)
def MessageReceivedTrigger(self, client, message_elt, post_treat):
if message_elt.getAttribute("type") == C.MESS_TYPE_GROUPCHAT:
if message_elt.subject or message_elt.delay:
return False
from_jid = jid.JID(message_elt['from'])
room_jid = from_jid.userhostJID()
if room_jid in self.clients[client.profile].joined_rooms:
room = self.clients[client.profile].joined_rooms[room_jid]
if not room._room_ok:
log.warning(u"Received non delayed message in a room before its initialisation: {}".format(message_elt.toXml()))
room._cache.append(message_elt)
return False
else:
log.warning(u"Received groupchat message for a room which has not been joined, ignoring it: {}".format(message_elt.toXml()))
return False
return True
def checkClient(self, profile):
"""Check if the profile is connected and has used the MUC feature.
@ -132,29 +155,23 @@ class XEP_0045(object):
raise UnknownRoom("This room has not been joined")
return profile
def __room_joined(self, room, profile):
def _joinCb(self, room, profile):
"""Called when the user is in the requested room"""
def _sendBridgeSignal(ignore=None):
self.host.bridge.roomJoined(room.roomJID.userhost(), [user.nick for user in room.roster.values()], room.nick, profile)
self.clients[profile].joined_rooms[room.roomJID] = room
if room.locked:
# FIXME: the current behaviour is to create an instant room
# and send the signal only when the room is unlocked
# a proper configuration management should be done
print "room locked !"
self.clients[profile].configure(room.roomJID, {}).addCallbacks(_sendBridgeSignal, lambda x: log.error(_(u'Error while configuring the room')))
else:
_sendBridgeSignal()
d = self.clients[profile].configure(room.roomJID, {})
d.addErrback(lambda dummy: log.error(_(u'Error while configuring the room')))
return room
def __err_joining_room(self, failure, room_jid, nick, history_options, password, profile):
def _joinEb(self, failure, room_jid, nick, password, profile):
"""Called when something is going wrong when joining the room"""
if hasattr(failure.value, "condition") and failure.value.condition == 'conflict':
# we have a nickname conflict, we try again with "_" suffixed to current nickname
nick += '_'
return self.clients[profile].join(room_jid, nick, history_options, password).addCallbacks(self.__room_joined, self.__err_joining_room, callbackKeywords={'profile': profile}, errbackArgs=[room_jid, nick, history_options, password, profile])
return self.clients[profile].join(room_jid, nick, password).addCallbacks(self._joinCb, self._joinEb, callbackKeywords={'profile': profile}, errbackArgs=[room_jid, nick, password, profile])
mess = D_("Error while joining the room %s" % room_jid.userhost())
try:
mess += " with condition '%s'" % failure.value.condition
@ -164,6 +181,11 @@ class XEP_0045(object):
self.host.bridge.newAlert(mess, D_("Group chat error"), "ERROR", profile)
raise failure
@staticmethod
def _getOccupants(room):
"""Get occupants of a room in a form suitable for bridge"""
return {u.nick: {k:unicode(getattr(u,k) or '') for k in OCCUPANT_KEYS} for u in room.roster.values()}
def isRoom(self, entity_bare, profile_key):
"""Tell if a bare entity is a MUC room.
@ -181,7 +203,8 @@ class XEP_0045(object):
if not self.checkClient(profile):
return result
for room in self.clients[profile].joined_rooms.values():
result.append((room.roomJID.userhost(), [user.nick for user in room.roster.values()], room.nick))
if room._room_ok:
result.append((room.roomJID.userhost(), self._getOccupants(room), room.nick, room.subject))
return result
def getRoomNick(self, room_jid, profile_key=C.PROF_KEY_NONE):
@ -301,6 +324,7 @@ class XEP_0045(object):
def getRoomsSubjects(self, profile_key=C.PROF_KEY_NONE):
"""Return received subjects of rooms"""
# FIXME: to be removed
profile = self.host.memory.getProfileName(profile_key)
if not self.checkClient(profile):
return []
@ -355,45 +379,31 @@ class XEP_0045(object):
"""
return self.host.memory.getConfig(CONFIG_SECTION, 'default_muc', default_conf['default_muc'])
def join(self, room_jid, nick, options, profile_key=C.PROF_KEY_NONE):
def join(self, client, room_jid, nick, options):
def _errDeferred(exc_obj=Exception, txt='Error while joining room'):
d = defer.Deferred()
d.errback(exc_obj(txt))
return d
profile = self.host.memory.getProfileName(profile_key)
if not self.checkClient(profile):
return _errDeferred()
if room_jid in self.clients[profile].joined_rooms:
log.warning(_(u'%(profile)s is already in room %(room_jid)s') % {'profile': profile, 'room_jid': room_jid.userhost()})
if room_jid in self.clients[client.profile].joined_rooms:
log.warning(_(u'%(profile)s is already in room %(room_jid)s') % {'profile': client.profile, 'room_jid': room_jid.userhost()})
return _errDeferred(AlreadyJoinedRoom, D_(u"The room has already been joined"))
log.info(_(u"[%(profile)s] is joining room %(room)s with nick %(nick)s") % {'profile': profile, 'room': room_jid.userhost(), 'nick': nick})
log.info(_(u"[%(profile)s] is joining room %(room)s with nick %(nick)s") % {'profile': client.profile, 'room': room_jid.userhost(), 'nick': nick})
if "history" in options:
history_limit = int(options["history"])
else:
history_limit = int(self.host.memory.getParamA(C.HISTORY_LIMIT, 'General', profile_key=profile))
# http://xmpp.org/extensions/xep-0045.html#enter-managehistory
history_options = muc.HistoryOptions(maxStanzas=history_limit)
password = options["password"] if "password" in options else None
return self.clients[profile].join(room_jid, nick, history_options, password).addCallbacks(self.__room_joined, self.__err_joining_room, callbackKeywords={'profile': profile}, errbackArgs=[room_jid, nick, history_options, password, profile])
# FIXME: how to set the cancel method on the Deferred created by wokkel?
# This happens when the room is not reachable, e.g. no internet connection:
# > /usr/local/lib/python2.7/dist-packages/twisted/internet/defer.py(480)_startRunCallbacks()
# -> raise AlreadyCalledError(extra)
return self.clients[client.profile].join(room_jid, nick, password).addCallbacks(self._joinCb, self._joinEb, callbackKeywords={'profile': client.profile}, errbackArgs=[room_jid, nick, password, client.profile])
def _join(self, room_jid_s, nick, options=None, profile_key=C.PROF_KEY_NONE):
"""join method used by bridge: use the join method, but doesn't return any deferred
"""join method used by bridge
@return: unicode (the room bare)
"""
client = self.host.getClient(profile_key)
if options is None:
options = {}
profile = self.host.memory.getProfileName(profile_key)
if not self.checkClient(profile):
return
if room_jid_s:
muc_service = self.host.getClient(profile).muc_service
muc_service = self.host.getClient(client.profile).muc_service
try:
room_jid = jid.JID(room_jid_s)
except (RuntimeError, jid.InvalidFormat, AttributeError):
@ -401,9 +411,9 @@ class XEP_0045(object):
if not room_jid.user:
room_jid.user, room_jid.host = room_jid.host, muc_service
else:
room_jid = self.getUniqueName(profile_key=profile_key)
room_jid = self.getUniqueName(profile_key=client.profile)
# TODO: error management + signal in bridge
d = self.join(room_jid, nick, options, profile)
d = self.join(client, room_jid, nick, options)
return d.addCallback(lambda room: room.roomJID.userhost())
def nick(self, room_jid, nick, profile_key):
@ -477,7 +487,7 @@ class XEP_0045(object):
# Text commands #
def cmd_nick(self, mess_data, profile):
def cmd_nick(self, client, mess_data):
"""change nickname
@command (group): new_nick
@ -486,11 +496,11 @@ class XEP_0045(object):
nick = mess_data["unparsed"].strip()
if nick:
room = mess_data["to"]
self.nick(room, nick, profile)
self.nick(room, nick, client.profile)
return False
def cmd_join(self, mess_data, profile):
def cmd_join(self, client, mess_data):
"""join a new room
@command (all): JID
@ -498,13 +508,13 @@ class XEP_0045(object):
"""
if mess_data["unparsed"].strip():
room_jid = self.host.plugins[C.TEXT_CMDS].getRoomJID(mess_data["unparsed"].strip(), mess_data["to"].host)
nick = (self.getRoomNick(room_jid, profile) or
self.host.getClient(profile).jid.user)
self.join(room_jid, nick, {}, profile)
nick = (self.getRoomNick(room_jid, client.profile) or
self.host.getClient(client.profile).jid.user)
self.join(client, room_jid, nick, {})
return False
def cmd_leave(self, mess_data, profile):
def cmd_leave(self, client, mess_data):
"""quit a room
@command (group): [ROOM_JID]
@ -515,19 +525,19 @@ class XEP_0045(object):
else:
room = mess_data["to"]
self.leave(room, profile)
self.leave(room, client.profile)
return False
def cmd_part(self, mess_data, profile):
def cmd_part(self, client, mess_data):
"""just a synonym of /leave
@command (group): [ROOM_JID]
- ROOM_JID: jid of the room to live (current room if not specified)
"""
return self.cmd_leave(mess_data, profile)
return self.cmd_leave(client, mess_data)
def cmd_kick(self, mess_data, profile):
def cmd_kick(self, client, mess_data):
"""kick a room member
@command (group): ROOM_NICK
@ -536,24 +546,24 @@ class XEP_0045(object):
options = mess_data["unparsed"].strip().split()
try:
nick = options[0]
assert(self.isNickInRoom(mess_data["to"], nick, profile))
assert(self.isNickInRoom(mess_data["to"], nick, client.profile))
except (IndexError, AssertionError):
feedback = _(u"You must provide a member's nick to kick.")
self.host.plugins[C.TEXT_CMDS].feedBack(feedback, mess_data, profile)
self.host.plugins[C.TEXT_CMDS].feedBack(client, feedback, mess_data)
return False
d = self.kick(nick, mess_data["to"], {} if len(options) == 1 else {'reason': options[1]}, profile)
d = self.kick(nick, mess_data["to"], {} if len(options) == 1 else {'reason': options[1]}, client.profile)
def cb(dummy):
feedback_msg = _(u'You have kicked {}').format(nick)
if len(options) > 1:
feedback_msg += _(u' for the following reason: {}').format(options[1])
self.host.plugins[C.TEXT_CMDS].feedBack(feedback_msg, mess_data, profile)
self.host.plugins[C.TEXT_CMDS].feedBack(client, feedback_msg, mess_data)
return True
d.addCallback(cb)
return d
def cmd_ban(self, mess_data, profile):
def cmd_ban(self, client, mess_data):
"""ban an entity from the room
@command (group): (JID) [reason]
@ -568,21 +578,21 @@ class XEP_0045(object):
assert(entity_jid.host)
except (RuntimeError, jid.InvalidFormat, AttributeError, IndexError, AssertionError):
feedback = _(u"You must provide a valid JID to ban, like in '/ban contact@example.net'")
self.host.plugins[C.TEXT_CMDS].feedBack(feedback, mess_data, profile)
self.host.plugins[C.TEXT_CMDS].feedBack(client, feedback, mess_data)
return False
d = self.ban(entity_jid, mess_data["to"], {} if len(options) == 1 else {'reason': options[1]}, profile)
d = self.ban(entity_jid, mess_data["to"], {} if len(options) == 1 else {'reason': options[1]}, client.profile)
def cb(dummy):
feedback_msg = _(u'You have banned {}').format(entity_jid)
if len(options) > 1:
feedback_msg += _(u' for the following reason: {}').format(options[1])
self.host.plugins[C.TEXT_CMDS].feedBack(feedback_msg, mess_data, profile)
self.host.plugins[C.TEXT_CMDS].feedBack(client, feedback_msg, mess_data)
return True
d.addCallback(cb)
return d
def cmd_affiliate(self, mess_data, profile):
def cmd_affiliate(self, client, mess_data):
"""affiliate an entity to the room
@command (group): (JID) [owner|admin|member|none|outcast]
@ -601,25 +611,25 @@ class XEP_0045(object):
assert(entity_jid.host)
except (RuntimeError, jid.InvalidFormat, AttributeError, IndexError, AssertionError):
feedback = _(u"You must provide a valid JID to affiliate, like in '/affiliate contact@example.net member'")
self.host.plugins[C.TEXT_CMDS].feedBack(feedback, mess_data, profile)
self.host.plugins[C.TEXT_CMDS].feedBack(client, feedback, mess_data)
return False
affiliation = options[1] if len(options) > 1 else 'none'
if affiliation not in AFFILIATIONS:
feedback = _(u"You must provide a valid affiliation: %s") % ' '.join(AFFILIATIONS)
self.host.plugins[C.TEXT_CMDS].feedBack(feedback, mess_data, profile)
self.host.plugins[C.TEXT_CMDS].feedBack(client, feedback, mess_data)
return False
d = self.affiliate(entity_jid, mess_data["to"], {'affiliation': affiliation}, profile)
d = self.affiliate(entity_jid, mess_data["to"], {'affiliation': affiliation}, client.profile)
def cb(dummy):
feedback_msg = _(u'New affiliation for %(entity)s: %(affiliation)s').format(entity=entity_jid, affiliation=affiliation)
self.host.plugins[C.TEXT_CMDS].feedBack(feedback_msg, mess_data, profile)
self.host.plugins[C.TEXT_CMDS].feedBack(client, feedback_msg, mess_data)
return True
d.addCallback(cb)
return d
def cmd_title(self, mess_data, profile):
def cmd_title(self, client, mess_data):
"""change room's subject
@command (group): title
@ -629,28 +639,28 @@ class XEP_0045(object):
if subject:
room = mess_data["to"]
self.subject(room, subject, profile)
self.subject(room, subject, client.profile)
return False
def cmd_topic(self, mess_data, profile):
def cmd_topic(self, client, mess_data):
"""just a synonym of /title
@command (group): title
- title: new room subject
"""
return self.cmd_title(mess_data, profile)
return self.cmd_title(client, mess_data)
def _whois(self, whois_msg, mess_data, target_jid, profile):
def _whois(self, client, whois_msg, mess_data, target_jid):
""" Add MUC user information to whois """
if mess_data['type'] != "groupchat":
return
if target_jid.userhostJID() not in self.clients[profile].joined_rooms:
if target_jid.userhostJID() not in self.clients[client.profile].joined_rooms:
log.warning(_("This room has not been joined"))
return
if not target_jid.resource:
return
user = self.clients[profile].joined_rooms[target_jid.userhostJID()].getUser(target_jid.resource)
user = self.clients[client.profile].joined_rooms[target_jid.userhostJID()].getUser(target_jid.resource)
whois_msg.append(_("Nickname: %s") % user.nick)
if user.entity:
whois_msg.append(_("Entity: %s") % user.entity)
@ -681,16 +691,40 @@ class SatMUCClient (muc.MUCClient):
self.host = plugin_parent.host
muc.MUCClient.__init__(self)
self.rec_subjects = {}
self.__changing_nicks = set() # used to keep trace of who is changing nick,
# and to discard userJoinedRoom signal in this case
self._changing_nicks = set() # used to keep trace of who is changing nick,
# and to discard userJoinedRoom signal in this case
print "init SatMUCClient OK"
@property
def joined_rooms(self):
return self._rooms
def subject(self, room, subject):
return muc.MUCClientProtocol.subject(self, room, subject)
def _addRoom(self, room):
super(SatMUCClient, self)._addRoom(room)
room._roster_ok = False
room._room_ok = None # False when roster, history and subject are available
# True when new messages are saved to database
room._history_d = defer.Deferred() # use to send bridge signal once backlog are written in history
room._history_d.callback(None)
room._cache = []
def _gotLastDbHistory(self, mess_data_list, room_jid, nick, password):
if mess_data_list:
timestamp = mess_data_list[0][1]
# we use seconds since last message to get backlog without duplicates
# and we remove 1 second to avoid getting the last message again
seconds = int(time.time() - timestamp) - 1
else:
seconds = None
d = super(SatMUCClient, self).join(room_jid, nick, muc.HistoryOptions(seconds=seconds), password)
return d
def join(self, room_jid, nick, password=None):
d = self.host.memory.historyGet(self.parent.jid.userhostJID(), room_jid, 1, True, profile=self.parent.profile)
d.addCallback(self._gotLastDbHistory, room_jid, nick, password)
return d
## presence/roster ##
def availableReceived(self, presence):
"""
@ -698,7 +732,6 @@ class SatMUCClient (muc.MUCClient):
"""
# XXX: we override MUCClient.availableReceived to fix bugs
# (affiliation and role are not set)
# FIXME: propose a patch upstream
room, user = self._getRoomUser(presence)
@ -720,9 +753,9 @@ class SatMUCClient (muc.MUCClient):
else:
room.addUser(user)
self.userJoinedRoom(room, user)
def unavailableReceived(self, presence):
# XXX: we override this method to manage nickname change
# TODO: feed this back to Wokkel
"""
Unavailable presence was received.
@ -737,22 +770,49 @@ class SatMUCClient (muc.MUCClient):
room.removeUser(user)
if muc.STATUS_CODE.NEW_NICK in presence.mucStatuses:
self.__changing_nicks.add(presence.nick)
self._changing_nicks.add(presence.nick)
self.userChangedNick(room, user, presence.nick)
else:
self.__changing_nicks.discard(presence.nick)
self._changing_nicks.discard(presence.nick)
self.userLeftRoom(room, user)
def userJoinedRoom(self, room, user):
self.host.memory.updateEntityData(room.roomJID, "type", "chatroom", profile_key=self.parent.profile)
if user.nick in self.__changing_nicks:
self.__changing_nicks.remove(user.nick)
else:
log.debug(_(u"user %(nick)s has joined room (%(room_id)s)") % {'nick': user.nick, 'room_id': room.occupantJID.userhost()})
if not self.host.trigger.point("MUC user joined", room, user, self.parent.profile):
return
user_data = {'entity': user.entity.full() if user.entity else '', 'affiliation': user.affiliation, 'role': user.role}
self.host.bridge.roomUserJoined(room.roomJID.userhost(), user.nick, user_data, self.parent.profile)
if user.nick == room.nick:
# we have received our own nick, this mean that the full room roster was received
room._roster_ok = True
log.debug(u"room {room} joined with nick {nick}".format(room=room.occupantJID.userhost(), nick=user.nick))
# We set type so we don't have use a deferred with disco to check entity type
self.host.memory.updateEntityData(room.roomJID, C.ENTITY_TYPE, ENTITY_TYPE_MUC, profile_key=self.parent.profile)
elif room._roster_ok:
try:
self._changing_nicks.remove(user.nick)
except KeyError:
# this is a new user
log.debug(_(u"user {nick} has joined room {room_id}").format(nick=user.nick, room_id=room.occupantJID.userhost()))
if not self.host.trigger.point("MUC user joined", room, user, self.parent.profile):
return
extra = {'info_type': ROOM_USER_JOINED,
'user_affiliation': user.affiliation,
'user_role': user.role,
'user_nick': user.nick
}
if user.entity is not None:
extra['user_entity'] = user.entity.full()
mess_data = { # dict is similar to the one used in client.onMessage
"from": room.roomJID,
"to": self.parent.jid,
"uid": unicode(uuid.uuid4()),
"message": {'': D_(u"=> {} has joined the room").format(user.nick)},
"subject": {},
"type": C.MESS_TYPE_INFO,
"extra": extra,
"timestamp": time.time(),
}
self.host.messageAddToHistory(mess_data, self.parent)
self.host.messageSendToBridge(mess_data, self.parent)
def userLeftRoom(self, room, user):
if not self.host.trigger.point("MUC user left", room, user, self.parent.profile):
@ -760,14 +820,31 @@ class SatMUCClient (muc.MUCClient):
if user.nick == room.nick:
# we left the room
room_jid_s = room.roomJID.userhost()
log.info(_(u"Room [%(room)s] left (%(profile)s))") % {"room": room_jid_s,
"profile": self.parent.profile})
log.info(_(u"Room ({room}) left ({profile})").format(
room = room_jid_s, profile = self.parent.profile))
self.host.memory.delEntityCache(room.roomJID, profile_key=self.parent.profile)
self.host.bridge.roomLeft(room.roomJID.userhost(), self.parent.profile)
else:
log.debug(_(u"user %(nick)s left room (%(room_id)s)") % {'nick': user.nick, 'room_id': room.occupantJID.userhost()})
user_data = {'entity': user.entity.full() if user.entity else '', 'affiliation': user.affiliation, 'role': user.role}
self.host.bridge.roomUserLeft(room.roomJID.userhost(), user.nick, user_data, self.parent.profile)
log.debug(_(u"user {nick} left room {room_id}").format(nick=user.nick, room_id=room.occupantJID.userhost()))
extra = {'info_type': ROOM_USER_LEFT,
'user_affiliation': user.affiliation,
'user_role': user.role,
'user_nick': user.nick
}
if user.entity is not None:
extra['user_entity'] = user.entity.full()
mess_data = { # dict is similar to the one used in client.onMessage
"from": room.roomJID,
"to": self.parent.jid,
"uid": unicode(uuid.uuid4()),
"message": {'': D_(u"<= {} has left the room").format(user.nick)},
"subject": {},
"type": C.MESS_TYPE_INFO,
"extra": extra,
"timestamp": time.time(),
}
self.host.messageAddToHistory(mess_data, self.parent)
self.host.messageSendToBridge(mess_data, self.parent)
def userChangedNick(self, room, user, new_nick):
self.host.bridge.roomUserChangedNick(room.roomJID.userhost(), user.nick, new_nick, self.parent.profile)
@ -775,19 +852,111 @@ class SatMUCClient (muc.MUCClient):
def userUpdatedStatus(self, room, user, show, status):
self.host.bridge.presenceUpdate(room.roomJID.userhost() + '/' + user.nick, show or '', 0, {C.PRESENCE_STATUSES_DEFAULT: status or ''}, self.parent.profile)
## messages ##
def receivedGroupChat(self, room, user, body):
log.debug(u'receivedGroupChat: room=%s user=%s body=%s' % (room.roomJID.full(), user, body))
def _addToHistory(self, dummy, user, message):
# we check if message is not in history
# and raise ConflictError else
stamp = message.delay.stamp.astimezone(tzutc()).timetuple()
timestamp = float(calendar.timegm(stamp))
data = { # dict is similar to the one used in client.onMessage
"from": message.sender,
"to": message.recipient,
"uid": unicode(uuid.uuid4()),
"type": C.MESS_TYPE_GROUPCHAT,
"extra": {},
"timestamp": timestamp,
"received_timestamp": unicode(time.time()),
}
# FIXME: message and subject don't handle xml:lang
data['message'] = {'': message.body} if message.body is not None else {}
data['subject'] = {'': message.subject} if message.subject is not None else {}
if data['message'] or data['subject']:
return self.host.memory.addToHistory(self.parent, data)
else:
return defer.succeed(None)
def _addToHistoryEb(self, failure):
failure.trap(exceptions.CancelError)
def receivedHistory(self, room, user, message):
# http://xmpp.org/extensions/xep-0045.html#enter-history
# log.debug(u'receivedHistory: room=%s user=%s body=%s' % (room.roomJID.full(), user, message))
pass
"""Called when history (backlog) message are received
we check if message is not already in our history
and add it if needed
@param room(muc.Room): room instance
@param user(muc.User, None): the user that sent the message
None if the message come from the room
@param message(muc.GroupChat): the parsed message
"""
room._history_d.addCallback(self._addToHistory, user, message)
room._history_d.addErrback(self._addToHistoryEb)
## subject ##
def groupChatReceived(self, message):
"""
A group chat message has been received from a MUC room.
There are a few event methods that may get called here.
L{receivedGroupChat}, L{receivedSubject} or L{receivedHistory}.
"""
# We override this method to fix subject handling
# FIXME: remove this merge fixed upstream
room, user = self._getRoomUser(message)
if room is None:
return
if message.subject is not None:
self.receivedSubject(room, user, message.subject)
elif message.delay is None:
self.receivedGroupChat(room, user, message)
else:
self.receivedHistory(room, user, message)
def subject(self, room, subject):
return muc.MUCClientProtocol.subject(self, room, subject)
def _historyCb(self, dummy, room):
self.host.bridge.roomJoined(
room.roomJID.userhost(),
XEP_0045._getOccupants(room),
room.nick,
room.subject,
self.parent.profile)
del room._history_d
cache = room._cache
del room._cache
room._room_ok = True
for elem in cache:
self.parent.xmlstream.dispatch(elem)
def _historyEb(self, failure_, room):
log.error(u"Error while managing history: {}".format(failure_))
self._historyCb(None, room)
def receivedSubject(self, room, user, subject):
# http://xmpp.org/extensions/xep-0045.html#enter-subject
log.debug(_(u"New subject for room (%(room_id)s): %(subject)s") % {'room_id': room.roomJID.full(), 'subject': subject})
# when subject is received, we know that we have whole roster and history
# cf. http://xmpp.org/extensions/xep-0045.html#enter-subject
room.subject = subject # FIXME: subject doesn't handle xml:lang
self.rec_subjects[room.roomJID.userhost()] = (room.roomJID.userhost(), subject)
self.host.bridge.roomNewSubject(room.roomJID.userhost(), subject, self.parent.profile)
if room._room_ok is None:
# this is the first subject we receive
# that mean that we have received everything we need
room._room_ok = False
room._history_d.addCallbacks(self._historyCb, self._historyEb, [room], errbackArgs=[room])
else:
# the subject has been changed
log.debug(_(u"New subject for room ({room_id}): {subject}").format(room_id = room.roomJID.full(), subject = subject))
self.host.bridge.roomNewSubject(room.roomJID.userhost(), subject, self.parent.profile)
## disco ##
def getDiscoInfo(self, requestor, target, nodeIdentifier=''):
return [disco.DiscoFeature(NS_MUC)]

View file

@ -88,7 +88,7 @@ class XEP_0048(object):
for (room_jid, data) in bookmarks[XEP_0048.MUC_TYPE].items():
if data.get('autojoin', 'false') == 'true':
nick = data.get('nick', client.jid.user)
self.host.plugins['XEP-0045'].join(room_jid, nick, {}, profile_key=client.profile)
self.host.plugins['XEP-0045'].join(client, room_jid, nick, {})
@defer.inlineCallbacks
def _getServerBookmarks(self, storage_type, profile):
@ -219,9 +219,10 @@ class XEP_0048(object):
log.warning(_("No room jid selected"))
return {}
d = self.host.plugins['XEP-0045'].join(room_jid, nick, {}, profile_key=profile)
client = self.host.getClient(profile)
d = self.host.plugins['XEP-0045'].join(client, room_jid, nick, {})
def join_eb(failure):
log.warning(u"Error while trying to join room: %s" % failure)
log.warning(u"Error while trying to join room: {}".format(failure))
# FIXME: failure are badly managed in plugin XEP-0045. Plugin XEP-0045 need to be fixed before managing errors correctly here
return {}
d.addCallbacks(lambda dummy: {}, join_eb)
@ -417,33 +418,32 @@ class XEP_0048(object):
location = jid.JID(location)
return self.addBookmark(type_, location, data, storage_type, profile_key)
def cmd_bookmark(self, mess_data, profile):
def cmd_bookmark(self, client, mess_data):
"""(Un)bookmark a MUC room
@command (group): [autojoin | remove]
- autojoin: join room automatically on connection
- remove: remove bookmark(s) for this room
"""
client = self.host.getClient(profile)
txt_cmd = self.host.plugins[C.TEXT_CMDS]
options = mess_data["unparsed"].strip().split()
if options and options[0] not in ('autojoin', 'remove'):
txt_cmd.feedBack(_("Bad arguments"), mess_data, profile)
txt_cmd.feedBack(client, _("Bad arguments"), mess_data)
return False
room_jid = mess_data["to"].userhostJID()
if "remove" in options:
self.removeBookmark(XEP_0048.MUC_TYPE, room_jid, profile_key = profile)
txt_cmd.feedBack(_("All [%s] bookmarks are being removed") % room_jid.full(), mess_data, profile)
self.removeBookmark(XEP_0048.MUC_TYPE, room_jid, profile_key = client.profile)
txt_cmd.feedBack(client, _("All [%s] bookmarks are being removed") % room_jid.full(), mess_data)
return False
data = { "name": room_jid.user,
"nick": client.jid.user,
"autojoin": "true" if "autojoin" in options else "false",
}
self.addBookmark(XEP_0048.MUC_TYPE, room_jid, data, profile_key=profile)
txt_cmd.feedBack(_("Bookmark added"), mess_data, profile)
self.addBookmark(XEP_0048.MUC_TYPE, room_jid, data, profile_key=client.profile)
txt_cmd.feedBack(client, _("Bookmark added"), mess_data)
return False

View file

@ -18,6 +18,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from sat.core.i18n import _
from sat.core.constants import Const as C
from sat.core import exceptions
from sat.core.log import getLogger
log = getLogger(__name__)
@ -91,7 +92,7 @@ class XEP_0071(object):
@return: the data with the extra parameter updated
"""
# TODO: check if text only body is empty, then try to convert XHTML-IM to pure text and show a warning message
def converted(xhtml):
def converted(xhtml, lang):
if lang:
data['extra']['xhtml_{}'.format(lang)] = xhtml
else:
@ -99,7 +100,7 @@ class XEP_0071(object):
defers = []
for body_elt in body_elts:
lang = body_elt.getAttribute('xml:lang', '')
lang = body_elt.getAttribute((C.NS_XML, 'lang'), '')
d = self._s.convert(body_elt.toXml(), self.SYNTAX_XHTML_IM, safe=True)
d.addCallback(converted, lang)
defers.append(d)
@ -121,7 +122,7 @@ class XEP_0071(object):
def syntax_converted(xhtml_im, lang):
body_elt = html_elt.addElement((NS_XHTML, 'body'))
if lang:
body_elt['xml:lang'] = lang
body_elt[(C.NS_XML, 'lang')] = lang
data['extra']['xhtml_{}'.format(lang)] = xhtml_im
else:
data['extra']['xhtml'] = xhtml_im

View file

@ -99,7 +99,7 @@ class XEP_0092(object):
return tuple(ret)
def _whois(self, whois_msg, mess_data, target_jid, profile):
def _whois(self, client, whois_msg, mess_data, target_jid):
""" Add software/OS information to whois """
def versionCb(version_data):
name, version, os = version_data
@ -116,7 +116,7 @@ class XEP_0092(object):
else:
whois_msg.append(_("Client software version request timeout"))
d = self.getVersion(target_jid, profile)
d = self.getVersion(target_jid, client.profile)
d.addCallbacks(versionCb, versionEb)
return d

View file

@ -125,13 +125,9 @@ class XEP_0249(object):
@param room (jid.JID): JID of the room
"""
profile = self.host.memory.getProfileName(profile_key)
if not profile:
log.error(_("Profile doesn't exists !"))
return
log.info(_(u'Invitation accepted for room %(room)s [%(profile)s]') % {'room': room_jid.userhost(), 'profile': profile})
_jid, xmlstream = self.host.getJidNStream(profile)
d = self.host.plugins["XEP-0045"].join(room_jid, _jid.user, {}, profile)
client = self.host.getClient(profile_key)
log.info(_(u'Invitation accepted for room %(room)s [%(profile)s]') % {'room': room_jid.userhost(), 'profile': client.profile})
d = self.host.plugins["XEP-0045"].join(client, room_jid, client.jid.user, {})
return d
def onInvitation(self, message, profile):
@ -165,23 +161,23 @@ class XEP_0249(object):
data = {"message": _("You have been invited by %(user)s to join the room %(room)s. Do you accept?") % {'user': from_jid_s, 'room': room_jid_s}, "title": _("MUC invitation")}
self.host.askConfirmation(room_jid_s, "YES/NO", data, accept_cb, profile)
def cmd_invite(self, mess_data, profile):
def cmd_invite(self, client, mess_data):
"""invite someone in the room
@command (group): JID
- JID: the JID of the person to invite
"""
contact_jid_s = mess_data["unparsed"].strip()
my_host = self.host.profiles[profile].jid.host
my_host = client.jid.host
try:
contact_jid = jid.JID(contact_jid_s)
except (RuntimeError, jid.InvalidFormat, AttributeError):
feedback = _(u"You must provide a valid JID to invite, like in '/invite contact@{host}'").format(host=my_host)
self.host.plugins[C.TEXT_CMDS].feedBack(feedback, mess_data, profile)
self.host.plugins[C.TEXT_CMDS].feedBack(client, feedback, mess_data)
return False
if not contact_jid.user:
contact_jid.user, contact_jid.host = contact_jid.host, my_host
self.invite(contact_jid, mess_data["to"], {}, profile)
self.invite(contact_jid, mess_data["to"], {}, client.profile)
return False