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
23 changed files with 987 additions and 598 deletions
|
|
@ -554,7 +554,7 @@ class DBusBridgeFrontend(BridgeFrontend):
|
|||
kwargs['error_handler'] = error_handler
|
||||
return self.db_core_iface.loadParamsTemplate(filename, **kwargs)
|
||||
|
||||
def messageSend(self, to_jid, message, subject='', mess_type="auto", extra={}, profile_key="@NONE@", callback=None, errback=None):
|
||||
def messageSend(self, to_jid, message, subject={}, mess_type="auto", extra={}, profile_key="@NONE@", callback=None, errback=None):
|
||||
if callback is None:
|
||||
error_handler = None
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@
|
|||
from sat_frontends.jp import base
|
||||
import sys
|
||||
from sat.core.i18n import _
|
||||
from sat.core.constants import Const as C
|
||||
from sat.tools.utils import clean_ustr
|
||||
|
||||
__commands__ = ["Message"]
|
||||
|
|
@ -31,9 +32,13 @@ class Send(base.CommandBase):
|
|||
super(Send, self).__init__(host, 'send', help=_('send a message to a contact'))
|
||||
|
||||
def add_parser_options(self):
|
||||
self.parser.add_argument("-l", "--lang", type=str, default='', help=_("language of the message"))
|
||||
self.parser.add_argument("-s", "--separate", action="store_true", help=_("separate xmpp messages: send one message per line instead of one message alone."))
|
||||
self.parser.add_argument("-n", "--new-line", action="store_true", help=_("add a new line at the beginning of the input (usefull for ascii art ;))"))
|
||||
self.parser.add_argument("jid", type=str, help=_("the destination jid"))
|
||||
self.parser.add_argument("-S", "--subject", type=base.unicode_decoder, help=_("subject of the message"))
|
||||
self.parser.add_argument("-L", "--subject_lang", type=str, default='', help=_("language of subject"))
|
||||
self.parser.add_argument("-t", "--type", choices=C.MESS_TYPE_STANDARD + (C.MESS_TYPE_AUTO,), default=C.MESS_TYPE_AUTO, help=_("type of the message"))
|
||||
self.parser.add_argument("jid", type=base.unicode_decoder, help=_("the destination jid"))
|
||||
|
||||
def start(self):
|
||||
jids = self.host.check_jids([self.args.jid])
|
||||
|
|
@ -46,21 +51,25 @@ class Send(base.CommandBase):
|
|||
@param dest_jid: destination jid
|
||||
"""
|
||||
header = "\n" if self.args.new_line else ""
|
||||
if self.args.subject is None:
|
||||
subject = {}
|
||||
else:
|
||||
subject = {self.args.subject_lang: self.args.subject}
|
||||
|
||||
if self.args.separate: #we send stdin in several messages
|
||||
|
||||
if header:
|
||||
self.host.bridge.sendMessage(dest_jid, header, profile_key=self.profile, callback=lambda: None, errback=lambda ignore: ignore)
|
||||
self.host.bridge.messageSend(dest_jid, {self.args.lang: header}, subject, self.args.type, profile_key=self.profile, callback=lambda: None, errback=lambda ignore: ignore)
|
||||
|
||||
while (True):
|
||||
line = clean_ustr(sys.stdin.readline().decode('utf-8','ignore'))
|
||||
if not line:
|
||||
break
|
||||
self.host.bridge.sendMessage(dest_jid, line.replace("\n",""), profile_key=self.host.profile, callback=lambda: None, errback=lambda ignore: ignore)
|
||||
self.host.bridge.messageSend(dest_jid, {self.args.lang: line.replace("\n","")}, subject, self.args.type, profile_key=self.host.profile, callback=lambda: None, errback=lambda ignore: ignore)
|
||||
|
||||
else:
|
||||
msg = header + clean_ustr(u"".join([stream.decode('utf-8','ignore') for stream in sys.stdin.readlines()]))
|
||||
self.host.bridge.sendMessage(dest_jid, msg, profile_key=self.host.profile, callback=lambda: None, errback=lambda ignore: ignore)
|
||||
self.host.bridge.messageSend(dest_jid, {self.args.lang: msg}, subject, self.args.type, profile_key=self.host.profile, callback=lambda: None, errback=lambda ignore: ignore)
|
||||
|
||||
|
||||
class Message(base.CommandBase):
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ log = logging.getLogger(__name__)
|
|||
import urwid
|
||||
from urwid_satext import sat_widgets
|
||||
from sat_frontends.quick_frontend import quick_widgets
|
||||
from sat_frontends.quick_frontend.quick_chat import QuickChat
|
||||
from sat_frontends.quick_frontend import quick_chat
|
||||
from sat_frontends.quick_frontend import quick_games
|
||||
from sat_frontends.primitivus import game_tarot
|
||||
from sat_frontends.primitivus.constants import Const as C
|
||||
|
|
@ -31,19 +31,63 @@ from sat_frontends.primitivus.keys import action_key_map as a_key
|
|||
from sat_frontends.primitivus.widget import PrimitivusWidget
|
||||
import time
|
||||
from sat_frontends.tools import jid
|
||||
from functools import total_ordering
|
||||
import bisect
|
||||
|
||||
|
||||
class ChatText(urwid.FlowWidget):
|
||||
"""Manage the printing of chat message"""
|
||||
class MessageWidget(urwid.WidgetWrap):
|
||||
|
||||
def __init__(self, parent, timestamp, nick, my_mess, message, align='left', is_info=False):
|
||||
self.parent = parent
|
||||
self.timestamp = time.localtime(timestamp)
|
||||
self.nick = nick
|
||||
self.my_mess = my_mess
|
||||
self.message = unicode(message)
|
||||
self.align = align
|
||||
self.is_info = is_info
|
||||
def __init__(self, mess_data):
|
||||
"""
|
||||
@param mess_data(quick_chat.Message, None): message data
|
||||
None: used only for non text widgets (e.g.: focus separator)
|
||||
"""
|
||||
self.mess_data = mess_data
|
||||
mess_data.widgets.add(self)
|
||||
self.timestamp = time.localtime(mess_data.timestamp)
|
||||
super(MessageWidget, self).__init__(urwid.Text(self.markup))
|
||||
|
||||
@property
|
||||
def markup(self):
|
||||
return self._generateInfoMarkup() if self.mess_data.type == C.MESS_TYPE_INFO else self._generateMarkup()
|
||||
|
||||
@property
|
||||
def info_type(self):
|
||||
return self.mess_data.info_type
|
||||
|
||||
@property
|
||||
def parent(self):
|
||||
return self.mess_data.parent
|
||||
|
||||
@property
|
||||
def message(self):
|
||||
"""Return currently displayed message"""
|
||||
message = self.mess_data.message
|
||||
if self.parent.lang in message:
|
||||
self.selected_lang = self.parent.lang
|
||||
return message[self.parent.lang]
|
||||
try:
|
||||
self.selected_lang = ''
|
||||
return message['']
|
||||
except KeyError:
|
||||
try:
|
||||
lang, mess = message.iteritems().next()
|
||||
self.selected_lang = lang
|
||||
return mess
|
||||
except StopIteration:
|
||||
log.error(u"Can't find message for uid {}".format(self.mess_data.uid))
|
||||
|
||||
@message.setter
|
||||
def message(self, value):
|
||||
self.mess_data.message = {'':value}
|
||||
self._w.set_text(self.markup)
|
||||
|
||||
@property
|
||||
def type(self):
|
||||
try:
|
||||
return self.mess_data.type
|
||||
except AttributeError:
|
||||
return C.MESS_TYPE_INFO
|
||||
|
||||
def selectable(self):
|
||||
return True
|
||||
|
|
@ -51,42 +95,103 @@ class ChatText(urwid.FlowWidget):
|
|||
def keypress(self, size, key):
|
||||
return key
|
||||
|
||||
def rows(self, size, focus=False):
|
||||
return self.display_widget(size, focus).rows(size, focus)
|
||||
def get_cursor_coords(self, size):
|
||||
return 0, 0
|
||||
|
||||
def render(self, size, focus=False):
|
||||
canvas = urwid.CompositeCanvas(self.display_widget(size, focus).render(size, focus))
|
||||
# Text widget doesn't render cursor, but we want one
|
||||
# so we add it here
|
||||
canvas = urwid.CompositeCanvas(self._w.render(size, focus))
|
||||
if focus:
|
||||
canvas.set_cursor(self.get_cursor_coords(size))
|
||||
return canvas
|
||||
|
||||
def _generateInfoMarkup(self):
|
||||
return ('info_msg', self.message)
|
||||
|
||||
def _generateMarkup(self):
|
||||
"""Generate text markup according to message data and Widget options"""
|
||||
markup = []
|
||||
d = self.mess_data
|
||||
|
||||
# timestamp
|
||||
if self.parent.show_timestamp:
|
||||
# if the message was sent before today, we print the full date
|
||||
time_format = "%c" if self.timestamp < self.parent.day_change else "%H:%M"
|
||||
markup.append(('date', "[{}]".format(time.strftime(time_format, self.timestamp).decode('utf-8'))))
|
||||
|
||||
# nickname
|
||||
if self.parent.show_short_nick:
|
||||
markup.append(('my_nick' if d.own_mess else 'other_nick', "**" if d.own_mess else "*"))
|
||||
else:
|
||||
markup.append(('my_nick' if d.own_mess else 'other_nick', u"[{}] ".format(d.nick or '')))
|
||||
|
||||
msg = self.message # needed to generate self.selected_lang
|
||||
|
||||
if self.selected_lang:
|
||||
markup.append(("msg_lang", u"[{}] ".format(self.selected_lang)))
|
||||
|
||||
# message body
|
||||
markup.append(msg)
|
||||
|
||||
return markup
|
||||
|
||||
@total_ordering
|
||||
class OccupantWidget(urwid.WidgetWrap):
|
||||
|
||||
def __init__(self, occupant_data):
|
||||
self.occupant_data = occupant_data
|
||||
occupant_data.widgets.add(self)
|
||||
markup = self._generateMarkup()
|
||||
super(OccupantWidget, self).__init__(urwid.Text(markup))
|
||||
|
||||
def __eq__(self, other):
|
||||
return self.occupant_data.nick == other.occupant_data.nick
|
||||
|
||||
def __lt__(self, other):
|
||||
return self.occupant_data.nick.lower() < other.occupant_data.nick.lower()
|
||||
|
||||
@property
|
||||
def parent(self):
|
||||
return self.mess_data.parent
|
||||
|
||||
def selectable(self):
|
||||
return True
|
||||
|
||||
def keypress(self, size, key):
|
||||
return key
|
||||
|
||||
def get_cursor_coords(self, size):
|
||||
return 0, 0
|
||||
|
||||
def display_widget(self, size, focus):
|
||||
render_txt = []
|
||||
if not self.is_info:
|
||||
if self.parent.show_timestamp:
|
||||
time_format = "%c" if self.timestamp < self.parent.day_change else "%H:%M" # if the message was sent before today, we print the full date
|
||||
render_txt.append(('date', "[%s]" % time.strftime(time_format, self.timestamp).decode('utf-8')))
|
||||
if self.parent.show_short_nick:
|
||||
render_txt.append(('my_nick' if self.my_mess else 'other_nick', "**" if self.my_mess else "*"))
|
||||
else:
|
||||
render_txt.append(('my_nick' if self.my_mess else 'other_nick', "[%s] " % (self.nick or '')))
|
||||
render_txt.append(self.message)
|
||||
txt_widget = urwid.Text(render_txt, align=self.align)
|
||||
if self.is_info:
|
||||
return urwid.AttrMap(txt_widget, 'info_msg')
|
||||
return txt_widget
|
||||
def render(self, size, focus=False):
|
||||
# Text widget doesn't render cursor, but we want one
|
||||
# so we add it here
|
||||
canvas = urwid.CompositeCanvas(self._w.render(size, focus))
|
||||
if focus:
|
||||
canvas.set_cursor(self.get_cursor_coords(size))
|
||||
return canvas
|
||||
|
||||
def _generateMarkup(self):
|
||||
# TODO: role and affiliation are shown in a Q&D way
|
||||
# should be more intuitive and themable
|
||||
o = self.occupant_data
|
||||
markup = []
|
||||
markup.append(('info_msg', '{}{} '.format(
|
||||
o.role[0].upper(),
|
||||
o.affiliation[0].upper(),
|
||||
)))
|
||||
markup.append(o.nick)
|
||||
return markup
|
||||
|
||||
|
||||
class Chat(PrimitivusWidget, QuickChat):
|
||||
class Chat(PrimitivusWidget, quick_chat.QuickChat):
|
||||
|
||||
def __init__(self, host, target, type_=C.CHAT_ONE2ONE, profiles=None):
|
||||
QuickChat.__init__(self, host, target, type_, profiles=profiles)
|
||||
self.content = urwid.SimpleListWalker([])
|
||||
self.text_list = urwid.ListBox(self.content)
|
||||
self.chat_widget = urwid.Frame(self.text_list)
|
||||
def __init__(self, host, target, type_=C.CHAT_ONE2ONE, occupants=None, subject=None, profiles=None):
|
||||
quick_chat.QuickChat.__init__(self, host, target, type_, occupants, subject, profiles=profiles)
|
||||
self.mess_walker = urwid.SimpleListWalker([])
|
||||
self.mess_widgets = urwid.ListBox(self.mess_walker)
|
||||
self.chat_widget = urwid.Frame(self.mess_widgets)
|
||||
self.chat_colums = urwid.Columns([('weight', 8, self.chat_widget)])
|
||||
self.pile = urwid.Pile([self.chat_colums])
|
||||
PrimitivusWidget.__init__(self, self.pile, self.target)
|
||||
|
|
@ -94,16 +199,28 @@ class Chat(PrimitivusWidget, QuickChat):
|
|||
# we must adapt the behaviour with the type
|
||||
if type_ == C.CHAT_GROUP:
|
||||
if len(self.chat_colums.contents) == 1:
|
||||
self.occupants_list = sat_widgets.GenericList([], option_type=sat_widgets.ClickableText, on_click=self._occupantsClicked)
|
||||
self.occupants_panel = sat_widgets.VerticalSeparator(self.occupants_list)
|
||||
self.occupants_walker = urwid.SimpleListWalker([])
|
||||
# TODO: put a real ContactPanel class here, based on FocusWidget ?
|
||||
self.occupants_widgets = urwid.ListBox(self.occupants_walker)
|
||||
# FIXME
|
||||
# , option_type=sat_widgets.ClickableText, on_click=self._occupantsClicked)
|
||||
self.occupants_panel = sat_widgets.VerticalSeparator(self.occupants_widgets)
|
||||
self._appendOccupantsPanel()
|
||||
occupants_list = sorted(self.occupants.keys(), key=lambda o:o.lower())
|
||||
for occupant in occupants_list:
|
||||
occupant_data = self.occupants[occupant]
|
||||
self.occupants_walker.append(OccupantWidget(occupant_data))
|
||||
|
||||
self.host.addListener('presence', self.presenceListener, [profiles])
|
||||
|
||||
# focus marker is a separator indicated last visible message before focus was lost
|
||||
self.focus_marker = None # link to current marker
|
||||
self.focus_marker_set = None # True if a new marker has been inserted
|
||||
self.day_change = time.strptime(time.strftime("%a %b %d 00:00:00 %Y")) # struct_time of day changing time
|
||||
self.show_timestamp = True
|
||||
self.show_short_nick = False
|
||||
self.show_title = 1 # 0: clip title; 1: full title; 2: no title
|
||||
self.subject = None
|
||||
self.postInit()
|
||||
|
||||
def keypress(self, size, key):
|
||||
if key == a_key['OCCUPANTS_HIDE']: # user wants to (un)hide the occupants panel
|
||||
|
|
@ -115,11 +232,11 @@ class Chat(PrimitivusWidget, QuickChat):
|
|||
self._appendOccupantsPanel()
|
||||
elif key == a_key['TIMESTAMP_HIDE']: # user wants to (un)hide timestamp
|
||||
self.show_timestamp = not self.show_timestamp
|
||||
for wid in self.content:
|
||||
for wid in self.mess_walker:
|
||||
wid._invalidate()
|
||||
elif key == a_key['SHORT_NICKNAME']: # user wants to (not) use short nick
|
||||
self.show_short_nick = not self.show_short_nick
|
||||
for wid in self.content:
|
||||
for wid in self.mess_walker:
|
||||
wid._invalidate()
|
||||
elif key == a_key['SUBJECT_SWITCH']: # user wants to (un)hide group's subject or change its apperance
|
||||
if self.subject:
|
||||
|
|
@ -160,76 +277,87 @@ class Chat(PrimitivusWidget, QuickChat):
|
|||
@param statuses: dict of statuses
|
||||
@param profile: %(doc_profile)s
|
||||
"""
|
||||
assert self.type == C.CHAT_GROUP
|
||||
if entity.bare != self.target:
|
||||
return
|
||||
self.update(entity)
|
||||
# FIXME: disable for refactoring, need to be checked and re-enabled
|
||||
return
|
||||
# assert self.type == C.CHAT_GROUP
|
||||
# if entity.bare != self.target:
|
||||
# return
|
||||
# self.update(entity)
|
||||
|
||||
def update(self, entity=None):
|
||||
"""Update one or all entities.
|
||||
def createMessage(self, message):
|
||||
self.appendMessage(message)
|
||||
|
||||
@param entity (jid.JID): entity to update
|
||||
def _user_moved(self, message):
|
||||
"""return true if message is a user left/joined message
|
||||
|
||||
@param message(quick_chat.Message): message to add
|
||||
"""
|
||||
contact_list = self.host.contact_lists[self.profile]
|
||||
if message.type != C.MESS_TYPE_INFO:
|
||||
return False
|
||||
try:
|
||||
info_type = message.extra['info_type']
|
||||
except KeyError:
|
||||
return False
|
||||
else:
|
||||
return info_type in quick_chat.ROOM_USER_MOVED
|
||||
|
||||
if self.type == C.CHAT_ONE2ONE: # only update the chat title
|
||||
states = self.getEntityStates(self.target)
|
||||
self.title_dynamic = ' '.join([u'({})'.format(state) for state in states.values()])
|
||||
self.host.redraw()
|
||||
return
|
||||
def appendMessage(self, message):
|
||||
"""Create a MessageWidget and append it
|
||||
|
||||
nicks = list(self.occupants)
|
||||
if entity is None: # rebuild all the occupants list
|
||||
values = []
|
||||
nicks.sort()
|
||||
for nick in nicks:
|
||||
values.append(self._buildOccupantMarkup(jid.newResource(self.target, nick)))
|
||||
self.occupants_list.changeValues(values)
|
||||
else: # add, remove or update only one occupant
|
||||
nick = entity.resource
|
||||
show = contact_list.getCache(entity, C.PRESENCE_SHOW)
|
||||
if show == C.PRESENCE_UNAVAILABLE or show is None:
|
||||
try:
|
||||
self.occupants_list.deleteValue(nick)
|
||||
except ValueError:
|
||||
pass
|
||||
else:
|
||||
values = self.occupants_list.getAllValues()
|
||||
markup = self._buildOccupantMarkup(entity)
|
||||
if not values: # room has just been created
|
||||
values = [markup]
|
||||
else: # add or update the occupant, keep the list sorted
|
||||
index = 0
|
||||
for entry in values:
|
||||
order = cmp(entry.value if hasattr(entry, 'value') else entry, nick)
|
||||
if order < 0:
|
||||
index += 1
|
||||
continue
|
||||
if order > 0: # insert the occupant
|
||||
values.insert(index, markup)
|
||||
else: # update an existing occupant
|
||||
values[index] = markup
|
||||
break
|
||||
if index == len(values): # add to the end of the list
|
||||
values.append(markup)
|
||||
self.occupants_list.changeValues(values)
|
||||
self.host.redraw()
|
||||
|
||||
def _buildOccupantMarkup(self, entity):
|
||||
"""Return the option attributes for a MUC occupant.
|
||||
|
||||
@param nick (unicode): occupant nickname
|
||||
Can merge messages together is desirable (e.g.: multiple joined/leave)
|
||||
@param message(quick_chat.Message): message to add
|
||||
"""
|
||||
# TODO: for now it's not a markup but a simple text, the problem is that ListOption is unicode and not urwid.Text
|
||||
contact_list = self.host.contact_lists[self.profile]
|
||||
show = contact_list.getCache(entity, C.PRESENCE_SHOW)
|
||||
states = self.getEntityStates(entity)
|
||||
nick = entity.resource
|
||||
show_icon, entity_attr = C.PRESENCE.get(show, (u'', u'default')) # TODO: use entity_attr and return (nick, markup)
|
||||
text = "%s%s %s" % (u''.join(states.values()), show_icon, nick)
|
||||
return (nick, text)
|
||||
if self._user_moved(message):
|
||||
for wid in reversed(self.mess_walker):
|
||||
# we merge in/out messages if no message was sent meanwhile
|
||||
if not isinstance(wid, MessageWidget):
|
||||
continue
|
||||
if wid.mess_data.type != C.MESS_TYPE_INFO:
|
||||
break
|
||||
if wid.info_type in quick_chat.ROOM_USER_MOVED and wid.mess_data.nick == message.nick:
|
||||
try:
|
||||
count = wid.reentered_count
|
||||
except AttributeError:
|
||||
count = wid.reentered_count = 1
|
||||
nick = wid.mess_data.nick
|
||||
if message.info_type == quick_chat.ROOM_USER_LEFT:
|
||||
wid.message = _(u"<= {nick} has left the room ({count})").format(nick=nick, count=count)
|
||||
else:
|
||||
wid.message = _(u"<=> {nick} re-entered the room ({count})") .format(nick=nick, count=count)
|
||||
wid.reentered_count+=1
|
||||
return
|
||||
|
||||
if ((self.host.selected_widget != self or not self.host.x_notify.hasFocus())
|
||||
and self.focus_marker_set is not None):
|
||||
if not self.focus_marker_set and not self._locked and self.mess_walker:
|
||||
if self.focus_marker is not None:
|
||||
self.mess_walker.remove(self.focus_marker)
|
||||
self.focus_marker = urwid.Divider('—')
|
||||
self.mess_walker.append(self.focus_marker)
|
||||
self.focus_marker_set = True
|
||||
else:
|
||||
if self.focus_marker_set:
|
||||
self.focus_marker_set = False
|
||||
|
||||
if not message.message:
|
||||
log.error(u"Received an empty message for uid {}".format(message.uid))
|
||||
else:
|
||||
self.mess_walker.append(MessageWidget(message))
|
||||
self.mess_widgets.focus_position = len(self.mess_walker) - 1 # scroll down
|
||||
self.host.redraw() # FIXME: should not be necessary
|
||||
|
||||
def addUser(self, nick):
|
||||
occupant = super(Chat, self).addUser(nick)
|
||||
bisect.insort(self.occupants_walker, OccupantWidget(occupant))
|
||||
|
||||
def removeUser(self, occupant_data):
|
||||
occupant = super(Chat, self).removeUser(occupant_data)
|
||||
if occupant is not None:
|
||||
for widget in occupant.widgets:
|
||||
self.occupants_walker.remove(widget)
|
||||
|
||||
def _occupantsClicked(self, list_wid, clicked_wid):
|
||||
# FIXME: not called anymore after refactoring
|
||||
assert self.type == C.CHAT_GROUP
|
||||
nick = clicked_wid.getValue().value
|
||||
if nick == self.nick:
|
||||
|
|
@ -274,55 +402,32 @@ class Chat(PrimitivusWidget, QuickChat):
|
|||
|
||||
def setSubject(self, subject, wrap='space'):
|
||||
"""Set title for a group chat"""
|
||||
QuickChat.setSubject(self, subject)
|
||||
self.subject = subject
|
||||
quick_chat.QuickChat.setSubject(self, subject)
|
||||
self.subj_wid = urwid.Text(unicode(subject.replace('\n', '|') if wrap == 'clip' else subject),
|
||||
align='left' if wrap == 'clip' else 'center', wrap=wrap)
|
||||
self.chat_widget.header = urwid.AttrMap(self.subj_wid, 'title')
|
||||
self.host.redraw()
|
||||
|
||||
def clearHistory(self):
|
||||
"""Clear the content of this chat."""
|
||||
del self.content[:]
|
||||
## Messages
|
||||
|
||||
def afterHistoryPrint(self):
|
||||
def updateHistory(self, size=C.HISTORY_LIMIT_DEFAULT, search='', profile='@NONE@'):
|
||||
del self.mess_walker[:]
|
||||
if search:
|
||||
self.mess_walker.append(urwid.Text(_(u"Results for searching the globbing pattern: {}").format(search)))
|
||||
self.mess_walker.append(urwid.Text(_(u"Type ':history <lines>' to reset the chat history").format(search)))
|
||||
super(Chat, self).updateHistory(size, search, profile)
|
||||
|
||||
def _onHistoryPrinted(self):
|
||||
"""Refresh or scroll down the focus after the history is printed"""
|
||||
if len(self.content):
|
||||
self.text_list.focus_position = len(self.content) - 1 # scroll down
|
||||
self.host.redraw()
|
||||
for message in self.messages.itervalues():
|
||||
self.appendMessage(message)
|
||||
super(Chat, self)._onHistoryPrinted()
|
||||
|
||||
def onPrivateCreated(self, widget):
|
||||
self.host.contact_lists[widget.profile].specialResourceVisible(widget.target)
|
||||
|
||||
def printMessage(self, nick, my_message, message, timestamp, extra=None, profile=C.PROF_KEY_NONE):
|
||||
"""Print message in chat window.
|
||||
|
||||
@param nick (unicode): author nick
|
||||
@param my_message (boolean): True if profile is the author
|
||||
@param message (unicode): message content
|
||||
@param extra (dict): extra data
|
||||
"""
|
||||
new_text = ChatText(self, timestamp, nick, my_message, message)
|
||||
self.content.append(new_text)
|
||||
QuickChat.printMessage(self, nick, my_message, message, timestamp, extra, profile)
|
||||
|
||||
def printInfo(self, msg, type_='normal', extra=None):
|
||||
"""Print general info
|
||||
@param msg: message to print
|
||||
@type_: one of:
|
||||
normal: general info like "toto has joined the room"
|
||||
me: "/me" information like "/me clenches his fist" ==> "toto clenches his fist"
|
||||
@param timestamp (float): number of seconds since epoch
|
||||
"""
|
||||
if extra is None:
|
||||
extra = {}
|
||||
try:
|
||||
timestamp = float(extra['timestamp'])
|
||||
except KeyError:
|
||||
timestamp = None
|
||||
_widget = ChatText(self, timestamp, None, False, msg, is_info=True)
|
||||
self.content.append(_widget)
|
||||
QuickChat.printInfo(self, msg, type_, extra)
|
||||
def onSelected(self):
|
||||
self.focus_marker_set = False
|
||||
|
||||
def notify(self, contact="somebody", msg=""):
|
||||
"""Notify the user of a new message if primitivus doesn't have the focus.
|
||||
|
|
@ -330,12 +435,13 @@ class Chat(PrimitivusWidget, QuickChat):
|
|||
@param contact (unicode): contact who wrote to the users
|
||||
@param msg (unicode): the message that has been received
|
||||
"""
|
||||
# FIXME: not called anymore after refactoring
|
||||
if msg == "":
|
||||
return
|
||||
if self.text_list.get_focus()[1] == len(self.content) - 2:
|
||||
if self.mess_widgets.get_focus()[1] == len(self.mess_walker) - 2:
|
||||
# we don't change focus if user is not at the bottom
|
||||
# as that mean that he is probably watching discussion history
|
||||
self.text_list.focus_position = len(self.content) - 1
|
||||
self.mess_widgets.focus_position = len(self.mess_walker) - 1
|
||||
self.host.redraw()
|
||||
if not self.host.x_notify.hasFocus():
|
||||
if self.type == C.CHAT_ONE2ONE:
|
||||
|
|
@ -354,10 +460,11 @@ class Chat(PrimitivusWidget, QuickChat):
|
|||
# MISC EVENTS #
|
||||
|
||||
def onDelete(self):
|
||||
QuickChat.onDelete(self)
|
||||
# FIXME: to be checked after refactoring
|
||||
quick_chat.QuickChat.onDelete(self)
|
||||
if self.type == C.CHAT_GROUP:
|
||||
self.host.removeListener('presence', self.presenceListener)
|
||||
|
||||
|
||||
quick_widgets.register(QuickChat, Chat)
|
||||
quick_widgets.register(quick_chat.QuickChat, Chat)
|
||||
quick_widgets.register(quick_games.Tarot, game_tarot.TarotGame)
|
||||
|
|
|
|||
|
|
@ -32,10 +32,13 @@ class Const(constants.Const):
|
|||
('default_focus', 'default,bold', 'default'),
|
||||
('alert', 'default,underline', 'default'),
|
||||
('alert_focus', 'default,bold,underline', 'default'),
|
||||
# Messages
|
||||
('date', 'light gray', 'default'),
|
||||
('my_nick', 'dark red,bold', 'default'),
|
||||
('other_nick', 'dark cyan,bold', 'default'),
|
||||
('info_msg', 'yellow', 'default', 'bold'),
|
||||
('msg_lang', 'dark cyan', 'default'),
|
||||
|
||||
('menubar', 'light gray,bold', 'dark red'),
|
||||
('menubar_focus', 'light gray,bold', 'dark green'),
|
||||
('selected_menu', 'light gray,bold', 'dark green'),
|
||||
|
|
|
|||
|
|
@ -99,7 +99,7 @@ class EditBar(sat_widgets.ModalEdit):
|
|||
self.host.messageSend(
|
||||
chat_widget.target,
|
||||
{'': editBar.get_edit_text()}, # TODO: handle language
|
||||
mess_type = "groupchat" if chat_widget.type == 'group' else "chat", # TODO: put this in QuickChat
|
||||
mess_type = C.MESS_TYPE_GROUPCHAT if chat_widget.type == C.CHAT_GROUP else C.MESS_TYPE_CHAT, # TODO: put this in QuickChat
|
||||
errback=lambda failure: self.host.notify(_("Error while sending message ({})").format(failure)),
|
||||
profile_key=chat_widget.profile
|
||||
)
|
||||
|
|
@ -139,19 +139,15 @@ class EditBar(sat_widgets.ModalEdit):
|
|||
limit = int(args[0])
|
||||
except (IndexError, ValueError):
|
||||
limit = 50
|
||||
widget.clearHistory()
|
||||
if limit > 0:
|
||||
widget.historyPrint(size=limit, profile=widget.profile)
|
||||
widget.updateHistory(size=limit, profile=widget.profile)
|
||||
elif command == 'search':
|
||||
widget = self.host.selected_widget
|
||||
if isinstance(widget, quick_chat.QuickChat):
|
||||
pattern = " ".join(args)
|
||||
if not pattern:
|
||||
self.host.notif_bar.addMessage(D_("Please specify the globbing pattern to search for"))
|
||||
widget.clearHistory()
|
||||
widget.printInfo(D_("Results for searching the globbing pattern: %s") % pattern, extra={'timestamp': 0})
|
||||
widget.historyPrint(size=C.HISTORY_LIMIT_NONE, search=pattern, profile=widget.profile)
|
||||
widget.printInfo(D_("Type ':history <lines>' to reset the chat history"))
|
||||
else:
|
||||
widget.updateHistory(size=C.HISTORY_LIMIT_NONE, search=pattern, profile=widget.profile)
|
||||
else:
|
||||
return
|
||||
self.set_edit_text('')
|
||||
|
|
@ -540,7 +536,8 @@ class PrimitivusApp(QuickApp, InputHistory):
|
|||
self.redraw()
|
||||
|
||||
def newWidget(self, widget):
|
||||
self.selectWidget(widget)
|
||||
if self.selected_widget is None:
|
||||
self.selectWidget(widget)
|
||||
|
||||
def selectWidget(self, widget):
|
||||
"""Display a widget if possible,
|
||||
|
|
@ -556,6 +553,12 @@ class PrimitivusApp(QuickApp, InputHistory):
|
|||
except KeyError:
|
||||
log.debug("No menu to delete")
|
||||
self.selected_widget = widget
|
||||
try:
|
||||
onSelected = self.selected_widget.onSelected
|
||||
except AttributeError:
|
||||
pass
|
||||
else:
|
||||
onSelected()
|
||||
self._visible_widgets = set([widget]) # XXX: we can only have one widget visible at the time for now
|
||||
self.contact_lists.select(None)
|
||||
|
||||
|
|
@ -722,11 +725,12 @@ class PrimitivusApp(QuickApp, InputHistory):
|
|||
log.error (_("FIXME FIXME FIXME: type [%s] not implemented") % type_)
|
||||
raise NotImplementedError
|
||||
|
||||
def roomJoinedHandler(self, room_jid_s, room_nicks, user_nick, profile):
|
||||
super(PrimitivusApp, self).roomJoinedHandler(room_jid_s, room_nicks, user_nick, profile)
|
||||
for contact_list in self.widgets.getWidgets(ContactList):
|
||||
if profile in contact_list.profiles:
|
||||
contact_list.setFocus(jid.JID(room_jid_s), True)
|
||||
def roomJoinedHandler(self, room_jid_s, room_nicks, user_nick, subject, profile):
|
||||
super(PrimitivusApp, self).roomJoinedHandler(room_jid_s, room_nicks, user_nick, subject, profile)
|
||||
# if self.selected_widget is None:
|
||||
# for contact_list in self.widgets.getWidgets(ContactList):
|
||||
# if profile in contact_list.profiles:
|
||||
# contact_list.setFocus(jid.JID(room_jid_s), True)
|
||||
|
||||
def progressStartedHandler(self, pid, metadata, profile):
|
||||
super(PrimitivusApp, self).progressStartedHandler(pid, metadata, profile)
|
||||
|
|
@ -745,7 +749,7 @@ class PrimitivusApp(QuickApp, InputHistory):
|
|||
def onJoinRoom(self, button, edit):
|
||||
self.removePopUp()
|
||||
room_jid = jid.JID(edit.get_edit_text())
|
||||
self.bridge.joinMUC(room_jid, self.profiles[self.current_profile].whoami.node, {}, self.current_profile, callback=lambda dummy: None, errback=self.dialogFailure)
|
||||
self.bridge.mucJoin(room_jid, self.profiles[self.current_profile].whoami.node, {}, self.current_profile, callback=lambda dummy: None, errback=self.dialogFailure)
|
||||
|
||||
#MENU EVENTS#
|
||||
def onConnectRequest(self, menu):
|
||||
|
|
|
|||
|
|
@ -122,13 +122,6 @@ class ProfileManager(object):
|
|||
#Now we open the MUC window where we already are:
|
||||
for room_args in rooms_args:
|
||||
self.host.roomJoinedHandler(*room_args, profile=self.profile)
|
||||
|
||||
self.bridge.getRoomsSubjects(self.profile, callback=self._plug_profile_gotRoomsSubjects)
|
||||
|
||||
def _plug_profile_gotRoomsSubjects(self, subjects_args):
|
||||
for subject_args in subjects_args:
|
||||
self.host.roomNewSubjectHandler(*subject_args, profile=self.profile)
|
||||
|
||||
#Presence must be requested after rooms are filled
|
||||
self.host.bridge.getPresenceStatuses(self.profile, callback=self._plug_profile_gotPresences)
|
||||
|
||||
|
|
@ -263,8 +256,6 @@ class QuickApp(object):
|
|||
self.registerSignal("actionResultExt", self.actionResultHandler)
|
||||
self.registerSignal("roomJoined", iface="plugin")
|
||||
self.registerSignal("roomLeft", iface="plugin")
|
||||
self.registerSignal("roomUserJoined", iface="plugin")
|
||||
self.registerSignal("roomUserLeft", iface="plugin")
|
||||
self.registerSignal("roomUserChangedNick", iface="plugin")
|
||||
self.registerSignal("roomNewSubject", iface="plugin")
|
||||
self.registerSignal("chatStateReceived", iface="plugin")
|
||||
|
|
@ -490,11 +481,8 @@ class QuickApp(object):
|
|||
|
||||
from_me = from_jid.bare == self.profiles[profile].whoami.bare
|
||||
target = to_jid if from_me else from_jid
|
||||
|
||||
chat_type = C.CHAT_GROUP if type_ == C.MESS_TYPE_GROUPCHAT else C.CHAT_ONE2ONE
|
||||
contact_list = self.contact_lists[profile]
|
||||
|
||||
chat_widget = self.widgets.getOrCreateWidget(quick_chat.QuickChat, target, type_=chat_type, on_new_widget=None, profile=profile)
|
||||
chat_widget = self.widgets.getOrCreateWidget(quick_chat.QuickChat, target, type_=C.CHAT_ONE2ONE, on_new_widget=None, profile=profile)
|
||||
|
||||
self.current_action_ids = set() # FIXME: to be removed
|
||||
self.current_action_ids_cb = {} # FIXME: to be removed
|
||||
|
|
@ -560,14 +548,14 @@ class QuickApp(object):
|
|||
|
||||
self.callListeners('presence', entity, show, priority, statuses, profile=profile)
|
||||
|
||||
def roomJoinedHandler(self, room_jid_s, room_nicks, user_nick, profile):
|
||||
def roomJoinedHandler(self, room_jid_s, occupants, user_nick, subject, profile):
|
||||
"""Called when a MUC room is joined"""
|
||||
log.debug(u"Room [%(room_jid)s] joined by %(profile)s, users presents:%(users)s" % {'room_jid': room_jid_s, 'profile': profile, 'users': room_nicks})
|
||||
log.debug(u"Room [{room_jid}] joined by {profile}, users presents:{users}".format(room_jid=room_jid_s, profile=profile, users=occupants.keys()))
|
||||
room_jid = jid.JID(room_jid_s)
|
||||
chat_widget = self.widgets.getOrCreateWidget(quick_chat.QuickChat, room_jid, type_=C.CHAT_GROUP, profile=profile)
|
||||
chat_widget = self.widgets.getOrCreateWidget(quick_chat.QuickChat, room_jid, type_=C.CHAT_GROUP, occupants=occupants, subject=subject, profile=profile)
|
||||
chat_widget.setUserNick(unicode(user_nick))
|
||||
self.contact_lists[profile].setSpecial(room_jid, C.CONTACT_SPECIAL_GROUP)
|
||||
chat_widget.update()
|
||||
# chat_widget.update()
|
||||
|
||||
def roomLeftHandler(self, room_jid_s, profile):
|
||||
"""Called when a MUC room is left"""
|
||||
|
|
@ -578,20 +566,6 @@ class QuickApp(object):
|
|||
self.widgets.deleteWidget(chat_widget)
|
||||
self.contact_lists[profile].removeContact(room_jid)
|
||||
|
||||
def roomUserJoinedHandler(self, room_jid_s, user_nick, user_data, profile):
|
||||
"""Called when an user joined a MUC room"""
|
||||
room_jid = jid.JID(room_jid_s)
|
||||
chat_widget = self.widgets.getOrCreateWidget(quick_chat.QuickChat, room_jid, type_=C.CHAT_GROUP, profile=profile)
|
||||
chat_widget.addUser(user_nick)
|
||||
log.debug(u"user [%(user_nick)s] joined room [%(room_jid)s]" % {'user_nick': user_nick, 'room_jid': room_jid})
|
||||
|
||||
def roomUserLeftHandler(self, room_jid_s, user_nick, user_data, profile):
|
||||
"""Called when an user joined a MUC room"""
|
||||
room_jid = jid.JID(room_jid_s)
|
||||
chat_widget = self.widgets.getOrCreateWidget(quick_chat.QuickChat, room_jid, type_=C.CHAT_GROUP, profile=profile)
|
||||
chat_widget.removeUser(user_nick)
|
||||
log.debug(u"user [%(user_nick)s] left room [%(room_jid)s]" % {'user_nick': user_nick, 'room_jid': room_jid})
|
||||
|
||||
def roomUserChangedNickHandler(self, room_jid_s, old_nick, new_nick, profile):
|
||||
"""Called when an user joined a MUC room"""
|
||||
room_jid = jid.JID(room_jid_s)
|
||||
|
|
@ -613,20 +587,20 @@ class QuickApp(object):
|
|||
@param state (unicode): new state
|
||||
@param profile (unicode): current profile
|
||||
"""
|
||||
log.debug(_(u"Received new chat state {} from {} [{}]").format(state, from_jid_s, profile))
|
||||
from_jid = jid.JID(from_jid_s) if from_jid_s != C.ENTITY_ALL else C.ENTITY_ALL
|
||||
contact_list = self.contact_lists[profile]
|
||||
for widget in self.widgets.getWidgets(quick_chat.QuickChat):
|
||||
if profile != widget.profile:
|
||||
continue
|
||||
to_display = C.USER_CHAT_STATES[state] if (state and widget.type == C.CHAT_GROUP) else state
|
||||
if widget.type == C.CHAT_GROUP and from_jid_s == C.ENTITY_ALL:
|
||||
for occupant in [jid.newResource(widget.target, nick) for nick in widget.occupants]:
|
||||
contact_list.setCache(occupant, 'chat_state', to_display)
|
||||
widget.update(occupant)
|
||||
elif from_jid.bare == widget.target.bare: # roster contact or MUC occupant
|
||||
contact_list.setCache(from_jid, 'chat_state', to_display)
|
||||
widget.update(from_jid)
|
||||
# log.debug(_(u"Received new chat state {} from {} [{}]").format(state, from_jid_s, profile))
|
||||
# from_jid = jid.JID(from_jid_s) if from_jid_s != C.ENTITY_ALL else C.ENTITY_ALL
|
||||
# contact_list = self.contact_lists[profile]
|
||||
# for widget in self.widgets.getWidgets(quick_chat.QuickChat):
|
||||
# if profile != widget.profile:
|
||||
# continue
|
||||
# to_display = C.USER_CHAT_STATES[state] if (state and widget.type == C.CHAT_GROUP) else state
|
||||
# if widget.type == C.CHAT_GROUP and from_jid_s == C.ENTITY_ALL:
|
||||
# for occupant in [jid.newResource(widget.target, nick) for nick in widget.occupants]:
|
||||
# contact_list.setCache(occupant, 'chat_state', to_display)
|
||||
# widget.update(occupant)
|
||||
# elif from_jid.bare == widget.target.bare: # roster contact or MUC occupant
|
||||
# contact_list.setCache(from_jid, 'chat_state', to_display)
|
||||
# widget.update(from_jid)
|
||||
|
||||
def psEventHandler(self, category, service_s, node, event_type, data, profile):
|
||||
"""Called when a PubSub event is received.
|
||||
|
|
|
|||
|
|
@ -20,11 +20,17 @@
|
|||
from sat.core.i18n import _
|
||||
from sat.core.log import getLogger
|
||||
log = getLogger(__name__)
|
||||
from sat_frontends.tools import jid
|
||||
from sat.core import exceptions
|
||||
from sat_frontends.quick_frontend import quick_widgets
|
||||
from sat_frontends.quick_frontend.constants import Const as C
|
||||
from collections import OrderedDict
|
||||
from datetime import datetime
|
||||
from sat_frontends.tools import jid
|
||||
|
||||
ROOM_USER_JOINED = 'ROOM_USER_JOINED'
|
||||
ROOM_USER_LEFT = 'ROOM_USER_LEFT'
|
||||
ROOM_USER_MOVED = (ROOM_USER_JOINED, ROOM_USER_LEFT)
|
||||
|
||||
# from datetime import datetime
|
||||
|
||||
try:
|
||||
# FIXME: to be removed when an acceptable solution is here
|
||||
|
|
@ -32,26 +38,109 @@ try:
|
|||
except (TypeError, AttributeError): # Error raised is not the same depending on pyjsbuild options
|
||||
unicode = str
|
||||
|
||||
# FIXME: day_format need to be settable (i18n)
|
||||
|
||||
class Message(object):
|
||||
"""Message metadata"""
|
||||
|
||||
def __init__(self, parent, uid, timestamp, from_jid, to_jid, msg, subject, type_, extra, profile):
|
||||
self.parent = parent
|
||||
self.profile = profile
|
||||
self.uid = uid
|
||||
self.timestamp = timestamp
|
||||
self.from_jid = from_jid
|
||||
self.to_jid = to_jid
|
||||
self.message = msg
|
||||
self.subject = subject
|
||||
self.type = type_
|
||||
self.extra = extra
|
||||
self.nick = self.getNick(from_jid)
|
||||
# own_mess is True if message was sent by profile's jid
|
||||
self.own_mess = (from_jid.resource == self.parent.nick) if self.parent.type == C.CHAT_GROUP else (from_jid.bare == self.host.profiles[profile].whoami.bare)
|
||||
self.widgets = set() # widgets linked to this message
|
||||
|
||||
@property
|
||||
def host(self):
|
||||
return self.parent.host
|
||||
|
||||
@property
|
||||
def info_type(self):
|
||||
return self.extra.get('info_type')
|
||||
|
||||
def getNick(self, entity):
|
||||
"""Return nick of an entity when possible"""
|
||||
contact_list = self.host.contact_lists[self.profile]
|
||||
if self.type == C.MESS_TYPE_INFO and self.info_type in ROOM_USER_MOVED:
|
||||
try:
|
||||
return self.extra['user_nick']
|
||||
except KeyError:
|
||||
log.error(u"extra data is missing user nick for uid {}".format(self.uid))
|
||||
return ""
|
||||
if self.parent.type == C.CHAT_GROUP or entity in contact_list.getSpecialExtras(C.CONTACT_SPECIAL_GROUP):
|
||||
return entity.resource or ""
|
||||
if entity.bare in contact_list:
|
||||
return contact_list.getCache(entity, 'nick') or contact_list.getCache(entity, 'name') or entity.node or entity
|
||||
return entity.node or entity
|
||||
|
||||
|
||||
class Occupant(object):
|
||||
"""Occupant metadata"""
|
||||
|
||||
def __init__(self, parent, data, profile):
|
||||
self.parent = parent
|
||||
self.profile = profile
|
||||
self.nick = data['nick']
|
||||
self.entity = data.get('entity')
|
||||
if not self.entity:
|
||||
self.entity = jid.JID(u"{}/{}".format(parent.target.bare, self.nick)),
|
||||
self.affiliation = data['affiliation']
|
||||
self.role = data['role']
|
||||
self.widgets = set() # widgets linked to this occupant
|
||||
|
||||
@property
|
||||
def host(self):
|
||||
return self.parent.host
|
||||
|
||||
|
||||
class QuickChat(quick_widgets.QuickWidget):
|
||||
|
||||
visible_states = ['chat_state']
|
||||
|
||||
def __init__(self, host, target, type_=C.CHAT_ONE2ONE, profiles=None):
|
||||
def __init__(self, host, target, type_=C.CHAT_ONE2ONE, occupants=None, subject=None, profiles=None):
|
||||
"""
|
||||
@param type_: can be C.CHAT_ONE2ONE for single conversation or C.CHAT_GROUP for chat à la IRC
|
||||
"""
|
||||
self.lang = '' # default language to use for messages
|
||||
quick_widgets.QuickWidget.__init__(self, host, target, profiles=profiles)
|
||||
self._locked = False # True when we are waiting for history/search
|
||||
# messageNew signals are cached when locked
|
||||
self._cache = []
|
||||
assert type_ in (C.CHAT_ONE2ONE, C.CHAT_GROUP)
|
||||
if type_ == C.CHAT_GROUP and target.resource:
|
||||
raise ValueError("A group chat entity can't have a resource")
|
||||
self.current_target = target
|
||||
self.type = type_
|
||||
self.id = "" # FIXME: to be removed
|
||||
self.nick = None
|
||||
if type_ == C.CHAT_GROUP:
|
||||
if target.resource:
|
||||
raise exceptions.InternalError(u"a group chat entity can't have a resource")
|
||||
self.nick = None
|
||||
self.occupants = {}
|
||||
self.setOccupants(occupants)
|
||||
else:
|
||||
if occupants is not None:
|
||||
raise exceptions.InternalError(u"only group chat can have occupants")
|
||||
self.messages = OrderedDict() # key: uid, value: Message instance
|
||||
self.games = {} # key=game name (unicode), value=instance of quick_games.RoomGame
|
||||
self.subject = subject
|
||||
|
||||
def postInit(self):
|
||||
"""Method to be called by frontend after widget is initialised
|
||||
|
||||
handle the display of history and subject
|
||||
"""
|
||||
self.historyPrint(profile=self.profile)
|
||||
if self.subject is not None:
|
||||
self.setSubject(self.subject)
|
||||
|
||||
## Widget management ##
|
||||
|
||||
def __str__(self):
|
||||
return u"Chat Widget [target: {}, type: {}, profile: {}]".format(self.target, self.type, self.profile)
|
||||
|
|
@ -74,109 +163,6 @@ class QuickChat(quick_widgets.QuickWidget):
|
|||
if target.resource:
|
||||
self.current_target = target # FIXME: tmp, must use resource priority throught contactList instead
|
||||
|
||||
@property
|
||||
def target(self):
|
||||
if self.type == C.CHAT_GROUP:
|
||||
return self.current_target.bare
|
||||
return self.current_target
|
||||
|
||||
@property
|
||||
def occupants(self):
|
||||
"""Return the occupants of a group chat (nicknames).
|
||||
|
||||
@return: set(unicode)
|
||||
"""
|
||||
if self.type != C.CHAT_GROUP:
|
||||
return set()
|
||||
contact_list = self.host.contact_lists[self.profile]
|
||||
return contact_list.getCache(self.target, C.CONTACT_RESOURCES).keys()
|
||||
|
||||
def manageMessage(self, entity, mess_type):
|
||||
"""Tell if this chat widget manage an entity and message type couple
|
||||
|
||||
@param entity (jid.JID): (full) jid of the sending entity
|
||||
@param mess_type (str): message type as given by messageNew
|
||||
@return (bool): True if this Chat Widget manage this couple
|
||||
"""
|
||||
if self.type == C.CHAT_GROUP:
|
||||
if mess_type == C.MESS_TYPE_GROUPCHAT and self.target == entity.bare:
|
||||
return True
|
||||
else:
|
||||
if mess_type != C.MESS_TYPE_GROUPCHAT and entity in self.targets:
|
||||
return True
|
||||
return False
|
||||
|
||||
def addUser(self, nick):
|
||||
"""Add user if it is not in the group list"""
|
||||
self.printInfo("=> %s has joined the room" % nick)
|
||||
|
||||
def removeUser(self, nick):
|
||||
"""Remove a user from the group list"""
|
||||
self.printInfo("<= %s has left the room" % nick)
|
||||
|
||||
def setUserNick(self, nick):
|
||||
"""Set the nick of the user, usefull for e.g. change the color of the user"""
|
||||
self.nick = nick
|
||||
|
||||
def changeUserNick(self, old_nick, new_nick):
|
||||
"""Change nick of a user in group list"""
|
||||
self.printInfo("%s is now known as %s" % (old_nick, new_nick))
|
||||
|
||||
def setSubject(self, subject):
|
||||
"""Set title for a group chat"""
|
||||
log.debug(_("Setting subject to %s") % subject)
|
||||
if self.type != C.CHAT_GROUP:
|
||||
log.error (_("[INTERNAL] trying to set subject for a non group chat window"))
|
||||
raise Exception("INTERNAL ERROR") #TODO: raise proper Exception here
|
||||
|
||||
def afterHistoryPrint(self):
|
||||
"""Refresh or scroll down the focus after the history is printed"""
|
||||
pass
|
||||
|
||||
def historyPrint(self, size=C.HISTORY_LIMIT_DEFAULT, search='', profile='@NONE@'):
|
||||
"""Print the current history
|
||||
|
||||
@param size (int): number of messages
|
||||
@param search (str): pattern to filter the history results
|
||||
@param profile (str): %(doc_profile)s
|
||||
"""
|
||||
log_msg = _(u"now we print the history")
|
||||
if size != C.HISTORY_LIMIT_DEFAULT:
|
||||
log_msg += _(u" (%d messages)" % size)
|
||||
log.debug(log_msg)
|
||||
|
||||
target = self.target.bare
|
||||
|
||||
def _historyGetCb(history):
|
||||
day_format = "%A, %d %b %Y" # to display the day change
|
||||
previous_day = datetime.now().strftime(day_format)
|
||||
for data in history:
|
||||
uid, timestamp, from_jid, to_jid, message, subject, type_, extra = data # FIXME: extra is unused !
|
||||
if ((self.type == C.CHAT_GROUP and type_ != C.MESS_TYPE_GROUPCHAT) or
|
||||
(self.type == C.CHAT_ONE2ONE and type_ == C.MESS_TYPE_GROUPCHAT)):
|
||||
continue
|
||||
message_day = datetime.fromtimestamp(timestamp).strftime(day_format)
|
||||
if previous_day != message_day:
|
||||
self.printDayChange(message_day)
|
||||
previous_day = message_day
|
||||
extra["timestamp"] = timestamp
|
||||
self.messageNew(uid, timestamp, jid.JID(from_jid), target, message, subject, type_, extra, profile)
|
||||
self.afterHistoryPrint()
|
||||
|
||||
def _historyGetEb(err):
|
||||
log.error(_("Can't get history"))
|
||||
|
||||
self.host.bridge.historyGet(unicode(self.host.profiles[profile].whoami.bare), unicode(target), size, True, search, profile, callback=_historyGetCb, errback=_historyGetEb)
|
||||
|
||||
def _get_nick(self, entity):
|
||||
"""Return nick of this entity when possible"""
|
||||
contact_list = self.host.contact_lists[self.profile]
|
||||
if self.type == C.CHAT_GROUP or entity in contact_list.getSpecialExtras(C.CONTACT_SPECIAL_GROUP):
|
||||
return entity.resource or ""
|
||||
if entity.bare in contact_list:
|
||||
return contact_list.getCache(entity, 'nick') or contact_list.getCache(entity, 'name') or entity.node or entity
|
||||
return entity.node or entity
|
||||
|
||||
def onPrivateCreated(self, widget):
|
||||
"""Method called when a new widget for private conversation (MUC) is created"""
|
||||
raise NotImplementedError
|
||||
|
|
@ -188,57 +174,171 @@ class QuickChat(quick_widgets.QuickWidget):
|
|||
"""
|
||||
return self.host.widgets.getOrCreateWidget(QuickChat, entity, type_=C.CHAT_ONE2ONE, force_hash=self.getPrivateHash(self.profile, entity), on_new_widget=self.onPrivateCreated, profile=self.profile) # we force hash to have a new widget, not this one again
|
||||
|
||||
def messageNew(self, uid, timestamp, from_jid, target, msg, subject, type_, extra, profile):
|
||||
@property
|
||||
def target(self):
|
||||
if self.type == C.CHAT_GROUP:
|
||||
return self.current_target.bare
|
||||
return self.current_target
|
||||
|
||||
## occupants ##
|
||||
|
||||
def setOccupants(self, occupants):
|
||||
"""set the whole list of occupants"""
|
||||
assert len(self.occupants) == 0
|
||||
for nick, data in occupants.iteritems():
|
||||
self.occupants[nick] = Occupant(
|
||||
self,
|
||||
data,
|
||||
self.profile
|
||||
)
|
||||
|
||||
def addUser(self, occupant_data):
|
||||
"""Add user if it is not in the group list"""
|
||||
occupant = Occupant(
|
||||
self,
|
||||
occupant_data,
|
||||
self.profile
|
||||
)
|
||||
self.occupants[occupant.nick] = occupant
|
||||
return occupant
|
||||
|
||||
def removeUser(self, occupant_data):
|
||||
"""Remove a user from the group list"""
|
||||
nick = occupant_data['nick']
|
||||
try:
|
||||
msg = msg.itervalues().next() # FIXME: tmp fix until message refactoring is finished (msg is now a dict)
|
||||
except StopIteration:
|
||||
log.warning(u"No message found (uid: {})".format(uid))
|
||||
msg = ''
|
||||
if self.type == C.CHAT_GROUP and target.resource and type_ != C.MESS_TYPE_GROUPCHAT:
|
||||
# we have a private message, we forward it to a private conversation widget
|
||||
chat_widget = self.getOrCreatePrivateWidget(target)
|
||||
chat_widget.messageNew(uid, timestamp, from_jid, target, msg, subject, type_, extra, profile)
|
||||
return
|
||||
|
||||
if type_ == C.MESS_TYPE_INFO:
|
||||
self.printInfo(msg, extra=extra)
|
||||
occupant = self.occupants.pop(nick)
|
||||
except KeyError:
|
||||
log.warning(u"Trying to remove an unknown occupant: {}".format(nick))
|
||||
else:
|
||||
nick = self._get_nick(from_jid)
|
||||
if msg.startswith('/me '):
|
||||
self.printInfo('* {} {}'.format(nick, msg[4:]), type_='me', extra=extra)
|
||||
else:
|
||||
# my_message is True if message comes from local user
|
||||
my_message = (from_jid.resource == self.nick) if self.type == C.CHAT_GROUP else (from_jid.bare == self.host.profiles[profile].whoami.bare)
|
||||
self.printMessage(nick, my_message, msg, timestamp, extra, profile)
|
||||
# FIXME: to be checked/removed after message refactoring
|
||||
# if timestamp:
|
||||
self.afterHistoryPrint()
|
||||
return occupant
|
||||
|
||||
def printMessage(self, nick, my_message, message, timestamp, extra=None, profile=C.PROF_KEY_NONE):
|
||||
"""Print message in chat window.
|
||||
def setUserNick(self, nick):
|
||||
"""Set the nick of the user, usefull for e.g. change the color of the user"""
|
||||
self.nick = nick
|
||||
|
||||
@param nick (unicode): author nick
|
||||
@param my_message (boolean): True if profile is the author
|
||||
@param message (unicode): message content
|
||||
@param extra (dict): extra data
|
||||
def changeUserNick(self, old_nick, new_nick):
|
||||
"""Change nick of a user in group list"""
|
||||
self.printInfo("%s is now known as %s" % (old_nick, new_nick))
|
||||
|
||||
## Messages ##
|
||||
|
||||
def manageMessage(self, entity, mess_type):
|
||||
"""Tell if this chat widget manage an entity and message type couple
|
||||
|
||||
@param entity (jid.JID): (full) jid of the sending entity
|
||||
@param mess_type (str): message type as given by messageNew
|
||||
@return (bool): True if this Chat Widget manage this couple
|
||||
"""
|
||||
# FIXME: check/remove this if necessary (message refactoring)
|
||||
# if not timestamp:
|
||||
# # XXX: do not send notifications for each line of the history being displayed
|
||||
# # FIXME: this must be changed in the future if the timestamp is passed with
|
||||
# # all messages and not only with the messages coming from the history.
|
||||
self.notify(nick, message)
|
||||
if self.type == C.CHAT_GROUP:
|
||||
if mess_type in (C.MESS_TYPE_GROUPCHAT, C.MESS_TYPE_INFO) and self.target == entity.bare:
|
||||
return True
|
||||
else:
|
||||
if mess_type != C.MESS_TYPE_GROUPCHAT and entity in self.targets:
|
||||
return True
|
||||
return False
|
||||
|
||||
def printInfo(self, msg, type_='normal', extra=None):
|
||||
"""Print general info.
|
||||
def updateHistory(self, size=C.HISTORY_LIMIT_DEFAULT, search='', profile='@NONE@'):
|
||||
"""Called when history need to be recreated
|
||||
|
||||
@param msg (unicode): message to print
|
||||
@param type_ (unicode):
|
||||
- 'normal': general info like "toto has joined the room"
|
||||
- 'me': "/me" information like "/me clenches his fist" ==> "toto clenches his fist"
|
||||
@param extra (dict): message data
|
||||
Remove all message from history then call historyPrint
|
||||
Must probably be overriden by frontend to clear widget
|
||||
@param size (int): number of messages
|
||||
@param search (str): pattern to filter the history results
|
||||
@param profile (str): %(doc_profile)s
|
||||
"""
|
||||
self.notify(msg=msg)
|
||||
self._locked = True
|
||||
self.messages.clear()
|
||||
self.historyPrint(size, search, profile)
|
||||
|
||||
def _onHistoryPrinted(self):
|
||||
"""Method called when history is printed (or failed)
|
||||
|
||||
unlock the widget, and can be used to refresh or scroll down
|
||||
the focus after the history is printed
|
||||
"""
|
||||
self._locked = False
|
||||
for data in self._cache:
|
||||
self.messageNew(*data)
|
||||
|
||||
def historyPrint(self, size=C.HISTORY_LIMIT_DEFAULT, search='', profile='@NONE@'):
|
||||
"""Print the current history
|
||||
|
||||
@param size (int): number of messages
|
||||
@param search (str): pattern to filter the history results
|
||||
@param profile (str): %(doc_profile)s
|
||||
"""
|
||||
if size == 0:
|
||||
log.debug(u"Empty history requested, skipping")
|
||||
self._onHistoryPrinted()
|
||||
return
|
||||
log_msg = _(u"now we print the history")
|
||||
if size != C.HISTORY_LIMIT_DEFAULT:
|
||||
log_msg += _(u" ({} messages)".format(size))
|
||||
log.debug(log_msg)
|
||||
|
||||
target = self.target.bare
|
||||
|
||||
def _historyGetCb(history):
|
||||
# day_format = "%A, %d %b %Y" # to display the day change
|
||||
# previous_day = datetime.now().strftime(day_format)
|
||||
# message_day = datetime.fromtimestamp(timestamp).strftime(self.day_format)
|
||||
# if previous_day != message_day:
|
||||
# self.printDayChange(message_day)
|
||||
# previous_day = message_day
|
||||
for data in history:
|
||||
uid, timestamp, from_jid, to_jid, message, subject, type_, extra = data
|
||||
from_jid = jid.JID(from_jid)
|
||||
to_jid = jid.JID(to_jid)
|
||||
# if ((self.type == C.CHAT_GROUP and type_ != C.MESS_TYPE_GROUPCHAT) or
|
||||
# (self.type == C.CHAT_ONE2ONE and type_ == C.MESS_TYPE_GROUPCHAT)):
|
||||
# continue
|
||||
self.messages[uid] = Message(self, uid, timestamp, from_jid, to_jid, message, subject, type_, extra, profile)
|
||||
self._onHistoryPrinted()
|
||||
|
||||
def _historyGetEb(err):
|
||||
log.error(_(u"Can't get history"))
|
||||
self._onHistoryPrinted()
|
||||
|
||||
self.host.bridge.historyGet(unicode(self.host.profiles[profile].whoami.bare), unicode(target), size, True, search, profile, callback=_historyGetCb, errback=_historyGetEb)
|
||||
|
||||
def messageNew(self, uid, timestamp, from_jid, to_jid, msg, subject, type_, extra, profile):
|
||||
log.debug(u"messageNew ==> {}".format((uid, timestamp, from_jid, to_jid, msg, subject, type_, extra, profile)))
|
||||
if self._locked:
|
||||
self._cache.append(uid, timestamp, from_jid, to_jid, msg, subject, type_, extra, profile)
|
||||
return
|
||||
if self.type == C.CHAT_GROUP:
|
||||
if to_jid.resource and type_ != C.MESS_TYPE_GROUPCHAT:
|
||||
# we have a private message, we forward it to a private conversation widget
|
||||
chat_widget = self.getOrCreatePrivateWidget(to_jid)
|
||||
chat_widget.messageNew(uid, timestamp, from_jid, to_jid, msg, subject, type_, extra, profile)
|
||||
return
|
||||
if type_ == C.MESS_TYPE_INFO:
|
||||
try:
|
||||
info_type = extra['info_type']
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
user_data = {k[5:]:v for k,v in extra.iteritems() if k.startswith('user_')}
|
||||
if info_type == ROOM_USER_JOINED:
|
||||
self.addUser(user_data)
|
||||
elif info_type == ROOM_USER_LEFT:
|
||||
self.removeUser(user_data)
|
||||
|
||||
message = Message(self, uid, timestamp, from_jid, to_jid, msg, subject, type_, extra, profile)
|
||||
self.messages[uid] = message
|
||||
|
||||
if 'received_timestamp' in extra:
|
||||
log.warning(u"Delayed message received after history, this should not happen")
|
||||
self.createMessage(message)
|
||||
|
||||
def createMessage(self, message, append=False):
|
||||
"""Must be implemented by frontend to create and show a new message widget
|
||||
|
||||
This is only called on messageNew, not on history.
|
||||
You need to override historyPrint to handle the later
|
||||
@param message(Message): message data
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def notify(self, contact="somebody", msg=""):
|
||||
"""Notify the user of a new message if the frontend doesn't have the focus.
|
||||
|
|
@ -246,6 +346,7 @@ class QuickChat(quick_widgets.QuickWidget):
|
|||
@param contact (unicode): contact who wrote to the users
|
||||
@param msg (unicode): the message that has been received
|
||||
"""
|
||||
# FIXME: not called anymore after refactoring
|
||||
raise NotImplemented
|
||||
|
||||
def printDayChange(self, day):
|
||||
|
|
@ -253,21 +354,16 @@ class QuickChat(quick_widgets.QuickWidget):
|
|||
|
||||
@param day(unicode): day to display (or not if this method is not overwritten)
|
||||
"""
|
||||
# FIXME: not called anymore after refactoring
|
||||
pass
|
||||
|
||||
def getEntityStates(self, entity):
|
||||
"""Retrieve states for an entity.
|
||||
## Room ##
|
||||
|
||||
@param entity (jid.JID): entity
|
||||
@return: OrderedDict{unicode: unicode}
|
||||
"""
|
||||
states = OrderedDict()
|
||||
clist = self.host.contact_lists[self.profile]
|
||||
for key in self.visible_states:
|
||||
value = clist.getCache(entity, key)
|
||||
if value:
|
||||
states[key] = value
|
||||
return states
|
||||
def setSubject(self, subject):
|
||||
"""Set title for a group chat"""
|
||||
self.subject = subject
|
||||
if self.type != C.CHAT_GROUP:
|
||||
raise exceptions.InternalError("trying to set subject for a non group chat window")
|
||||
|
||||
def addGamePanel(self, widget):
|
||||
"""Insert a game panel to this Chat dialog.
|
||||
|
|
|
|||
|
|
@ -425,7 +425,7 @@ class DbusObject(dbus.service.Object):
|
|||
@dbus.service.method(const_INT_PREFIX+const_CORE_SUFFIX,
|
||||
in_signature='sa{ss}a{ss}sa{ss}s', out_signature='',
|
||||
async_callbacks=('callback', 'errback'))
|
||||
def messageSend(self, to_jid, message, subject='', mess_type="auto", extra={}, profile_key="@NONE@", callback=None, errback=None):
|
||||
def messageSend(self, to_jid, message, subject={}, mess_type="auto", extra={}, profile_key="@NONE@", callback=None, errback=None):
|
||||
return self._callback("messageSend", unicode(to_jid), message, subject, unicode(mess_type), extra, unicode(profile_key), callback=callback, errback=errback)
|
||||
|
||||
@dbus.service.method(const_INT_PREFIX+const_CORE_SUFFIX,
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@ doc_param_7=extra: extra message information, can have data added by plugins and
|
|||
- thread_parent: id of the parent of the current thread
|
||||
- received_timestamp: date of receiption for delayed messages
|
||||
- delay_sender: entity which has originally sent or which has delayed the message
|
||||
- info_type: subtype for info messages
|
||||
doc_param_8=%(doc_profile)s
|
||||
|
||||
[newAlert]
|
||||
|
|
@ -447,7 +448,7 @@ type=method
|
|||
category=core
|
||||
sig_in=sa{ss}a{ss}sa{ss}s
|
||||
sig_out=
|
||||
param_2_default=''
|
||||
param_2_default={}
|
||||
param_3_default="auto"
|
||||
param_4_default={}
|
||||
param_5_default="@NONE@"
|
||||
|
|
|
|||
|
|
@ -82,6 +82,7 @@ class Const(object):
|
|||
ENTITY_ALL_RESOURCES = '@ALL_RESOURCES@'
|
||||
ENTITY_MAIN_RESOURCE = '@MAIN_RESOURCE@'
|
||||
ENTITY_CAP_HASH = 'CAP_HASH'
|
||||
ENTITY_TYPE = 'TYPE'
|
||||
|
||||
|
||||
## Roster jids selection ##
|
||||
|
|
@ -98,6 +99,11 @@ class Const(object):
|
|||
MESS_TYPE_GROUPCHAT = 'groupchat'
|
||||
MESS_TYPE_HEADLINE = 'headline'
|
||||
MESS_TYPE_NORMAL = 'normal'
|
||||
MESS_TYPE_AUTO = 'auto' # magic value to let the backend guess the type
|
||||
MESS_TYPE_STANDARD = (MESS_TYPE_CHAT, MESS_TYPE_ERROR, MESS_TYPE_GROUPCHAT, MESS_TYPE_HEADLINE, MESS_TYPE_NORMAL)
|
||||
MESS_TYPE_ALL = MESS_TYPE_STANDARD + (MESS_TYPE_INFO, MESS_TYPE_AUTO)
|
||||
|
||||
MESS_EXTRA_INFO = "info_type"
|
||||
|
||||
|
||||
## Presence ##
|
||||
|
|
@ -113,6 +119,7 @@ class Const(object):
|
|||
|
||||
|
||||
## Common namespaces ##
|
||||
NS_XML = 'http://www.w3.org/XML/1998/namespace'
|
||||
NS_CLIENT = 'jabber:client'
|
||||
NS_FORWARD = 'urn:xmpp:forward:0'
|
||||
NS_DELAY = 'urn:xmpp:delay'
|
||||
|
|
|
|||
|
|
@ -95,7 +95,7 @@ class SAT(service.Service):
|
|||
self.bridge.register("getParamsUI", self.memory.getParamsUI)
|
||||
self.bridge.register("getParamsCategories", self.memory.getParamsCategories)
|
||||
self.bridge.register("paramsRegisterApp", self.memory.paramsRegisterApp)
|
||||
self.bridge.register("historyGet", self.memory.historyGet)
|
||||
self.bridge.register("historyGet", self.memory._historyGet)
|
||||
self.bridge.register("setPresence", self._setPresence)
|
||||
self.bridge.register("subscription", self.subscription)
|
||||
self.bridge.register("addContact", self._addContact)
|
||||
|
|
@ -553,11 +553,11 @@ class SAT(service.Service):
|
|||
for lang, subject in data["subject"].iteritems():
|
||||
subject_elt = message_elt.addElement("subject", content=subject)
|
||||
if lang:
|
||||
subject_elt['xml:lang'] = lang
|
||||
subject_elt[(C.NS_XML, 'lang')] = lang
|
||||
for lang, message in data["message"].iteritems():
|
||||
body_elt = message_elt.addElement("body", content=message)
|
||||
if lang:
|
||||
body_elt['xml:lang'] = lang
|
||||
body_elt[(C.NS_XML, 'lang')] = lang
|
||||
try:
|
||||
thread = data['extra']['thread']
|
||||
except KeyError:
|
||||
|
|
@ -646,21 +646,21 @@ class SAT(service.Service):
|
|||
|
||||
pre_xml_treatments.addCallback(lambda dummy: self.generateMessageXML(data))
|
||||
pre_xml_treatments.chainDeferred(post_xml_treatments)
|
||||
post_xml_treatments.addCallback(self._sendMessageToStream, client)
|
||||
post_xml_treatments.addCallback(self.messageSendToStream, client)
|
||||
if send_only:
|
||||
log.debug(_("Triggers, storage and echo have been inhibited by the 'send_only' parameter"))
|
||||
else:
|
||||
post_xml_treatments.addCallback(self._storeMessage, client)
|
||||
post_xml_treatments.addCallback(self.sendMessageToBridge, client)
|
||||
post_xml_treatments.addCallback(self.messageAddToHistory, client)
|
||||
post_xml_treatments.addCallback(self.messageSendToBridge, client)
|
||||
post_xml_treatments.addErrback(self._cancelErrorTrap)
|
||||
pre_xml_treatments.callback(data)
|
||||
return pre_xml_treatments
|
||||
|
||||
def _cancelErrorTrap(failure):
|
||||
def _cancelErrorTrap(self, failure):
|
||||
"""A message sending can be cancelled by a plugin treatment"""
|
||||
failure.trap(exceptions.CancelError)
|
||||
|
||||
def _sendMessageToStream(self, data, client):
|
||||
def messageSendToStream(self, data, client):
|
||||
"""Actualy send the message to the server
|
||||
|
||||
@param data: message data dictionnary
|
||||
|
|
@ -669,7 +669,7 @@ class SAT(service.Service):
|
|||
client.xmlstream.send(data['xml'])
|
||||
return data
|
||||
|
||||
def _storeMessage(self, data, client):
|
||||
def messageAddToHistory(self, data, client):
|
||||
"""Store message into database (for local history)
|
||||
|
||||
@param data: message data dictionnary
|
||||
|
|
@ -678,13 +678,13 @@ class SAT(service.Service):
|
|||
if data["type"] != C.MESS_TYPE_GROUPCHAT:
|
||||
# we don't add groupchat message to history, as we get them back
|
||||
# and they will be added then
|
||||
if data['message']: # we need a message to save something
|
||||
if data['message'] or data['subject']: # we need a message to store
|
||||
self.memory.addToHistory(client, data)
|
||||
else:
|
||||
log.warning(u"No message found") # empty body should be managed by plugins before this point
|
||||
return data
|
||||
|
||||
def sendMessageToBridge(self, data, client):
|
||||
def messageSendToBridge(self, data, client):
|
||||
"""Send message to bridge, so frontends can display it
|
||||
|
||||
@param data: message data dictionnary
|
||||
|
|
@ -693,7 +693,7 @@ class SAT(service.Service):
|
|||
if data["type"] != C.MESS_TYPE_GROUPCHAT:
|
||||
# we don't send groupchat message to bridge, as we get them back
|
||||
# and they will be added the
|
||||
if data['message']: # we need a message to send something
|
||||
if data['message'] or data['subject']: # we need a message to send something
|
||||
# We send back the message, so all frontends are aware of it
|
||||
self.bridge.messageNew(data['uid'], data['timestamp'], data['from'].full(), data['to'].full(), data['message'], data['subject'], data['type'], data['extra'], profile=client.profile)
|
||||
else:
|
||||
|
|
@ -1039,6 +1039,7 @@ class SAT(service.Service):
|
|||
|
||||
def importMenu(self, path, callback, security_limit=C.NO_SECURITY_LIMIT, help_string="", type_=C.MENU_GLOBAL):
|
||||
"""register a new menu for frontends
|
||||
|
||||
@param path: path to go to the menu (category/subcategory/.../item), must be an iterable (e.g.: ("File", "Open"))
|
||||
/!\ use D_() instead of _() for translations (e.g. (D_("File"), D_("Open")))
|
||||
@param callback: method to be called when menuitem is selected, callable or a callback id (string) as returned by [registerCallback]
|
||||
|
|
|
|||
108
src/core/xmpp.py
108
src/core/xmpp.py
|
|
@ -25,7 +25,7 @@ from twisted.words.protocols.jabber import xmlstream
|
|||
from twisted.words.protocols.jabber import error
|
||||
from twisted.words.protocols.jabber import jid
|
||||
from twisted.python import failure
|
||||
from wokkel import client, disco, xmppim, generic, iwokkel
|
||||
from wokkel import client as wokkel_client, disco, xmppim, generic, iwokkel
|
||||
from wokkel import delay
|
||||
from sat.core.log import getLogger
|
||||
log = getLogger(__name__)
|
||||
|
|
@ -36,13 +36,13 @@ import calendar
|
|||
import uuid
|
||||
|
||||
|
||||
class SatXMPPClient(client.XMPPClient):
|
||||
class SatXMPPClient(wokkel_client.XMPPClient):
|
||||
implements(iwokkel.IDisco)
|
||||
|
||||
def __init__(self, host_app, profile, user_jid, password, host=None, port=C.XMPP_C2S_PORT, max_retries=C.XMPP_MAX_RETRIES):
|
||||
# XXX: DNS SRV records are checked when the host is not specified.
|
||||
# If no SRV record is found, the host is directly extracted from the JID.
|
||||
client.XMPPClient.__init__(self, user_jid, password, host or None, port or C.XMPP_C2S_PORT)
|
||||
wokkel_client.XMPPClient.__init__(self, user_jid, password, host or None, port or C.XMPP_C2S_PORT)
|
||||
self.factory.clientConnectionLost = self.connectionLost
|
||||
self.factory.maxRetries = max_retries
|
||||
self.__connected = False
|
||||
|
|
@ -80,7 +80,7 @@ class SatXMPPClient(client.XMPPClient):
|
|||
def _authd(self, xmlstream):
|
||||
if not self.host_app.trigger.point("XML Initialized", xmlstream, self.profile):
|
||||
return
|
||||
client.XMPPClient._authd(self, xmlstream)
|
||||
wokkel_client.XMPPClient._authd(self, xmlstream)
|
||||
self.__connected = True
|
||||
log.info(_("********** [%s] CONNECTED **********") % self.profile)
|
||||
self.streamInitialized()
|
||||
|
|
@ -112,7 +112,7 @@ class SatXMPPClient(client.XMPPClient):
|
|||
log.error(_(u"ERROR: XMPP connection failed for profile '%(profile)s': %(reason)s" % {'profile': self.profile, 'reason': reason}))
|
||||
self.conn_deferred.errback(reason.value)
|
||||
try:
|
||||
client.XMPPClient.initializationFailed(self, reason)
|
||||
wokkel_client.XMPPClient.initializationFailed(self, reason)
|
||||
except:
|
||||
# we already chained an errback, no need to raise an exception
|
||||
pass
|
||||
|
|
@ -152,8 +152,8 @@ class SatMessageProtocol(xmppim.MessageProtocol):
|
|||
message = {}
|
||||
subject = {}
|
||||
extra = {}
|
||||
data = {"from": message_elt['from'],
|
||||
"to": message_elt['to'],
|
||||
data = {"from": jid.JID(message_elt['from']),
|
||||
"to": jid.JID(message_elt['to']),
|
||||
"uid": message_elt.getAttribute('uid', unicode(uuid.uuid4())), # XXX: uid is not a standard attribute but may be added by plugins
|
||||
"message": message,
|
||||
"subject": subject,
|
||||
|
|
@ -169,11 +169,11 @@ class SatMessageProtocol(xmppim.MessageProtocol):
|
|||
|
||||
# message
|
||||
for e in message_elt.elements(C.NS_CLIENT, 'body'):
|
||||
message[e.getAttribute('xml:lang','')] = unicode(e)
|
||||
message[e.getAttribute((C.NS_XML,'lang'),'')] = unicode(e)
|
||||
|
||||
# subject
|
||||
for e in message_elt.elements(C.NS_CLIENT, 'subject'):
|
||||
subject[e.getAttribute('xml:lang','')] = unicode(e)
|
||||
subject[e.getAttribute((C.NS_XML, 'lang'),'')] = unicode(e)
|
||||
|
||||
# delay and timestamp
|
||||
try:
|
||||
|
|
@ -187,42 +187,39 @@ class SatMessageProtocol(xmppim.MessageProtocol):
|
|||
if parsed_delay.sender:
|
||||
data['delay_sender'] = parsed_delay.sender.full()
|
||||
|
||||
def skipEmptyMessage(data):
|
||||
if not data['message'] and not data['extra']:
|
||||
raise failure.Failure(exceptions.CancelError())
|
||||
return data
|
||||
|
||||
def bridgeSignal(data):
|
||||
try:
|
||||
data['extra']['received_timestamp'] = data['received_timestamp']
|
||||
data['extra']['delay_sender'] = data['delay_sender']
|
||||
except KeyError:
|
||||
pass
|
||||
if data is not None:
|
||||
self.host.bridge.messageNew(data['uid'], data['timestamp'], data['from'].full(), data['to'].full(), data['message'], data['subject'], data['type'], data['extra'], profile=client.profile)
|
||||
return data
|
||||
|
||||
def addToHistory(data):
|
||||
data['from'] = jid.JID(data['from'])
|
||||
data['to'] = jid.JID(data['to'])
|
||||
self.host.memory.addToHistory(client, data)
|
||||
return data
|
||||
|
||||
def treatmentsEb(failure_):
|
||||
failure_.trap(exceptions.SkipHistory)
|
||||
return data
|
||||
|
||||
def cancelErrorTrap(failure_):
|
||||
"""A message sending can be cancelled by a plugin treatment"""
|
||||
failure_.trap(exceptions.CancelError)
|
||||
|
||||
post_treat.addCallback(skipEmptyMessage)
|
||||
post_treat.addCallback(addToHistory)
|
||||
post_treat.addErrback(treatmentsEb)
|
||||
post_treat.addCallback(bridgeSignal)
|
||||
post_treat.addErrback(cancelErrorTrap)
|
||||
post_treat.addCallback(self.skipEmptyMessage)
|
||||
post_treat.addCallback(self.addToHistory, client)
|
||||
post_treat.addErrback(self.treatmentsEb)
|
||||
post_treat.addCallback(self.bridgeSignal, client, data)
|
||||
post_treat.addErrback(self.cancelErrorTrap)
|
||||
post_treat.callback(data)
|
||||
|
||||
def skipEmptyMessage(self, data):
|
||||
if not data['message'] and not data['extra'] and not data['subject']:
|
||||
raise failure.Failure(exceptions.CancelError("Cancelled empty message"))
|
||||
return data
|
||||
|
||||
def addToHistory(self, data, client):
|
||||
return self.host.memory.addToHistory(client, data)
|
||||
|
||||
def treatmentsEb(self, failure_):
|
||||
failure_.trap(exceptions.SkipHistory)
|
||||
|
||||
def bridgeSignal(self, dummy, client, data):
|
||||
try:
|
||||
data['extra']['received_timestamp'] = data['received_timestamp']
|
||||
data['extra']['delay_sender'] = data['delay_sender']
|
||||
except KeyError:
|
||||
pass
|
||||
if data is not None:
|
||||
self.host.bridge.messageNew(data['uid'], data['timestamp'], data['from'].full(), data['to'].full(), data['message'], data['subject'], data['type'], data['extra'], profile=client.profile)
|
||||
return data
|
||||
|
||||
def cancelErrorTrap(self, failure_):
|
||||
"""A message sending can be cancelled by a plugin treatment"""
|
||||
failure_.trap(exceptions.CancelError)
|
||||
|
||||
|
||||
class SatRosterProtocol(xmppim.RosterClientProtocol):
|
||||
|
||||
|
|
@ -283,8 +280,10 @@ class SatRosterProtocol(xmppim.RosterClientProtocol):
|
|||
|
||||
def getAttributes(self, item):
|
||||
"""Return dictionary of attributes as used in bridge from a RosterItem
|
||||
|
||||
@param item: RosterItem
|
||||
@return: dictionary of attributes"""
|
||||
@return: dictionary of attributes
|
||||
"""
|
||||
item_attr = {'to': unicode(item.subscriptionTo),
|
||||
'from': unicode(item.subscriptionFrom),
|
||||
'ask': unicode(item.ask)
|
||||
|
|
@ -339,8 +338,9 @@ class SatRosterProtocol(xmppim.RosterClientProtocol):
|
|||
def getItem(self, entity_jid):
|
||||
"""Return RosterItem for a given jid
|
||||
|
||||
@param entity_jid: jid of the contact
|
||||
@return: RosterItem or None if contact is not in cache
|
||||
@param entity_jid(jid.JID): jid of the contact
|
||||
@return(RosterItem, None): RosterItem instance
|
||||
None if contact is not in cache
|
||||
"""
|
||||
return self._jids.get(entity_jid, None)
|
||||
|
||||
|
|
@ -384,6 +384,18 @@ class SatRosterProtocol(xmppim.RosterClientProtocol):
|
|||
else:
|
||||
raise ValueError(u'Unexpected type_ {}'.format(type_))
|
||||
|
||||
def getNick(self, entity_jid):
|
||||
"""Return a nick name for an entity
|
||||
|
||||
return nick choosed by user if available
|
||||
else return user part of entity_jid
|
||||
"""
|
||||
item = self.getItem(entity_jid)
|
||||
if item is None:
|
||||
return entity_jid.user
|
||||
else:
|
||||
return item.name or entity_jid.user
|
||||
|
||||
|
||||
class SatPresenceProtocol(xmppim.PresenceClientProtocol):
|
||||
|
||||
|
|
@ -474,7 +486,9 @@ class SatPresenceProtocol(xmppim.PresenceClientProtocol):
|
|||
return
|
||||
self.send(presence_elt)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def subscribed(self, entity):
|
||||
yield self.parent.roster.got_roster
|
||||
xmppim.PresenceClientProtocol.subscribed(self, entity)
|
||||
self.host.memory.delWaitingSub(entity.userhost(), self.parent.profile)
|
||||
item = self.parent.roster.getItem(entity)
|
||||
|
|
@ -494,8 +508,10 @@ class SatPresenceProtocol(xmppim.PresenceClientProtocol):
|
|||
log.debug(_(u"unsubscription confirmed for [%s]") % entity.userhost())
|
||||
self.host.bridge.subscribe('unsubscribed', entity.userhost(), self.parent.profile)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def subscribeReceived(self, entity):
|
||||
log.debug(_(u"subscription request from [%s]") % entity.userhost())
|
||||
yield self.parent.roster.got_roster
|
||||
item = self.parent.roster.getItem(entity)
|
||||
if item and item.subscriptionTo:
|
||||
# We automatically accept subscription if we are already subscribed to contact presence
|
||||
|
|
@ -505,8 +521,10 @@ class SatPresenceProtocol(xmppim.PresenceClientProtocol):
|
|||
self.host.memory.addWaitingSub('subscribe', entity.userhost(), self.parent.profile)
|
||||
self.host.bridge.subscribe('subscribe', entity.userhost(), self.parent.profile)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def unsubscribeReceived(self, entity):
|
||||
log.debug(_(u"unsubscription asked for [%s]") % entity.userhost())
|
||||
yield self.parent.roster.got_roster
|
||||
item = self.parent.roster.getItem(entity)
|
||||
if item and item.subscriptionFrom: # we automatically remove contact
|
||||
log.debug(_('automatic contact deletion'))
|
||||
|
|
|
|||
|
|
@ -513,6 +513,9 @@ class Memory(object):
|
|||
def addToHistory(self, client, data):
|
||||
return self.storage.addToHistory(data, client.profile)
|
||||
|
||||
def _historyGet(self, from_jid_s, to_jid_s, limit=C.HISTORY_LIMIT_NONE, between=True, search=None, profile=C.PROF_KEY_NONE):
|
||||
return self.historyGet(jid.JID(from_jid_s), jid.JID(to_jid_s), limit, between, search, profile)
|
||||
|
||||
def historyGet(self, from_jid, to_jid, limit=C.HISTORY_LIMIT_NONE, between=True, search=None, profile=C.PROF_KEY_NONE):
|
||||
"""Retrieve messages in history
|
||||
|
||||
|
|
@ -525,7 +528,7 @@ class Memory(object):
|
|||
@param between (bool): confound source and dest (ignore the direction)
|
||||
@param search (str): pattern to filter the history results
|
||||
@param profile (str): %(doc_profile)s
|
||||
@return: list of message data as in [messageNew]
|
||||
@return (D(list)): list of message data as in [messageNew]
|
||||
"""
|
||||
assert profile != C.PROF_KEY_NONE
|
||||
if limit == C.HISTORY_LIMIT_DEFAULT:
|
||||
|
|
@ -534,7 +537,7 @@ class Memory(object):
|
|||
limit = None
|
||||
if limit == 0:
|
||||
return defer.succeed([])
|
||||
return self.storage.historyGet(jid.JID(from_jid), jid.JID(to_jid), limit, between, search, profile)
|
||||
return self.storage.historyGet(from_jid, to_jid, limit, between, search, profile)
|
||||
|
||||
## Statuses ##
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue