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:
parent
feadc8c89e
commit
a7de473107
10 changed files with 987 additions and 598 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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)]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue