summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/core.py2752
1 files changed, 1401 insertions, 1351 deletions
diff --git a/src/core.py b/src/core.py
index 9159bfe0..029179de 100644
--- a/src/core.py
+++ b/src/core.py
@@ -313,44 +313,6 @@ class Core(object):
self.plugin_manager.load(plugin)
self.plugins_autoloaded = True
- def validate_ssl(self, pem):
- """
- Check the server certificate using the sleekxmpp ssl_cert event
- """
- if config.get('ignore_certificate', 'false').lower() == 'true':
- return
- cert = config.get('certificate', '')
- der = ssl.PEM_cert_to_DER_cert(pem)
- found_cert = sha1(der).hexdigest()
- if cert:
- if found_cert == cert:
- log.debug('Cert %s OK', found_cert)
- return
- else:
- saved_input = self.current_tab().input
- log.debug('\nWARNING: CERTIFICATE CHANGED old: %s, new: %s\n', cert, found_cert)
- input = windows.YesNoInput(text="WARNING! Certificate hash changed to %s. Accept? (y/n)" % found_cert)
- self.current_tab().input = input
- input.resize(1, self.current_tab().width, self.current_tab().height-1, 0)
- input.refresh()
- self.doupdate()
- self.paused = True
- while input.value is None:
- pass
- self.current_tab().input = saved_input
- self.paused = False
- if input.value:
- self.information('Setting new certificate: old: %s, new: %s' % (cert, found_cert), 'Info')
- log.debug('Setting certificate to %s', found_cert)
- config.set_and_save('certificate', found_cert)
- else:
- self.information('You refused to validate the certificate. You are now disconnected', 'Info')
- self.xmpp.disconnect()
- else:
- log.debug('First time. Setting certificate to %s', found_cert)
- config.set_and_save('certificate', found_cert)
-
-
def start(self):
"""
Init curses, create the first tab, etc
@@ -371,130 +333,202 @@ class Core(object):
))
self.refresh_window()
- def resize_global_information_win(self):
+ def on_exception(self, typ, value, trace):
"""
- Resize the global_information_win only once at each resize.
+ When an exception is raised, just reset curses and call
+ the original exception handler (will nicely print the traceback)
"""
- with g_lock:
- self.information_win.resize(self.information_win_size, tabs.Tab.width,
- tabs.Tab.height - 1 - self.information_win_size - tabs.Tab.tab_win_height(), 0)
+ try:
+ self.reset_curses()
+ except:
+ pass
+ sys.__excepthook__(typ, value, trace)
- def outgoing_stanza(self, stanza):
+ def main_loop(self):
"""
- We are sending a new stanza, write it in the xml buffer if needed.
+ main loop waiting for the user to press a key
"""
- if self.xml_tabs:
- self.add_message_to_text_buffer(self.xml_buffer, '\x191}<--\x19o %s' % stanza)
- if isinstance(self.current_tab(), tabs.XMLTab):
- self.current_tab().refresh()
- self.doupdate()
+ def replace_line_breaks(key):
+ if key == '^J':
+ return '\n'
+ return key
+ def replace_tabulation(key):
+ if key == '^I':
+ return ' '
+ return key
+ while self.running:
+ if self.paused: continue
+ char_list = [common.replace_key_with_bound(key)\
+ for key in self.read_keyboard()]
+ if self.paused:
+ self.current_tab().input.do_command(char_list[0])
+ self.current_tab().input.prompt()
+ continue
+ # Special case for M-x where x is a number
+ if len(char_list) == 1:
+ char = char_list[0]
+ if char.startswith('M-') and len(char) == 3:
+ try:
+ nb = int(char[2])
+ except ValueError:
+ pass
+ else:
+ if self.current_tab().nb == nb:
+ self.go_to_previous_tab()
+ else:
+ self.command_win('%d' % nb)
+ # search for keyboard shortcut
+ func = self.key_func.get(char, None)
+ if func:
+ func()
+ else:
+ res = self.do_command(replace_line_breaks(char), False)
+ if res:
+ self.refresh_window()
+ else:
+ self.do_command(''.join(map(
+ lambda x: replace_line_breaks(replace_tabulation(x)),
+ char_list)
+ ), True)
+ self.refresh_window()
+ self.doupdate()
- def incoming_stanza(self, stanza):
+ def save_config(self):
"""
- We are receiving a new stanza, write it in the xml buffer if needed.
+ Save config in the file just before exit
"""
- if self.xml_tabs:
- self.add_message_to_text_buffer(self.xml_buffer, '\x192}-->\x19o %s' % stanza)
- if isinstance(self.current_tab(), tabs.XMLTab):
- self.current_tab().refresh()
- self.doupdate()
-
- def command_xml_tab(self, arg=''):
- """/xml_tab"""
- self.xml_tabs += 1
- tab = tabs.XMLTab()
- self.add_tab(tab, True)
+ roster.save_to_config_file()
+ config.set_and_save('info_win_height', self.information_win_size, 'var')
- def resize_global_info_bar(self):
+ def on_roster_enter_key(self, roster_row):
"""
- Resize the GlobalInfoBar only once at each resize
+ when enter is pressed on the roster window
"""
- with g_lock:
- self.tab_win.resize(1, tabs.Tab.width, tabs.Tab.height - 2, 0)
- if config.get('enable_vertical_tab_list', 'false') == 'true':
- height, width = self.stdscr.getmaxyx()
- truncated_win = self.stdscr.subwin(height, config.get('vertical_tab_list_size', 20), 0, 0)
- self.left_tab_win = windows.VerticalGlobalInfoBar(truncated_win)
+ if isinstance(roster_row, Contact):
+ if not self.get_conversation_by_jid(roster_row.bare_jid):
+ self.open_conversation_window(roster_row.bare_jid)
else:
- self.left_tab_win = None
+ self.focus_tab_named(roster_row.bare_jid)
+ if isinstance(roster_row, Resource):
+ if not self.get_conversation_by_jid(roster_row.jid.full):
+ self.open_conversation_window(roster_row.jid.full)
+ else:
+ self.focus_tab_named(roster_row.jid.full)
+ self.refresh_window()
- def on_exception(self, typ, value, trace):
+ def get_conversation_messages(self):
"""
- When an exception is raised, just reset curses and call
- the original exception handler (will nicely print the traceback)
+ Returns a list of all the messages in the current chat.
+ If the current tab is not a ChatTab, returns None.
+
+ Messages are namedtuples of the form
+ ('txt nick_color time str_time nickname user')
"""
- try:
- self.reset_curses()
- except:
- pass
- sys.__excepthook__(typ, value, trace)
+ if not isinstance(self.current_tab(), tabs.ChatTab):
+ return None
+ return self.current_tab().get_conversation_messages()
- @property
- def informations(self):
- return self.information_buffer
+ def insert_input_text(self, text):
+ """
+ Insert the given text into the current input
+ """
+ self.do_command(text, True)
- def grow_information_win(self, nb=1):
- if self.information_win_size >= self.current_tab().height -5 or \
- self.information_win_size+nb >= self.current_tab().height-4:
- return
- if self.information_win_size == 14:
- return
- self.information_win_size += nb
- if self.information_win_size > 14:
- self.information_win_size = 14
- self.resize_global_information_win()
- for tab in self.tabs:
- tab.on_info_win_size_changed()
- self.refresh_window()
- def shrink_information_win(self, nb=1):
- if self.information_win_size == 0:
+##################### Anything related to command execution ###################
+
+ def execute(self, line):
+ """
+ Execute the /command or just send the line on the current room
+ """
+ if line == "":
return
- self.information_win_size -= nb
- if self.information_win_size < 0:
- self.information_win_size = 0
- self.resize_global_information_win()
- for tab in self.tabs:
- tab.on_info_win_size_changed()
- self.refresh_window()
+ if line.startswith('/'):
+ command = line.strip()[:].split()[0][1:]
+ arg = line[2+len(command):] # jump the '/' and the ' '
+ # example. on "/link 0 open", command = "link" and arg = "0 open"
+ if command in self.commands:
+ func = self.commands[command][0]
+ func(arg)
+ return
+ else:
+ self.information(_("Unknown command (%s)") % (command), _('Error'))
- def scroll_info_up(self):
- self.information_win.scroll_up(self.information_win.height)
- if not isinstance(self.current_tab(), tabs.RosterInfoTab):
- self.information_win.refresh()
+ def exec_command(self, command):
+ """
+ Execute an external command on the local or a remote
+ machine, depending on the conf. For example, to open a link in a
+ browser, do exec_command("firefox http://poezio.eu"),
+ and this will call the command on the correct computer.
+ The remote execution is done by writing the command on a fifo.
+ That fifo has to be on the machine where poezio is running, and
+ accessible (through sshfs for example) from the local machine (where
+ poezio is not running). A very simple daemon reads on that fifo,
+ and executes any command that is read in it.
+ """
+ command = '%s\n' % (command,)
+ if config.get('exec_remote', 'false') == 'true':
+ # We just write the command in the fifo
+ if not self.remote_fifo:
+ try:
+ self.remote_fifo = Fifo(os.path.join(config.get('remote_fifo_path', './'), 'poezio.fifo'), 'w')
+ except (OSError, IOError) as e:
+ self.information('Could not open fifo file for writing: %s' % (e,), 'Error')
+ return
+ try:
+ self.remote_fifo.write(command)
+ except (IOError) as e:
+ self.information('Could not execute [%s]: %s' % (command.strip(), e,), 'Error')
+ self.remote_fifo = None
else:
- info = self.current_tab().information_win
- info.scroll_up(info.height)
- self.refresh_window()
+ e = Executor(command.strip())
+ try:
+ e.start()
+ except ValueError as e: # whenever shlex fails
+ self.information('%s' % (e,), 'Error')
- def scroll_info_down(self):
- self.information_win.scroll_down(self.information_win.height)
- if not isinstance(self.current_tab(), tabs.RosterInfoTab):
- self.information_win.refresh()
- else:
- info = self.current_tab().information_win
- info.scroll_down(info.height)
- self.refresh_window()
- def pop_information_win_up(self, size, time):
- """
- Temporarly increase the size of the information win of size lines
- during time seconds.
- After that delay, the size will decrease from size lines.
- """
- if time <= 0 or size <= 0:
+ def do_command(self, key, raw):
+ if not key:
return
- self.grow_information_win(size)
- timed_event = timed_events.DelayedEvent(time, self.shrink_information_win, size)
- self.add_timed_event(timed_event)
+ return self.current_tab().on_input(key, raw)
- def toggle_left_pane(self):
+
+ def try_execute(self, line):
"""
- Enable/disable the left panel.
+ Try to execute a command in the current tab
"""
- enabled = config.get('enable_vertical_tab_list', 'false')
- config.set('enable_vertical_tab_list', 'false' if enabled == 'true' else 'true')
- self.call_for_resize()
+ line = '/' + line
+ try:
+ self.current_tab().execute_command(line)
+ except:
+ import traceback
+ log.debug('Execute failed:\n%s', traceback.format_exc())
+
+
+########################## TImed Events #######################################
+
+ def remove_timed_event(self, event):
+ """Remove an existing timed event"""
+ if event and event in self.timed_events:
+ self.timed_events.remove(event)
+
+ def add_timed_event(self, event):
+ """Add a new timed event"""
+ self.timed_events.add(event)
+
+ def check_timed_events(self):
+ """Check for the execution of timed events"""
+ now = datetime.now()
+ for event in self.timed_events:
+ if event.has_timed_out(now):
+ res = event()
+ if not res:
+ self.timed_events.remove(event)
+ break
+
+
+####################### XMPP-related actions ##################################
def get_status(self):
"""
@@ -510,302 +544,262 @@ class Core(object):
"""
self.status = Status(show=pres, message=msg)
- def on_groupchat_invite(self, message):
- jid = message['from']
- if jid.bare in self.pending_invites:
- return
- # there are 2 'x' tags in the messages, making message['x'] useless
- invite = StanzaBase(self.xmpp, xml=message.find('{http://jabber.org/protocol/muc#user}x/{http://jabber.org/protocol/muc#user}invite'))
- inviter = invite['from']
- reason = invite['reason']
- password = invite['password']
- msg = "You are invited to the room %s by %s" % (jid.full, inviter.full)
- if reason:
- msg += "because: %s" % reason
- if password:
- msg += ". The password is \"%s\"." % password
- self.information(msg, 'Info')
- if 'invite' in config.get('beep_on', 'invite').split():
- curses.beep()
- self.pending_invites[jid.bare] = inviter.full
-
- def command_invite(self, arg):
- """/invite <to> <room> [reason]"""
- args = common.shell_split(arg)
- if len(args) < 2:
- return
- reason = args[2] if len(args) > 2 else ''
- to = args[0]
- room = args[1]
- self.xmpp.plugin['xep_0045'].invite(room, to, reason)
-
- def completion_invite(self, the_input):
- """Completion for /invite"""
- txt = the_input.get_text()
- args = common.shell_split(txt)
- n = len(args)
- if txt.endswith(' '):
- n += 1
- if n == 2:
- return the_input.auto_completion([jid for jid in roster.jids()], '')
- elif n == 3:
- rooms = []
- for tab in self.tabs:
- if isinstance(tab, tabs.MucTab) and tab.joined:
- rooms.append(tab.get_name())
- return the_input.auto_completion(rooms, '')
-
- def command_decline(self, arg):
- """/decline <room@server.tld> [reason]"""
- args = common.shell_split(arg)
- if not len(args):
- return
- jid = JID(args[0])
- if jid.bare not in self.pending_invites:
- return
- reason = args[1] if len(args) > 1 else ''
- del self.pending_invites[jid.bare]
- self.xmpp.plugin['xep_0045'].decline_invite(jid.bare, self.pending_invites[jid.bare], reason)
-
- def completion_decline(self, the_input):
- """Completion for /decline"""
- txt = the_input.get_text()
- args = common.shell_split(txt)
- n = len(args)
- if txt.endswith(' '):
- n += 1
- if n == 2:
- return the_input.auto_completion(list(self.pending_invites.keys()), '')
-
- def command_invitations(self, arg=''):
- """/invitations"""
- build = ""
- for invite in self.pending_invites:
- build += "%s by %s" % (invite, JID(self.pending_invites[invite]).bare)
- if self.pending_invites:
- build = "You are invited to the following rooms:\n" + build
- else:
- build = "You are do not have any pending invitation."
- self.information(build, 'Info')
-
- def on_groupchat_decline(self, decline):
- pass
-
- def on_data_form(self, message):
+ def get_bookmark_nickname(self, room_name):
"""
- When a data form is received
+ Returns the nickname associated with a bookmark
+ or the default nickname
"""
- self.information('%s' % message)
+ bm = bookmark.get_by_jid(room_name)
+ if bm:
+ return bm.nick
+ return self.own_nick
- def on_chatstate_active(self, message):
- self.on_chatstate(message, "active")
+ def disconnect(self, msg='', reconnect=False):
+ """
+ Disconnect from remote server and correctly set the states of all
+ parts of the client (for example, set the MucTabs as not joined, etc)
+ """
+ msg = msg or ''
+ for tab in self.tabs:
+ if isinstance(tab, tabs.MucTab) and tab.joined:
+ tab.command_part(msg)
+ self.save_config()
+ # Ugly fix thanks to gmail servers
+ self.xmpp.disconnect(reconnect)
- def on_chatstate_inactive(self, message):
- self.on_chatstate(message, "inactive")
+ def send_message(self, msg):
+ """
+ Function to use in plugins to send a message in the current conversation.
+ Returns False if the current tab is not a conversation tab
+ """
+ if not isinstance(self.current_tab(), tabs.ChatTab):
+ return False
+ self.current_tab().command_say(msg)
+ return True
- def on_chatstate_composing(self, message):
- self.on_chatstate(message, "composing")
+ def get_error_message(self, stanza, deprecated=False):
+ """
+ Takes a stanza of the form <message type='error'><error/></message>
+ and return a well formed string containing the error informations
+ """
+ sender = stanza.attrib['from']
+ msg = stanza['error']['type']
+ condition = stanza['error']['condition']
+ code = stanza['error']['code']
+ body = stanza['error']['text']
+ if not body:
+ if deprecated:
+ if code in DEPRECATED_ERRORS:
+ body = DEPRECATED_ERRORS[code]
+ else:
+ body = condition or _('Unknown error')
+ else:
+ if code in ERROR_AND_STATUS_CODES:
+ body = ERROR_AND_STATUS_CODES[code]
+ else:
+ body = condition or _('Unknown error')
+ if code:
+ message = _('%(from)s: %(code)s - %(msg)s: %(body)s') % {'from':sender, 'msg':msg, 'body':body, 'code':code}
+ else:
+ message = _('%(from)s: %(msg)s: %(body)s') % {'from':sender, 'msg':msg, 'body':body}
+ return message
- def on_chatstate_paused(self, message):
- self.on_chatstate(message, "paused")
- def on_chatstate_gone(self, message):
- self.on_chatstate(message, "gone")
+####################### Tab logic-related things ##############################
- def on_chatstate(self, message, state):
- if message['type'] == 'chat':
- if not self.on_chatstate_normal_conversation(message, state):
- tab = self.get_tab_by_name(message['from'].full, tabs.PrivateTab)
- if not tab:
- return
- self.on_chatstate_private_conversation(message, state)
- elif message['type'] == 'groupchat':
- self.on_chatstate_groupchat_conversation(message, state)
+ ### Tab getters ###
- def on_chatstate_normal_conversation(self, message, state):
- tab = self.get_tab_of_conversation_with_jid(message['from'], False)
- if not tab:
- return False
- self.events.trigger('normal_chatstate', message, tab)
- tab.chatstate = state
- if tab == self.current_tab():
- tab.refresh_info_header()
- self.doupdate()
- return True
+ def current_tab(self):
+ """
+ returns the current room, the one we are viewing
+ """
+ return self.tabs[0]
- def on_chatstate_private_conversation(self, message, state):
+ def get_tab_of_conversation_with_jid(self, jid, create=True):
"""
- Chatstate received in a private conversation from a MUC
+ From a JID, get the tab containing the conversation with it.
+ If none already exist, and create is "True", we create it
+ and return it. Otherwise, we return None
"""
- tab = self.get_tab_by_name(message['from'].full, tabs.PrivateTab)
- if not tab:
- return
- self.events.trigger('private_chatstate', message, tab)
- tab.chatstate = state
- if tab == self.current_tab():
- tab.refresh_info_header()
- self.doupdate()
- return True
+ # We first check if we have a conversation opened with this precise resource
+ conversation = self.get_tab_by_name(jid.full, tabs.ConversationTab)
+ if not conversation:
+ # If not, we search for a conversation with the bare jid
+ conversation = self.get_tab_by_name(jid.bare, tabs.ConversationTab)
+ if not conversation:
+ if create:
+ # We create the conversation with the full Jid if nothing was found
+ conversation = self.open_conversation_window(jid.full, False)
+ else:
+ conversation = None
+ return conversation
- def on_chatstate_groupchat_conversation(self, message, state):
+ def get_conversation_by_jid(self, jid):
"""
- Chatstate received in a MUC
+ Return the room of the ConversationTab with the given jid
"""
- nick = message['mucnick']
- room_from = message.getMucroom()
- tab = self.get_tab_by_name(room_from, tabs.MucTab)
- if tab and tab.get_user_by_name(nick):
- self.events.trigger('muc_chatstate', message, tab)
- tab.get_user_by_name(nick).chatstate = state
- if tab == self.current_tab():
- tab.user_win.refresh(tab.users)
- tab.input.refresh()
- self.doupdate()
+ for tab in self.tabs:
+ if isinstance(tab, tabs.ConversationTab):
+ if tab.get_name() == jid:
+ return tab
+ return None
- def on_attention(self, message):
+ def get_tab_by_name(self, name, typ=None):
"""
- Attention probe received.
+ Get the tab with the given name.
+ If typ is provided, return a tab of this type only
"""
- jid_from = message['from']
- self.information('%s requests your attention!' % jid_from, 'Info')
for tab in self.tabs:
- if tab.get_name() == jid_from:
- tab.state = 'attention'
- self.refresh_tab_win()
- return
+ if tab.get_name() == name:
+ if (typ and isinstance(tab, typ)) or\
+ not typ:
+ return tab
+ return None
+
+ def get_tab_by_number(self, number):
for tab in self.tabs:
- if tab.get_name() == jid_from.bare:
- tab.state = 'attention'
- self.refresh_tab_win()
- return
- self.information('%s tab not found.' % jid_from, 'Error')
+ if tab.nb == number:
+ return tab
+ return None
- def open_new_form(self, form, on_cancel, on_send, **kwargs):
+ def add_tab(self, new_tab, focus=False):
"""
- Open a new tab containing the form
- The callback are called with the completed form as parameter in
- addition with kwargs
+ Appends the new_tab in the tab list and
+ focus it if focus==True
"""
- form_tab = DataFormsTab(form, on_cancel, on_send, kwargs)
- self.add_tab(form_tab, True)
+ if self.current_tab().nb == 0:
+ self.tabs.append(new_tab)
+ else:
+ for ta in self.tabs:
+ if ta.nb == 0:
+ self.tabs.insert(self.tabs.index(ta), new_tab)
+ break
+ if focus:
+ self.command_win("%s" % new_tab.nb)
- def on_got_offline(self, presence):
+ ### Move actions (e.g. go to next room) ###
+
+ def rotate_rooms_right(self, args=None):
"""
- A JID got offline
+ rotate the rooms list to the right
"""
- jid = presence['from']
- logger.log_roster_change(jid.bare, 'got offline')
- # If a resource got offline, display the message in the conversation with this
- # precise resource.
- if jid.resource:
- self.add_information_message_to_conversation_tab(jid.full, '\x195}%s is \x191}offline' % (jid.full))
- if jid.server in roster.blacklist:
- return
- self.add_information_message_to_conversation_tab(jid.bare, '\x195}%s is \x191}offline' % (jid.bare))
- self.information('\x193}%s \x195}is \x191}offline' % (jid.bare), 'Roster')
- if isinstance(self.current_tab(), tabs.RosterInfoTab):
- self.refresh_window()
-
- def on_got_online(self, presence):
- jid = presence['from']
- contact = roster[jid.bare]
- if contact is None:
- # Todo, handle presence coming from contacts not in roster
- return
- logger.log_roster_change(jid.bare, 'got online')
- resource = Resource(jid.full, {
- 'priority': presence.get_priority() or 0,
- 'status': presence['status'],
- 'show': presence['show'],
- })
- self.events.trigger('normal_presence', presence, resource)
- self.add_information_message_to_conversation_tab(jid.full, '\x195}%s is \x194}online' % (jid.full))
- if time.time() - self.connection_time > 20:
- # We do not display messages if we recently logged in
- if presence['status']:
- self.information("\x193}%s \x195}is \x194}online\x195} (\x19o%s\x195})" % (resource.jid.bare, presence['status']), "Roster")
- else:
- self.information("\x193}%s \x195}is \x194}online\x195}" % resource.jid.bare, "Roster")
- self.add_information_message_to_conversation_tab(jid.bare, '\x195}%s is \x194}online' % (jid.bare))
- if isinstance(self.current_tab(), tabs.RosterInfoTab):
- self.refresh_window()
+ self.current_tab().on_lose_focus()
+ self.tabs.append(self.tabs.pop(0))
+ self.current_tab().on_gain_focus()
+ self.refresh_window()
- def add_information_message_to_conversation_tab(self, jid, msg):
+ def rotate_rooms_left(self, args=None):
"""
- Search for a ConversationTab with the given jid (full or bare), if yes, add
- the given message to it
+ rotate the rooms list to the right
"""
- tab = self.get_tab_by_name(jid, tabs.ConversationTab)
- if tab:
- self.add_message_to_text_buffer(tab._text_buffer, msg)
+ self.current_tab().on_lose_focus()
+ self.tabs.insert(0, self.tabs.pop())
+ self.current_tab().on_gain_focus()
+ self.refresh_window()
- def on_failed_connection(self):
+ def go_to_room_number(self):
"""
- We cannot contact the remote server
+ Read 2 more chars and go to the tab
+ with the given number
"""
- self.information(_("Connection to remote server failed"))
+ char = self.read_keyboard()[0]
+ try:
+ nb1 = int(char)
+ except ValueError:
+ return
+ char = self.read_keyboard()[0]
+ try:
+ nb2 = int(char)
+ except ValueError:
+ return
+ self.command_win('%s%s' % (nb1, nb2))
- def on_disconnected(self, event):
+ def go_to_roster(self):
+ self.command_win('0')
+
+ def go_to_previous_tab(self):
+ self.command_win('%s' % (self.previous_tab_nb,))
+
+ def go_to_important_room(self):
"""
- When we are disconnected from remote server
+ Go to the next room with activity, in the order defined in the
+ dict tabs.STATE_PRIORITY
"""
+ priority = tabs.STATE_PRIORITY
+ sorted_tabs = sorted(self.tabs, key=lambda tab: priority[tab.state],
+ reverse=True)
+ tab = sorted_tabs.pop(0) if sorted_tabs else None
+ if priority[tab.state] < 0 or not tab:
+ return
+ self.command_win('%s' % tab.nb)
+
+ def focus_tab_named(self, tab_name):
for tab in self.tabs:
- if isinstance(tab, tabs.MucTab):
- tab.disconnect()
- self.information(_("Disconnected from server."))
+ if tab.get_name() == tab_name:
+ self.command_win('%s' % (tab.nb,))
- def on_failed_auth(self, event):
+ ### Opening actions ###
+
+ def open_conversation_window(self, jid, focus=True):
"""
- Authentication failed
+ Open a new conversation tab and focus it if needed
"""
- self.information(_("Authentication failed."))
+ for tab in self.tabs: # if the room exists, focus it and return
+ if isinstance(tab, tabs.ConversationTab):
+ if tab.get_name() == jid:
+ self.command_win('%s' % tab.nb)
+ return tab
+ new_tab = tabs.ConversationTab(jid)
+ # insert it in the rooms
+ if not focus:
+ new_tab.state = "private"
+ self.add_tab(new_tab, focus)
+ self.refresh_window()
+ return new_tab
- def on_connected(self, event):
+ def open_private_window(self, room_name, user_nick, focus=True):
"""
- Remote host responded, but we are not yet authenticated
+ Open a Private conversation in a MUC and focus if needed.
"""
- self.information(_("Connected to server."))
+ complete_jid = room_name+'/'+user_nick
+ for tab in self.tabs: # if the room exists, focus it and return
+ if isinstance(tab, tabs.PrivateTab):
+ if tab.get_name() == complete_jid:
+ self.command_win('%s' % tab.nb)
+ return tab
+ # create the new tab
+ tab = self.get_tab_by_name(room_name, tabs.MucTab)
+ if not tab:
+ return None
+ new_tab = tabs.PrivateTab(complete_jid, tab.own_nick)
+ if hasattr(tab, 'directed_presence'):
+ new_tab.directed_presence = tab.directed_presence
+ if not focus:
+ new_tab.state = "private"
+ # insert it in the tabs
+ self.add_tab(new_tab, focus)
+ self.refresh_window()
+ tab.privates.append(new_tab)
+ return new_tab
- def on_session_start(self, event):
+ def open_new_room(self, room, nick, focus=True):
"""
- Called when we are connected and authenticated
+ Open a new tab.MucTab containing a muc Room, using the specified nick
"""
- self.connection_time = time.time()
- if not self.plugins_autoloaded: # Do not reload plugins on reconnection
- self.autoload_plugins()
- self.information(_("Authentication success."))
- self.information(_("Your JID is %s") % self.xmpp.boundjid.full)
- if not self.xmpp.anon:
- # request the roster
- self.xmpp.get_roster()
- # send initial presence
- if config.get('send_initial_presence', 'true').lower() == 'true':
- pres = self.xmpp.make_presence()
- self.events.trigger('send_normal_presence', pres)
- pres.send()
- bookmark.get_local()
- if not self.xmpp.anon and not config.get('use_remote_bookmarks', 'true').lower() == 'false':
- bookmark.get_remote(self.xmpp)
- for bm in [item for item in bookmark.bookmarks if item.autojoin]:
- tab = self.get_tab_by_name(bm.jid, tabs.MucTab)
- if not tab:
- self.open_new_room(bm.jid, bm.nick, False)
- nick = bm.nick if bm.nick else self.own_nick
- self.initial_joins.append(bm.jid)
- muc.join_groupchat(self.xmpp, bm.jid, nick)
+ new_tab = tabs.MucTab(room, nick)
+ self.add_tab(new_tab, focus)
+ self.refresh_window()
- def on_groupchat_presence(self, presence):
+ def open_new_form(self, form, on_cancel, on_send, **kwargs):
"""
- Triggered whenever a presence stanza is received from a user in a multi-user chat room.
- Display the presence on the room window and update the
- presence information of the concerned user
+ Open a new tab containing the form
+ The callback are called with the completed form as parameter in
+ addition with kwargs
"""
- from_room = presence['from'].bare
- tab = self.get_tab_by_name(from_room, tabs.MucTab)
- if tab:
- self.events.trigger('muc_presence', presence, tab)
- tab.handle_presence(presence)
+ form_tab = DataFormsTab(form, on_cancel, on_send, kwargs)
+ self.add_tab(form_tab, True)
+ ### Modifying actions ###
def rename_private_tabs(self, room_name, old_nick, new_nick):
"""
Call this method when someone changes his/her nick in a MUC, this updates
@@ -853,336 +847,61 @@ class Core(object):
if tab: # display the message in private
tab.add_message(msg)
- def on_message(self, message):
+ def close_tab(self, tab=None):
"""
- When receiving private message from a muc OR a normal message
- (from one of our contacts)
+ Close the given tab. If None, close the current one
"""
- if message.find('{http://jabber.org/protocol/muc#user}x/{http://jabber.org/protocol/muc#user}invite') != None:
- return
- if message['type'] == 'groupchat':
- return
- # Differentiate both type of messages, and call the appropriate handler.
- jid_from = message['from']
- for tab in self.tabs:
- if tab.get_name() == jid_from.bare and isinstance(tab, tabs.MucTab):
- if message['type'] == 'error':
- return self.room_error(message, jid_from)
- else:
- return self.on_groupchat_private_message(message)
- return self.on_normal_message(message)
+ tab = tab or self.current_tab()
+ if isinstance(tab, tabs.RosterInfoTab):
+ return # The tab 0 should NEVER be closed
+ del tab.key_func # Remove self references
+ del tab.commands # and make the object collectable
+ tab.on_close()
+ self.tabs.remove(tab)
+ if tab.get_name() in logger.fds:
+ logger.fds[tab.get_name()].close()
+ log.debug("Log file for %s closed.", tab.get_name())
+ del logger.fds[tab.get_name()]
+ self.tabs[0].on_gain_focus()
+ self.refresh_window()
+ import gc
+ gc.collect()
+ log.debug('___ Referrers of closing tab:\n%s\n______', gc.get_referrers(tab))
+ del tab
- def on_groupchat_private_message(self, message):
+ def add_information_message_to_conversation_tab(self, jid, msg):
"""
- We received a Private Message (from someone in a Muc)
+ Search for a ConversationTab with the given jid (full or bare), if yes, add
+ the given message to it
"""
- jid = message['from']
- nick_from = jid.resource
- room_from = jid.bare
- body = xhtml.get_body_from_message_stanza(message)
- tab = self.get_tab_by_name(jid.full, tabs.PrivateTab) # get the tab with the private conversation
- ignore = config.get_by_tabname('ignore_private', 'false',
- room_from).lower() == 'true'
- if not tab: # It's the first message we receive: create the tab
- if body and not ignore:
- tab = self.open_private_window(room_from, nick_from, False)
- if ignore:
- self.events.trigger('ignored_private', message, tab)
- msg = config.get_by_tabname('private_auto_response', None, room_from)
- if msg and body:
- self.xmpp.send_message(mto=jid.full, mbody=msg, mtype='chat')
- return
- self.events.trigger('private_msg', message, tab)
- if not body or not tab:
- return
- tab.add_message(body, time=None, nickname=nick_from,
- forced_user=self.get_tab_by_name(room_from, tabs.MucTab).get_user_by_name(nick_from))
- conversation = self.get_tab_by_name(jid.full, tabs.PrivateTab)
- if conversation and conversation.remote_wants_chatstates is None:
- if message['chat_state']:
- conversation.remote_wants_chatstates = True
- else:
- conversation.remote_wants_chatstates = False
- if 'private' in config.get('beep_on', 'highlight private').split():
- if config.get_by_tabname('disable_beep', 'false', jid.full, False).lower() != 'true':
- curses.beep()
- logger.log_message(jid.full.replace('/', '\\'), nick_from, body)
- if conversation is self.current_tab():
- self.refresh_window()
- else:
- conversation.state = 'private'
- self.refresh_tab_win()
+ tab = self.get_tab_by_name(jid, tabs.ConversationTab)
+ if tab:
+ self.add_message_to_text_buffer(tab._text_buffer, msg)
- def focus_tab_named(self, tab_name):
- for tab in self.tabs:
- if tab.get_name() == tab_name:
- self.command_win('%s' % (tab.nb,))
- def get_tab_of_conversation_with_jid(self, jid, create=True):
- """
- From a JID, get the tab containing the conversation with it.
- If none already exist, and create is "True", we create it
- and return it. Otherwise, we return None
- """
- # We first check if we have a conversation opened with this precise resource
- conversation = self.get_tab_by_name(jid.full, tabs.ConversationTab)
- if not conversation:
- # If not, we search for a conversation with the bare jid
- conversation = self.get_tab_by_name(jid.bare, tabs.ConversationTab)
- if not conversation:
- if create:
- # We create the conversation with the full Jid if nothing was found
- conversation = self.open_conversation_window(jid.full, False)
- else:
- conversation = None
- return conversation
+####################### Curses and ui-related stuff ###########################
- def on_normal_message(self, message):
- """
- When receiving "normal" messages (from someone in our roster)
- """
- jid = message['from']
- body = xhtml.get_body_from_message_stanza(message)
- if message['type'] == 'error':
- return self.information(self.get_error_message_from_error_stanza(message, deprecated=True), 'Error')
- if not body:
- return
- conversation = self.get_tab_of_conversation_with_jid(jid, create=True)
- self.events.trigger('conversation_msg', message, conversation)
- body = xhtml.get_body_from_message_stanza(message)
- if jid.bare in roster:
- remote_nick = roster[jid.bare].name or jid.user
- else:
- remote_nick = jid.user
- delay_tag = message.find('{urn:xmpp:delay}delay')
- if delay_tag is not None:
- delayed = True
- date = common.datetime_tuple(delay_tag.attrib['stamp'])
- else:
- delayed = False
- date = None
- conversation._text_buffer.add_message(body, date, nickname=remote_nick, nick_color=get_theme().COLOR_REMOTE_USER, history=delayed)
- if conversation.remote_wants_chatstates is None and not delayed:
- if message['chat_state']:
- conversation.remote_wants_chatstates = True
- else:
- conversation.remote_wants_chatstates = False
- logger.log_message(jid.bare, remote_nick, body)
- if 'private' in config.get('beep_on', 'highlight private').split():
- if config.get_by_tabname('disable_beep', 'false', jid.bare, False).lower() != 'true':
- curses.beep()
- if self.current_tab() is not conversation:
- conversation.state = 'private'
- self.refresh_tab_win()
- else:
- self.refresh_window()
-
- def on_presence(self, presence):
- jid = presence['from']
- contact = roster[jid.bare]
- if contact is None:
+ def doupdate(self):
+ if not self.running or self.background is True:
return
- self.events.trigger('normal_presence', presence, contact[jid.full])
- tab = self.get_tab_of_conversation_with_jid(jid, create=False)
- if isinstance(self.current_tab(), tabs.RosterInfoTab):
- self.refresh_window()
- elif self.current_tab() == tab:
- tab.refresh()
- self.doupdate()
+ curses.doupdate()
- def on_roster_update(self, iq):
+ def information(self, msg, typ=''):
"""
- The roster was received.
+ Displays an informational message in the "Info" buffer
"""
- for item in iq['roster']:
- jid = item['jid']
- if item['subscription'] == 'remove':
- del roster[jid]
- else:
- roster.update_contact_groups(jid)
- if isinstance(self.current_tab(), tabs.RosterInfoTab):
- self.refresh_window()
-
- def on_subscription_request(self, presence):
- """subscribe received"""
- jid = presence['from'].bare
- contact = roster[jid]
- if contact.subscription in ('from', 'both'):
- return
- elif contact.subscription == 'to':
- self.xmpp.sendPresence(pto=jid, ptype='subscribed')
- self.xmpp.sendPresence(pto=jid)
- else:
- roster.update_contact_groups(contact)
- contact.pending_in = True
- self.information('%s wants to subscribe to your presence' % jid, 'Roster')
- self.get_tab_by_number(0).state = 'highlight'
- if isinstance(self.current_tab(), tabs.RosterInfoTab):
- self.refresh_window()
-
- def on_subscription_authorized(self, presence):
- """subscribed received"""
- jid = presence['from'].bare
- contact = roster[jid]
- if contact.subscription not in ('both', 'from'):
- self.information('%s accepted your contact proposal' % jid, 'Roster')
- if contact.pending_out:
- contact.pending_out = False
- if isinstance(self.current_tab(), tabs.RosterInfoTab):
- self.refresh_window()
-
- def on_subscription_remove(self, presence):
- """unsubscribe received"""
- jid = presence['from'].bare
- contact = roster[jid]
- if not contact:
- return
- self.information('%s does not want to receive your status anymore.' % jid, 'Roster')
- self.get_tab_by_number(0).state = 'highlight'
- if isinstance(self.current_tab(), tabs.RosterInfoTab):
- self.refresh_window()
-
- def on_subscription_removed(self, presence):
- """unsubscribed received"""
- jid = presence['from'].bare
- contact = roster[jid]
- if not contact:
- return
- if contact.pending_out:
- self.information('%s rejected your contact proposal' % jid, 'Roster')
- contact.pending_out = False
- else:
- self.information('%s does not want you to receive his/her/its status anymore.'%jid, 'Roster')
- self.get_tab_by_number(0).state = 'highlight'
+ nb_lines = self.information_buffer.add_message(msg, nickname=typ)
if isinstance(self.current_tab(), tabs.RosterInfoTab):
self.refresh_window()
-
- def full_screen_redraw(self):
- """
- Completely erase and redraw the screen
- """
- self.stdscr.clear()
- self.refresh_window()
-
- def call_for_resize(self):
- """
- Called when we want to resize the screen
- """
- # If we have the tabs list on the left, we just give a truncated
- # window to each Tab class, so the draw themself in the portion
- # of the screen that the can occupy, and we draw the tab list
- # on the left remaining space
- if config.get('enable_vertical_tab_list', 'false') == 'true':
- scr = self.stdscr.subwin(0, config.get('vertical_tab_list_size', 20))
+ elif typ != '' and typ.lower() in config.get('information_buffer_popup_on',
+ 'error roster warning help info').split():
+ popup_time = config.get('popup_time', 4) + (nb_lines - 1) * 2
+ self.pop_information_win_up(nb_lines, popup_time)
else:
- scr = self.stdscr
- tabs.Tab.resize(scr)
- self.resize_global_info_bar()
- self.resize_global_information_win()
- with g_lock:
- for tab in self.tabs:
- if config.get('lazy_resize', 'true') == 'true':
- tab.need_resize = True
- else:
- tab.resize()
- if self.tabs:
- self.full_screen_redraw()
-
- def read_keyboard(self):
- """
- Get the next keyboard key pressed and returns it.
- read_char() has a timeout: it returns None when the timeout
- occurs. In that case we do not return (we loop until we get
- a non-None value), but we check for timed events instead.
- """
- res = read_char(self.stdscr)
- while res is None:
- self.check_timed_events()
- res = read_char(self.stdscr)
- return res
-
- def main_loop(self):
- """
- main loop waiting for the user to press a key
- """
- def replace_line_breaks(key):
- if key == '^J':
- return '\n'
- return key
- def replace_tabulation(key):
- if key == '^I':
- return ' '
- return key
- while self.running:
- if self.paused: continue
- char_list = [common.replace_key_with_bound(key)\
- for key in self.read_keyboard()]
- if self.paused:
- self.current_tab().input.do_command(char_list[0])
- self.current_tab().input.prompt()
- continue
- # Special case for M-x where x is a number
- if len(char_list) == 1:
- char = char_list[0]
- if char.startswith('M-') and len(char) == 3:
- try:
- nb = int(char[2])
- except ValueError:
- pass
- else:
- if self.current_tab().nb == nb:
- self.go_to_previous_tab()
- else:
- self.command_win('%d' % nb)
- # search for keyboard shortcut
- func = self.key_func.get(char, None)
- if func:
- func()
- else:
- res = self.do_command(replace_line_breaks(char), False)
- if res:
- self.refresh_window()
- else:
- self.do_command(''.join(map(
- lambda x: replace_line_breaks(replace_tabulation(x)),
- char_list)
- ), True)
- self.refresh_window()
- self.doupdate()
-
- def current_tab(self):
- """
- returns the current room, the one we are viewing
- """
- return self.tabs[0]
-
- def get_conversation_by_jid(self, jid):
- """
- Return the room of the ConversationTab with the given jid
- """
- for tab in self.tabs:
- if isinstance(tab, tabs.ConversationTab):
- if tab.get_name() == jid:
- return tab
- return None
-
- def get_tab_by_name(self, name, typ=None):
- """
- Get the tab with the given name.
- If typ is provided, return a tab of this type only
- """
- for tab in self.tabs:
- if tab.get_name() == name:
- if (typ and isinstance(tab, typ)) or\
- not typ:
- return tab
- return None
+ if self.information_win_size != 0:
+ self.information_win.refresh()
+ self.current_tab().input.refresh()
- def get_tab_by_number(self, number):
- for tab in self.tabs:
- if tab.nb == number:
- return tab
- return None
def init_curses(self, stdscr):
"""
@@ -1212,6 +931,10 @@ class Core(object):
curses.curs_set(1)
curses.endwin()
+ @property
+ def informations(self):
+ return self.information_buffer
+
def refresh_window(self):
"""
Refresh everything
@@ -1226,66 +949,6 @@ class Core(object):
self.current_tab().input.refresh()
self.doupdate()
- def add_tab(self, new_tab, focus=False):
- """
- Appends the new_tab in the tab list and
- focus it if focus==True
- """
- if self.current_tab().nb == 0:
- self.tabs.append(new_tab)
- else:
- for ta in self.tabs:
- if ta.nb == 0:
- self.tabs.insert(self.tabs.index(ta), new_tab)
- break
- if focus:
- self.command_win("%s" % new_tab.nb)
-
- def open_new_room(self, room, nick, focus=True):
- """
- Open a new tab.MucTab containing a muc Room, using the specified nick
- """
- new_tab = tabs.MucTab(room, nick)
- self.add_tab(new_tab, focus)
- self.refresh_window()
-
- def go_to_roster(self):
- self.command_win('0')
-
- def go_to_previous_tab(self):
- self.command_win('%s' % (self.previous_tab_nb,))
-
- def go_to_important_room(self):
- """
- Go to the next room with activity, in the order defined in the
- dict tabs.STATE_PRIORITY
- """
- priority = tabs.STATE_PRIORITY
- sorted_tabs = sorted(self.tabs, key=lambda tab: priority[tab.state],
- reverse=True)
- tab = sorted_tabs.pop(0) if sorted_tabs else None
- if priority[tab.state] < 0 or not tab:
- return
- self.command_win('%s' % tab.nb)
-
- def rotate_rooms_right(self, args=None):
- """
- rotate the rooms list to the right
- """
- self.current_tab().on_lose_focus()
- self.tabs.append(self.tabs.pop(0))
- self.current_tab().on_gain_focus()
- self.refresh_window()
-
- def rotate_rooms_left(self, args=None):
- """
- rotate the rooms list to the right
- """
- self.current_tab().on_lose_focus()
- self.tabs.insert(0, self.tabs.pop())
- self.current_tab().on_gain_focus()
- self.refresh_window()
-
def scroll_page_down(self, args=None):
self.current_tab().on_scroll_down()
self.refresh_window()
@@ -1310,208 +973,89 @@ class Core(object):
self.current_tab().on_half_scroll_down()
self.refresh_window()
- def get_error_message(self, stanza, deprecated=False):
- """
- Takes a stanza of the form <message type='error'><error/></message>
- and return a well formed string containing the error informations
- """
- sender = stanza.attrib['from']
- msg = stanza['error']['type']
- condition = stanza['error']['condition']
- code = stanza['error']['code']
- body = stanza['error']['text']
- if not body:
- if deprecated:
- if code in DEPRECATED_ERRORS:
- body = DEPRECATED_ERRORS[code]
- else:
- body = condition or _('Unknown error')
- else:
- if code in ERROR_AND_STATUS_CODES:
- body = ERROR_AND_STATUS_CODES[code]
- else:
- body = condition or _('Unknown error')
- if code:
- message = _('%(from)s: %(code)s - %(msg)s: %(body)s') % {'from':sender, 'msg':msg, 'body':body, 'code':code}
- else:
- message = _('%(from)s: %(msg)s: %(body)s') % {'from':sender, 'msg':msg, 'body':body}
- return message
-
- def room_error(self, error, room_name):
- """
- Display the error in the tab
- """
- tab = self.get_tab_by_name(room_name)
- error_message = self.get_error_message(error)
- self.add_message_to_text_buffer(tab._text_buffer, error_message)
- code = error['error']['code']
- if code == '401':
- msg = _('To provide a password in order to join the room, type "/join / password" (replace "password" by the real password)')
- self.add_message_to_text_buffer(tab._text_buffer, msg)
- if code == '409':
- if config.get('alternative_nickname', '') != '':
- self.command_join('%s/%s'% (tab.name, tab.own_nick+config.get('alternative_nickname', '')))
- else:
- self.add_message_to_text_buffer(tab._text_buffer, _('You can join the room with an other nick, by typing "/join /other_nick"'))
+ def grow_information_win(self, nb=1):
+ if self.information_win_size >= self.current_tab().height -5 or \
+ self.information_win_size+nb >= self.current_tab().height-4:
+ return
+ if self.information_win_size == 14:
+ return
+ self.information_win_size += nb
+ if self.information_win_size > 14:
+ self.information_win_size = 14
+ self.resize_global_information_win()
+ for tab in self.tabs:
+ tab.on_info_win_size_changed()
self.refresh_window()
- def open_conversation_window(self, jid, focus=True):
- """
- Open a new conversation tab and focus it if needed
- """
- for tab in self.tabs: # if the room exists, focus it and return
- if isinstance(tab, tabs.ConversationTab):
- if tab.get_name() == jid:
- self.command_win('%s' % tab.nb)
- return tab
- new_tab = tabs.ConversationTab(jid)
- # insert it in the rooms
- if not focus:
- new_tab.state = "private"
- self.add_tab(new_tab, focus)
+ def shrink_information_win(self, nb=1):
+ if self.information_win_size == 0:
+ return
+ self.information_win_size -= nb
+ if self.information_win_size < 0:
+ self.information_win_size = 0
+ self.resize_global_information_win()
+ for tab in self.tabs:
+ tab.on_info_win_size_changed()
self.refresh_window()
- return new_tab
- def open_private_window(self, room_name, user_nick, focus=True):
- """
- Open a Private conversation in a MUC and focus if needed.
- """
- complete_jid = room_name+'/'+user_nick
- for tab in self.tabs: # if the room exists, focus it and return
- if isinstance(tab, tabs.PrivateTab):
- if tab.get_name() == complete_jid:
- self.command_win('%s' % tab.nb)
- return tab
- # create the new tab
- tab = self.get_tab_by_name(room_name, tabs.MucTab)
- if not tab:
- return None
- new_tab = tabs.PrivateTab(complete_jid, tab.own_nick)
- if hasattr(tab, 'directed_presence'):
- new_tab.directed_presence = tab.directed_presence
- if not focus:
- new_tab.state = "private"
- # insert it in the tabs
- self.add_tab(new_tab, focus)
- self.refresh_window()
- tab.privates.append(new_tab)
- return new_tab
+ def scroll_info_up(self):
+ self.information_win.scroll_up(self.information_win.height)
+ if not isinstance(self.current_tab(), tabs.RosterInfoTab):
+ self.information_win.refresh()
+ else:
+ info = self.current_tab().information_win
+ info.scroll_up(info.height)
+ self.refresh_window()
- def on_groupchat_subject(self, message):
- """
- Triggered when the topic is changed.
- """
- nick_from = message['mucnick']
- room_from = message.getMucroom()
- tab = self.get_tab_by_name(room_from, tabs.MucTab)
- subject = message['subject']
- if not subject or not tab:
- return
- if nick_from:
- self.add_message_to_text_buffer(tab._text_buffer,
- _("\x19%(info_col)s}%(nick)s set the subject to: %(subject)s") %
- {'info_col': get_theme().COLOR_INFORMATION_TEXT[0], 'nick':nick_from, 'subject':subject},
- time=None)
+ def scroll_info_down(self):
+ self.information_win.scroll_down(self.information_win.height)
+ if not isinstance(self.current_tab(), tabs.RosterInfoTab):
+ self.information_win.refresh()
else:
- self.add_message_to_text_buffer(tab._text_buffer, _("\x19%(info_col)s}The subject is: %(subject)s") %
- {'subject':subject, 'info_col': get_theme().COLOR_INFORMATION_TEXT[0]},
- time=None)
- tab.topic = subject
- if self.get_tab_by_name(room_from, tabs.MucTab) is self.current_tab():
+ info = self.current_tab().information_win
+ info.scroll_down(info.height)
self.refresh_window()
- def on_groupchat_message(self, message):
+ def pop_information_win_up(self, size, time):
"""
- Triggered whenever a message is received from a multi-user chat room.
+ Temporarly increase the size of the information win of size lines
+ during time seconds.
+ After that delay, the size will decrease from size lines.
"""
- if message['subject']:
- return
- delay_tag = message.find('{urn:xmpp:delay}delay')
- if delay_tag is not None:
- delayed = True
- date = common.datetime_tuple(delay_tag.attrib['stamp'])
- else:
- # We support the OLD and deprecated XEP: http://xmpp.org/extensions/xep-0091.html
- # But it sucks, please, Jabber servers, don't do this :(
- delay_tag = message.find('{jabber:x:delay}x')
- if delay_tag is not None:
- delayed = True
- date = common.datetime_tuple(delay_tag.attrib['stamp'])
- else:
- delayed = False
- date = None
- nick_from = message['mucnick']
- room_from = message.getMucroom()
- if message['type'] == 'error': # Check if it's an error
- return self.room_error(message, room_from)
- tab = self.get_tab_by_name(room_from, tabs.MucTab)
- old_state = tab.state
- if not tab:
- self.information(_("message received for a non-existing room: %s") % (room_from))
- return
- if tab.get_user_by_name(nick_from) and\
- tab.get_user_by_name(nick_from) in tab.ignores:
+ if time <= 0 or size <= 0:
return
- self.events.trigger('muc_msg', message, tab)
- body = xhtml.get_body_from_message_stanza(message)
- if body:
- date = date if delayed == True else None
- if tab.add_message(body, date, nick_from, history=True if date else False):
- self.events.trigger('highlight', message, tab)
- if tab is self.current_tab():
- tab.text_win.refresh()
- tab.info_header.refresh(tab, tab.text_win)
- tab.input.refresh()
- self.doupdate()
- elif tab.state != old_state:
- self.refresh_tab_win()
- self.current_tab().input.refresh()
- self.doupdate()
- if 'message' in config.get('beep_on', 'highlight private').split():
- if config.get_by_tabname('disable_beep', 'false', room_from, False).lower() != 'true':
- curses.beep()
+ self.grow_information_win(size)
+ timed_event = timed_events.DelayedEvent(time, self.shrink_information_win, size)
+ self.add_timed_event(timed_event)
- def on_status_codes(self, message):
+ def toggle_left_pane(self):
"""
- Handle groupchat messages with status codes.
- Those are received when a room configuration change occurs.
+ Enable/disable the left panel.
"""
- room_from = message['from']
- tab = self.get_tab_by_name(room_from, tabs.MucTab)
- status_codes = set([s.attrib['code'] for s in message.findall('{%s}x/{%s}status' % (tabs.NS_MUC_USER, tabs.NS_MUC_USER))])
- if '101' in status_codes:
- self.information('Your affiliation in the room %s changed' % room_from, 'Info')
- elif tab and status_codes:
- show_unavailable = '102' in status_codes
- hide_unavailable = '103' in status_codes
- non_priv = '104' in status_codes
- logging_on = '170' in status_codes
- logging_off= '171' in status_codes
- non_anon = '172' in status_codes
- semi_anon = '173' in status_codes
- full_anon = '174' in status_codes
- modif = False
- if show_unavailable or hide_unavailable or non_priv or logging_off\
- or non_anon or semi_anon or full_anon:
- tab.add_message('\x19%(info_col)s}Info: A configuration change not privacy-related occured.' % {'info_col': get_theme().COLOR_INFORMATION_TEXT[0]})
- modif = True
- if show_unavailable:
- tab.add_message('\x19%(info_col)s}Info: The unavailable members are now shown.' % {'info_col': get_theme().COLOR_INFORMATION_TEXT[0]})
- elif hide_unavailable:
- tab.add_message('\x19%(info_col)s}Info: The unavailable members are now hidden.' % {'info_col': get_theme().COLOR_INFORMATION_TEXT[0]})
- if non_anon:
- tab.add_message('\x191}Warning:\x19%(info_col)s} The room is now not anonymous. (public JID)' % {'info_col': get_theme().COLOR_INFORMATION_TEXT[0]})
- elif semi_anon:
- tab.add_message('\x19%(info_col)s}Info: The room is now semi-anonymous. (moderators-only JID)' % {'info_col': get_theme().COLOR_INFORMATION_TEXT[0]})
- elif full_anon:
- tab.add_message('\x19%(info_col)s}Info: The room is now fully anonymous.' % {'info_col': get_theme().COLOR_INFORMATION_TEXT[0]})
- if logging_on:
- tab.add_message('\x191}Warning: \x19%(info_col)s}This room is publicly logged' % {'info_col': get_theme().COLOR_INFORMATION_TEXT[0]})
- elif logging_off:
- tab.add_message('\x19%(info_col)s}Info: This room is not logged anymore.' % {'info_col': get_theme().COLOR_INFORMATION_TEXT[0]})
- if modif:
- self.refresh_window()
+ enabled = config.get('enable_vertical_tab_list', 'false')
+ config.set('enable_vertical_tab_list', 'false' if enabled == 'true' else 'true')
+ self.call_for_resize()
+
+ def resize_global_information_win(self):
+ """
+ Resize the global_information_win only once at each resize.
+ """
+ with g_lock:
+ self.information_win.resize(self.information_win_size, tabs.Tab.width,
+ tabs.Tab.height - 1 - self.information_win_size - tabs.Tab.tab_win_height(), 0)
+ def resize_global_info_bar(self):
+ """
+ Resize the GlobalInfoBar only once at each resize
+ """
+ with g_lock:
+ self.tab_win.resize(1, tabs.Tab.width, tabs.Tab.height - 2, 0)
+ if config.get('enable_vertical_tab_list', 'false') == 'true':
+ height, width = self.stdscr.getmaxyx()
+ truncated_win = self.stdscr.subwin(height, config.get('vertical_tab_list_size', 20), 0, 0)
+ self.left_tab_win = windows.VerticalGlobalInfoBar(truncated_win)
+ else:
+ self.left_tab_win = None
def add_message_to_text_buffer(self, buff, txt, time=None, nickname=None, history=None):
"""
@@ -1523,6 +1067,52 @@ class Core(object):
else:
buff.add_message(txt, time, nickname, history=history)
+ def full_screen_redraw(self):
+ """
+ Completely erase and redraw the screen
+ """
+ self.stdscr.clear()
+ self.refresh_window()
+
+ def call_for_resize(self):
+ """
+ Called when we want to resize the screen
+ """
+ # If we have the tabs list on the left, we just give a truncated
+ # window to each Tab class, so the draw themself in the portion
+ # of the screen that the can occupy, and we draw the tab list
+ # on the left remaining space
+ if config.get('enable_vertical_tab_list', 'false') == 'true':
+ scr = self.stdscr.subwin(0, config.get('vertical_tab_list_size', 20))
+ else:
+ scr = self.stdscr
+ tabs.Tab.resize(scr)
+ self.resize_global_info_bar()
+ self.resize_global_information_win()
+ with g_lock:
+ for tab in self.tabs:
+ if config.get('lazy_resize', 'true') == 'true':
+ tab.need_resize = True
+ else:
+ tab.resize()
+ if self.tabs:
+ self.full_screen_redraw()
+
+ def read_keyboard(self):
+ """
+ Get the next keyboard key pressed and returns it.
+ read_char() has a timeout: it returns None when the timeout
+ occurs. In that case we do not return (we loop until we get
+ a non-None value), but we check for timed events instead.
+ """
+ res = read_char(self.stdscr)
+ while res is None:
+ self.check_timed_events()
+ res = read_char(self.stdscr)
+ return res
+
+####################### Commands and completions ##############################
+
def command_help(self, arg):
"""
/help <command_name>
@@ -1582,19 +1172,11 @@ class Core(object):
if isinstance(current, tabs.MucTab) and current.joined and show not in ('away', 'xa'):
current.send_chat_state('active')
- def command_rawxml(self, arg):
+ def completion_status(self, the_input):
"""
- /rawxml <xml stanza>
+ Completion of /status
"""
- if not arg:
- return
-
- try:
- StanzaBase(self.xmpp, xml=ET.fromstring(arg)).send()
- except:
- import traceback
- self.information(_('Could not send custom stanza'), 'Error')
- log.debug(_("Could not send custom stanza:\n") + traceback.format_exc())
+ return the_input.auto_completion([status for status in possible_show], ' ')
def command_presence(self, arg):
"""
@@ -1637,12 +1219,6 @@ class Core(object):
if self.current_tab() in tab.privates:
self.current_tab().send_chat_state(chatstate, True)
- def completion_status(self, the_input):
- """
- Completion of /status
- """
- return the_input.auto_completion([status for status in possible_show], ' ')
-
def completion_presence(self, the_input):
"""
Completion of /presence
@@ -1657,96 +1233,6 @@ class Core(object):
elif n == 3:
return the_input.auto_completion([status for status in possible_show], '')
- def command_load(self, arg):
- """
- /load <plugin>
- """
- args = arg.split()
- if len(args) != 1:
- self.command_help('load')
- return
- filename = args[0]
- self.plugin_manager.load(filename)
-
- def command_unload(self, arg):
- """
- /unload <plugin>
- """
- args = arg.split()
- if len(args) != 1:
- self.command_help('unload')
- return
- filename = args[0]
- self.plugin_manager.unload(filename)
-
- def command_plugins(self, arg=''):
- """
- /plugins
- """
- self.information("Plugins currently in use: %s" % repr(list(self.plugin_manager.plugins.keys())), 'Info')
-
- def command_message(self, arg):
- """
- /message <jid> [message]
- """
- args = common.shell_split(arg)
- if len(args) < 1:
- self.command_help('message')
- return
- jid = args[0]
- tab = self.open_conversation_window(jid, focus=True)
- if len(args) > 1:
- tab.command_say(args[1])
-
- def command_version(self, arg):
- """
- /version <jid>
- """
- def callback(res):
- if not res:
- return self.information('Could not get the software version from %s' % (jid,), 'Warning')
- version = '%s is running %s version %s on %s' % (jid,
- res.get('name') or _('an unknown software'),
- res.get('version') or _('unknown'),
- res.get('os') or _('on an unknown platform'))
- self.information(version, 'Info')
-
- args = common.shell_split(arg)
- if len(args) < 1:
- return self.command_help('version')
- jid = JID(args[0])
- if jid.resource or jid not in roster:
- self.xmpp.plugin['xep_0092'].get_version(jid, callback=callback)
- elif jid in roster:
- for resource in roster[jid].resources:
- self.xmpp.plugin['xep_0092'].get_version(resource.jid, callback=callback)
- else:
- self.xmpp.plugin['xep_0092'].get_version(jid, callback=callback)
-
- def command_reconnect(self, args=None):
- """
- /reconnect
- """
- self.disconnect(reconnect=True)
-
- def command_list(self, arg):
- """
- /list <server>
- Opens a MucListTab containing the list of the room in the specified server
- """
- arg = arg.split()
- if len(arg) > 1:
- return self.command_help('list')
- elif arg:
- server = JID(arg[0]).server
- else:
- if not isinstance(self.current_tab(), tabs.MucTab):
- return self.information('Please provide a server', 'Error')
- server = JID(self.current_tab().get_name()).server
- list_tab = tabs.MucListTab(server)
- self.add_tab(list_tab, True)
- self.xmpp.plugin['xep_0030'].get_items(jid=server, block=False, callback=list_tab.on_muc_list_item_received)
-
def command_theme(self, arg=''):
"""/theme <theme name>"""
args = arg.split()
@@ -1812,111 +1298,58 @@ class Core(object):
l.remove('')
return the_input.auto_completion(l, ' ', quotify=False)
- def completion_join(self, the_input):
+ def command_list(self, arg):
"""
- Try to complete the server of the MUC's jid (for now only from the currently
- open ones)
- TODO: have a history of recently joined MUCs, and use that too
+ /list <server>
+ Opens a MucListTab containing the list of the room in the specified server
"""
- txt = the_input.get_text()
- if len(txt.split()) != 2:
- # we are not on the 1st argument of the command line
- return False
- jid = JID(txt.split()[1])
- if jid.server:
- if jid.resource or jid.full.endswith('/'):
- # we are writing the resource: complete the node
- if not the_input.last_completion:
- try:
- response = self.xmpp.plugin['xep_0030'].get_items(jid=jid.server, block=True, timeout=1)
- except:
- response = None
- if response:
- items = response['disco_items'].get_items()
- else:
- return True
- items = ['%s/%s' % (tup[0], jid.resource) for tup in items]
- for _ in range(len(jid.server) + 2 + len(jid.resource)):
- the_input.key_backspace()
- else:
- items = []
- items.extend(list(self.pending_invites.keys()))
- the_input.auto_completion(items, '')
- else:
- # we are writing the server: complete the server
- serv_list = []
- for tab in self.tabs:
- if isinstance(tab, tabs.MucTab):
- serv_list.append('%s@%s'% (jid.user, JID(tab.get_name()).host))
- serv_list.extend(list(self.pending_invites.keys()))
- the_input.auto_completion(serv_list, '')
- return True
-
- def completion_bookmark_local(self, the_input):
- """Completion for /bookmark_local"""
- txt = the_input.get_text()
- args = common.shell_split(txt)
- n = len(args)
- if txt.endswith(' '):
- n += 1
-
- if len(args) == 1:
- jid = JID('')
+ arg = arg.split()
+ if len(arg) > 1:
+ return self.command_help('list')
+ elif arg:
+ server = JID(arg[0]).server
else:
- jid = JID(args[1])
- if len(args) > 2:
- return
- if jid.server and (jid.resource or jid.full.endswith('/')):
- tab = self.get_tab_by_name(jid.bare, tabs.MucTab)
- nicks = [tab.own_nick] if tab else []
- default = os.environ.get('USER') if os.environ.get('USER') else 'poezio'
- nick = config.get('default_nick', '')
- if not nick:
- if not default in nicks:
- nicks.append(default)
- else:
- if not nick in nicks:
- nicks.append(nick)
- jids_list = ['%s/%s' % (jid.bare, nick) for nick in nicks]
- return the_input.auto_completion(jids_list, '')
- muc_list = [tab.get_name() for tab in self.tabs if isinstance(tab, tabs.MucTab)]
- muc_list.append('*')
- return the_input.auto_completion(muc_list, '')
-
- def completion_bookmark(self, the_input):
- """Completion for /bookmark"""
- txt = the_input.get_text()
- args = common.shell_split(txt)
- n = len(args)
- if txt.endswith(' '):
- n += 1
+ if not isinstance(self.current_tab(), tabs.MucTab):
+ return self.information('Please provide a server', 'Error')
+ server = JID(self.current_tab().get_name()).server
+ list_tab = tabs.MucListTab(server)
+ self.add_tab(list_tab, True)
+ self.xmpp.plugin['xep_0030'].get_items(jid=server, block=False, callback=list_tab.on_muc_list_item_received)
- if len(args) == 1:
- jid = JID('')
- else:
- jid = JID(args[1])
+ def completion_list(self, the_input):
+ """Completion for /list"""
+ muc_serv_list = []
+ for tab in self.tabs: # TODO, also from an history
+ if isinstance(tab, tabs.MucTab) and\
+ tab.get_name() not in muc_serv_list:
+ muc_serv_list.append(JID(tab.get_name()).server)
+ if muc_serv_list:
+ return the_input.auto_completion(muc_serv_list, ' ', quotify=False)
- if len(args) == 2:
- return the_input.auto_completion(['true', 'false'], '')
- if len(args) == 3:
- return
+ def command_version(self, arg):
+ """
+ /version <jid>
+ """
+ def callback(res):
+ if not res:
+ return self.information('Could not get the software version from %s' % (jid,), 'Warning')
+ version = '%s is running %s version %s on %s' % (jid,
+ res.get('name') or _('an unknown software'),
+ res.get('version') or _('unknown'),
+ res.get('os') or _('on an unknown platform'))
+ self.information(version, 'Info')
- if jid.server and (jid.resource or jid.full.endswith('/')):
- tab = self.get_tab_by_name(jid.bare, tabs.MucTab)
- nicks = [tab.own_nick] if tab else []
- default = os.environ.get('USER') if os.environ.get('USER') else 'poezio'
- nick = config.get('default_nick', '')
- if not nick:
- if not default in nicks:
- nicks.append(default)
+ args = common.shell_split(arg)
+ if len(args) < 1:
+ return self.command_help('version')
+ jid = JID(args[0])
+ if jid.resource or jid not in roster:
+ self.xmpp.plugin['xep_0092'].get_version(jid, callback=callback)
+ elif jid in roster:
+ for resource in roster[jid].resources:
+ self.xmpp.plugin['xep_0092'].get_version(resource.jid, callback=callback)
else:
- if not nick in nicks:
- nicks.append(nick)
- jids_list = ['%s/%s' % (jid.bare, nick) for nick in nicks]
- return the_input.auto_completion(jids_list, '')
- muc_list = [tab.get_name() for tab in self.tabs if isinstance(tab, tabs.MucTab)]
- muc_list.append('*')
- return the_input.auto_completion(muc_list, '')
+ self.xmpp.plugin['xep_0092'].get_version(jid, callback=callback)
def completion_version(self, the_input):
"""Completion for /version"""
@@ -1927,16 +1360,6 @@ class Core(object):
comp = (str(res.jid) for res in comp)
return the_input.auto_completion(sorted(comp), '', quotify=False)
- def completion_list(self, the_input):
- """Completion for /list"""
- muc_serv_list = []
- for tab in self.tabs: # TODO, also from an history
- if isinstance(tab, tabs.MucTab) and\
- tab.get_name() not in muc_serv_list:
- muc_serv_list.append(JID(tab.get_name()).server)
- if muc_serv_list:
- return the_input.auto_completion(muc_serv_list, ' ', quotify=False)
-
def command_join(self, arg, histo_length=None):
"""
/join [room][/nick] [password]
@@ -2012,15 +1435,45 @@ class Core(object):
tab.refresh()
self.doupdate()
- def get_bookmark_nickname(self, room_name):
+ def completion_join(self, the_input):
"""
- Returns the nickname associated with a bookmark
- or the default nickname
+ Try to complete the server of the MUC's jid (for now only from the currently
+ open ones)
+ TODO: have a history of recently joined MUCs, and use that too
"""
- bm = bookmark.get_by_jid(room_name)
- if bm:
- return bm.nick
- return self.own_nick
+ txt = the_input.get_text()
+ if len(txt.split()) != 2:
+ # we are not on the 1st argument of the command line
+ return False
+ jid = JID(txt.split()[1])
+ if jid.server:
+ if jid.resource or jid.full.endswith('/'):
+ # we are writing the resource: complete the node
+ if not the_input.last_completion:
+ try:
+ response = self.xmpp.plugin['xep_0030'].get_items(jid=jid.server, block=True, timeout=1)
+ except:
+ response = None
+ if response:
+ items = response['disco_items'].get_items()
+ else:
+ return True
+ items = ['%s/%s' % (tup[0], jid.resource) for tup in items]
+ for _ in range(len(jid.server) + 2 + len(jid.resource)):
+ the_input.key_backspace()
+ else:
+ items = []
+ items.extend(list(self.pending_invites.keys()))
+ the_input.auto_completion(items, '')
+ else:
+ # we are writing the server: complete the server
+ serv_list = []
+ for tab in self.tabs:
+ if isinstance(tab, tabs.MucTab):
+ serv_list.append('%s@%s'% (jid.user, JID(tab.get_name()).host))
+ serv_list.extend(list(self.pending_invites.keys()))
+ the_input.auto_completion(serv_list, '')
+ return True
def command_bookmark_local(self, arg=''):
"""
@@ -2073,6 +1526,37 @@ class Core(object):
self.information(_('Your local bookmarks are now: %s') %
[b for b in bookmark.bookmarks if b.method == 'local'], 'Info')
+ def completion_bookmark_local(self, the_input):
+ """Completion for /bookmark_local"""
+ txt = the_input.get_text()
+ args = common.shell_split(txt)
+ n = len(args)
+ if txt.endswith(' '):
+ n += 1
+
+ if len(args) == 1:
+ jid = JID('')
+ else:
+ jid = JID(args[1])
+ if len(args) > 2:
+ return
+ if jid.server and (jid.resource or jid.full.endswith('/')):
+ tab = self.get_tab_by_name(jid.bare, tabs.MucTab)
+ nicks = [tab.own_nick] if tab else []
+ default = os.environ.get('USER') if os.environ.get('USER') else 'poezio'
+ nick = config.get('default_nick', '')
+ if not nick:
+ if not default in nicks:
+ nicks.append(default)
+ else:
+ if not nick in nicks:
+ nicks.append(nick)
+ jids_list = ['%s/%s' % (jid.bare, nick) for nick in nicks]
+ return the_input.auto_completion(jids_list, '')
+ muc_list = [tab.get_name() for tab in self.tabs if isinstance(tab, tabs.MucTab)]
+ muc_list.append('*')
+ return the_input.auto_completion(muc_list, '')
+
def command_bookmark(self, arg=''):
"""
/bookmark [room][/nick] [autojoin] [password]
@@ -2145,6 +1629,41 @@ class Core(object):
self.information(_('Your remote bookmarks are now: %s') %
[b for b in bookmark.bookmarks if b.method in ('pep', 'privatexml')], 'Info')
+ def completion_bookmark(self, the_input):
+ """Completion for /bookmark"""
+ txt = the_input.get_text()
+ args = common.shell_split(txt)
+ n = len(args)
+ if txt.endswith(' '):
+ n += 1
+
+ if len(args) == 1:
+ jid = JID('')
+ else:
+ jid = JID(args[1])
+
+ if len(args) == 2:
+ return the_input.auto_completion(['true', 'false'], '')
+ if len(args) == 3:
+ return
+
+ if jid.server and (jid.resource or jid.full.endswith('/')):
+ tab = self.get_tab_by_name(jid.bare, tabs.MucTab)
+ nicks = [tab.own_nick] if tab else []
+ default = os.environ.get('USER') if os.environ.get('USER') else 'poezio'
+ nick = config.get('default_nick', '')
+ if not nick:
+ if not default in nicks:
+ nicks.append(default)
+ else:
+ if not nick in nicks:
+ nicks.append(nick)
+ jids_list = ['%s/%s' % (jid.bare, nick) for nick in nicks]
+ return the_input.auto_completion(jids_list, '')
+ muc_list = [tab.get_name() for tab in self.tabs if isinstance(tab, tabs.MucTab)]
+ muc_list.append('*')
+ return the_input.auto_completion(muc_list, '')
+
def command_bookmarks(self, arg=''):
"""/bookmarks"""
self.information(_('Your remote bookmarks are: %s') %
@@ -2255,44 +1774,6 @@ class Core(object):
end_list = [config.get(args[2], '', args[1]), '']
return the_input.auto_completion(end_list, '')
- def completion_server_cycle(self, the_input):
- """Completion for /server_cycle"""
- txt = the_input.get_text()
- args = txt.split()
- n = len(args)
- if txt.endswith(' '):
- n += 1
- if n == 2:
- serv_list = []
- for tab in self.tabs:
- if isinstance(tab, tabs.MucTab):
- serv = JID(tab.get_name()).server
- if not serv in serv_list:
- serv_list.append(serv)
- return the_input.auto_completion(serv_list, ' ')
-
- def close_tab(self, tab=None):
- """
- Close the given tab. If None, close the current one
- """
- tab = tab or self.current_tab()
- if isinstance(tab, tabs.RosterInfoTab):
- return # The tab 0 should NEVER be closed
- del tab.key_func # Remove self references
- del tab.commands # and make the object collectable
- tab.on_close()
- self.tabs.remove(tab)
- if tab.get_name() in logger.fds:
- logger.fds[tab.get_name()].close()
- log.debug("Log file for %s closed.", tab.get_name())
- del logger.fds[tab.get_name()]
- self.tabs[0].on_gain_focus()
- self.refresh_window()
- import gc
- gc.collect()
- log.debug('___ Referrers of closing tab:\n%s\n______', gc.get_referrers(tab))
- del tab
-
def command_server_cycle(self, arg=''):
"""
Do a /cycle on each room of the given server. If none, do it on the current tab
@@ -2317,6 +1798,96 @@ class Core(object):
tab.joined = False
self.command_join('"%s/%s"' %(tab.get_name(), tab.own_nick))
+ def completion_server_cycle(self, the_input):
+ """Completion for /server_cycle"""
+ txt = the_input.get_text()
+ args = txt.split()
+ n = len(args)
+ if txt.endswith(' '):
+ n += 1
+ if n == 2:
+ serv_list = []
+ for tab in self.tabs:
+ if isinstance(tab, tabs.MucTab):
+ serv = JID(tab.get_name()).server
+ if not serv in serv_list:
+ serv_list.append(serv)
+ return the_input.auto_completion(serv_list, ' ')
+
+ def command_invite(self, arg):
+ """/invite <to> <room> [reason]"""
+ args = common.shell_split(arg)
+ if len(args) < 2:
+ return
+ reason = args[2] if len(args) > 2 else ''
+ to = args[0]
+ room = args[1]
+ self.xmpp.plugin['xep_0045'].invite(room, to, reason)
+
+ def completion_invite(self, the_input):
+ """Completion for /invite"""
+ txt = the_input.get_text()
+ args = common.shell_split(txt)
+ n = len(args)
+ if txt.endswith(' '):
+ n += 1
+ if n == 2:
+ return the_input.auto_completion([jid for jid in roster.jids()], '')
+ elif n == 3:
+ rooms = []
+ for tab in self.tabs:
+ if isinstance(tab, tabs.MucTab) and tab.joined:
+ rooms.append(tab.get_name())
+ return the_input.auto_completion(rooms, '')
+
+ def command_decline(self, arg):
+ """/decline <room@server.tld> [reason]"""
+ args = common.shell_split(arg)
+ if not len(args):
+ return
+ jid = JID(args[0])
+ if jid.bare not in self.pending_invites:
+ return
+ reason = args[1] if len(args) > 1 else ''
+ del self.pending_invites[jid.bare]
+ self.xmpp.plugin['xep_0045'].decline_invite(jid.bare, self.pending_invites[jid.bare], reason)
+
+ def completion_decline(self, the_input):
+ """Completion for /decline"""
+ txt = the_input.get_text()
+ args = common.shell_split(txt)
+ n = len(args)
+ if txt.endswith(' '):
+ n += 1
+ if n == 2:
+ return the_input.auto_completion(list(self.pending_invites.keys()), '')
+
+ ### Commands without a completion in this class ###
+
+ def command_invitations(self, arg=''):
+ """/invitations"""
+ build = ""
+ for invite in self.pending_invites:
+ build += "%s by %s" % (invite, JID(self.pending_invites[invite]).bare)
+ if self.pending_invites:
+ build = "You are invited to the following rooms:\n" + build
+ else:
+ build = "You are do not have any pending invitation."
+ self.information(build, 'Info')
+
+ def command_quit(self, arg=''):
+ """
+ /quit
+ """
+ if len(arg.strip()) != 0:
+ msg = arg
+ else:
+ msg = None
+ self.disconnect(msg)
+ self.running = False
+ self.reset_curses()
+ sys.exit()
+
def command_bind(self, arg):
"""
Bind a key.
@@ -2343,205 +1914,684 @@ class Core(object):
self.add_tab(new_tab, True)
self.refresh_window()
- def go_to_room_number(self):
+ def command_rawxml(self, arg):
"""
- Read 2 more chars and go to the tab
- with the given number
+ /rawxml <xml stanza>
"""
- char = self.read_keyboard()[0]
- try:
- nb1 = int(char)
- except ValueError:
+ if not arg:
return
- char = self.read_keyboard()[0]
+
try:
- nb2 = int(char)
- except ValueError:
+ StanzaBase(self.xmpp, xml=ET.fromstring(arg)).send()
+ except:
+ import traceback
+ self.information(_('Could not send custom stanza'), 'Error')
+ log.debug(_("Could not send custom stanza:\n") + traceback.format_exc())
+
+ def command_load(self, arg):
+ """
+ /load <plugin>
+ """
+ args = arg.split()
+ if len(args) != 1:
+ self.command_help('load')
return
- self.command_win('%s%s' % (nb1, nb2))
+ filename = args[0]
+ self.plugin_manager.load(filename)
- def information(self, msg, typ=''):
+ def command_unload(self, arg):
"""
- Displays an informational message in the "Info" buffer
+ /unload <plugin>
"""
- nb_lines = self.information_buffer.add_message(msg, nickname=typ)
- if isinstance(self.current_tab(), tabs.RosterInfoTab):
- self.refresh_window()
- elif typ != '' and typ.lower() in config.get('information_buffer_popup_on',
- 'error roster warning help info').split():
- popup_time = config.get('popup_time', 4) + (nb_lines - 1) * 2
- self.pop_information_win_up(nb_lines, popup_time)
- else:
- if self.information_win_size != 0:
- self.information_win.refresh()
- self.current_tab().input.refresh()
+ args = arg.split()
+ if len(args) != 1:
+ self.command_help('unload')
+ return
+ filename = args[0]
+ self.plugin_manager.unload(filename)
- def disconnect(self, msg='', reconnect=False):
+ def command_plugins(self, arg=''):
"""
- Disconnect from remote server and correctly set the states of all
- parts of the client (for example, set the MucTabs as not joined, etc)
+ /plugins
"""
- msg = msg or ''
- for tab in self.tabs:
- if isinstance(tab, tabs.MucTab) and tab.joined:
- tab.command_part(msg)
- self.save_config()
- # Ugly fix thanks to gmail servers
- self.xmpp.disconnect(reconnect)
+ self.information("Plugins currently in use: %s" % repr(list(self.plugin_manager.plugins.keys())), 'Info')
- def command_quit(self, arg=''):
+ def command_message(self, arg):
"""
- /quit
+ /message <jid> [message]
"""
- if len(arg.strip()) != 0:
- msg = arg
- else:
- msg = None
- self.disconnect(msg)
- self.running = False
- self.reset_curses()
- sys.exit()
+ args = common.shell_split(arg)
+ if len(args) < 1:
+ self.command_help('message')
+ return
+ jid = args[0]
+ tab = self.open_conversation_window(jid, focus=True)
+ if len(args) > 1:
+ tab.command_say(args[1])
- def save_config(self):
+ def command_reconnect(self, args=None):
"""
- Save config in the file just before exit
+ /reconnect
"""
- roster.save_to_config_file()
- config.set_and_save('info_win_height', self.information_win_size, 'var')
+ self.disconnect(reconnect=True)
- def do_command(self, key, raw):
- if not key:
+ def command_xml_tab(self, arg=''):
+ """/xml_tab"""
+ self.xml_tabs += 1
+ tab = tabs.XMLTab()
+ self.add_tab(tab, True)
+
+
+
+####################### XMPP Event Handlers ##################################
+
+ ### Invites ###
+
+ def on_groupchat_invite(self, message):
+ jid = message['from']
+ if jid.bare in self.pending_invites:
return
- return self.current_tab().on_input(key, raw)
+ # there are 2 'x' tags in the messages, making message['x'] useless
+ invite = StanzaBase(self.xmpp, xml=message.find('{http://jabber.org/protocol/muc#user}x/{http://jabber.org/protocol/muc#user}invite'))
+ inviter = invite['from']
+ reason = invite['reason']
+ password = invite['password']
+ msg = "You are invited to the room %s by %s" % (jid.full, inviter.full)
+ if reason:
+ msg += "because: %s" % reason
+ if password:
+ msg += ". The password is \"%s\"." % password
+ self.information(msg, 'Info')
+ if 'invite' in config.get('beep_on', 'invite').split():
+ curses.beep()
+ self.pending_invites[jid.bare] = inviter.full
- def on_roster_enter_key(self, roster_row):
+ def on_groupchat_decline(self, decline):
+ pass
+
+ ### "classic" messages ###
+
+ def on_message(self, message):
"""
- when enter is pressed on the roster window
+ When receiving private message from a muc OR a normal message
+ (from one of our contacts)
"""
- if isinstance(roster_row, Contact):
- if not self.get_conversation_by_jid(roster_row.bare_jid):
- self.open_conversation_window(roster_row.bare_jid)
+ if message.find('{http://jabber.org/protocol/muc#user}x/{http://jabber.org/protocol/muc#user}invite') != None:
+ return
+ if message['type'] == 'groupchat':
+ return
+ # Differentiate both type of messages, and call the appropriate handler.
+ jid_from = message['from']
+ for tab in self.tabs:
+ if tab.get_name() == jid_from.bare and isinstance(tab, tabs.MucTab):
+ if message['type'] == 'error':
+ return self.room_error(message, jid_from)
+ else:
+ return self.on_groupchat_private_message(message)
+ return self.on_normal_message(message)
+
+ def on_normal_message(self, message):
+ """
+ When receiving "normal" messages (from someone in our roster)
+ """
+ jid = message['from']
+ body = xhtml.get_body_from_message_stanza(message)
+ if message['type'] == 'error':
+ return self.information(self.get_error_message(message, deprecated=True), 'Error')
+ if not body:
+ return
+ conversation = self.get_tab_of_conversation_with_jid(jid, create=True)
+ self.events.trigger('conversation_msg', message, conversation)
+ body = xhtml.get_body_from_message_stanza(message)
+ if jid.bare in roster:
+ remote_nick = roster[jid.bare].name or jid.user
+ else:
+ remote_nick = jid.user
+ delay_tag = message.find('{urn:xmpp:delay}delay')
+ if delay_tag is not None:
+ delayed = True
+ date = common.datetime_tuple(delay_tag.attrib['stamp'])
+ else:
+ delayed = False
+ date = None
+ conversation._text_buffer.add_message(body, date, nickname=remote_nick, nick_color=get_theme().COLOR_REMOTE_USER, history=delayed)
+ if conversation.remote_wants_chatstates is None and not delayed:
+ if message['chat_state']:
+ conversation.remote_wants_chatstates = True
else:
- self.focus_tab_named(roster_row.bare_jid)
- if isinstance(roster_row, Resource):
- if not self.get_conversation_by_jid(roster_row.jid.full):
- self.open_conversation_window(roster_row.jid.full)
+ conversation.remote_wants_chatstates = False
+ logger.log_message(jid.bare, remote_nick, body)
+ if 'private' in config.get('beep_on', 'highlight private').split():
+ if config.get_by_tabname('disable_beep', 'false', jid.bare, False).lower() != 'true':
+ curses.beep()
+ if self.current_tab() is not conversation:
+ conversation.state = 'private'
+ self.refresh_tab_win()
+ else:
+ self.refresh_window()
+
+ def on_groupchat_message(self, message):
+ """
+ Triggered whenever a message is received from a multi-user chat room.
+ """
+ if message['subject']:
+ return
+ delay_tag = message.find('{urn:xmpp:delay}delay')
+ if delay_tag is not None:
+ delayed = True
+ date = common.datetime_tuple(delay_tag.attrib['stamp'])
+ else:
+ # We support the OLD and deprecated XEP: http://xmpp.org/extensions/xep-0091.html
+ # But it sucks, please, Jabber servers, don't do this :(
+ delay_tag = message.find('{jabber:x:delay}x')
+ if delay_tag is not None:
+ delayed = True
+ date = common.datetime_tuple(delay_tag.attrib['stamp'])
else:
- self.focus_tab_named(roster_row.jid.full)
- self.refresh_window()
+ delayed = False
+ date = None
+ nick_from = message['mucnick']
+ room_from = message.getMucroom()
+ if message['type'] == 'error': # Check if it's an error
+ return self.room_error(message, room_from)
+ tab = self.get_tab_by_name(room_from, tabs.MucTab)
+ old_state = tab.state
+ if not tab:
+ self.information(_("message received for a non-existing room: %s") % (room_from))
+ return
+ if tab.get_user_by_name(nick_from) and\
+ tab.get_user_by_name(nick_from) in tab.ignores:
+ return
+ self.events.trigger('muc_msg', message, tab)
+ body = xhtml.get_body_from_message_stanza(message)
+ if body:
+ date = date if delayed == True else None
+ if tab.add_message(body, date, nick_from, history=True if date else False):
+ self.events.trigger('highlight', message, tab)
+ if tab is self.current_tab():
+ tab.text_win.refresh()
+ tab.info_header.refresh(tab, tab.text_win)
+ tab.input.refresh()
+ self.doupdate()
+ elif tab.state != old_state:
+ self.refresh_tab_win()
+ self.current_tab().input.refresh()
+ self.doupdate()
+ if 'message' in config.get('beep_on', 'highlight private').split():
+ if config.get_by_tabname('disable_beep', 'false', room_from, False).lower() != 'true':
+ curses.beep()
- def remove_timed_event(self, event):
- """Remove an existing timed event"""
- if event and event in self.timed_events:
- self.timed_events.remove(event)
+ def on_groupchat_private_message(self, message):
+ """
+ We received a Private Message (from someone in a Muc)
+ """
+ jid = message['from']
+ nick_from = jid.resource
+ room_from = jid.bare
+ body = xhtml.get_body_from_message_stanza(message)
+ tab = self.get_tab_by_name(jid.full, tabs.PrivateTab) # get the tab with the private conversation
+ ignore = config.get_by_tabname('ignore_private', 'false',
+ room_from).lower() == 'true'
+ if not tab: # It's the first message we receive: create the tab
+ if body and not ignore:
+ tab = self.open_private_window(room_from, nick_from, False)
+ if ignore:
+ self.events.trigger('ignored_private', message, tab)
+ msg = config.get_by_tabname('private_auto_response', None, room_from)
+ if msg and body:
+ self.xmpp.send_message(mto=jid.full, mbody=msg, mtype='chat')
+ return
+ self.events.trigger('private_msg', message, tab)
+ if not body or not tab:
+ return
+ tab.add_message(body, time=None, nickname=nick_from,
+ forced_user=self.get_tab_by_name(room_from, tabs.MucTab).get_user_by_name(nick_from))
+ conversation = self.get_tab_by_name(jid.full, tabs.PrivateTab)
+ if conversation and conversation.remote_wants_chatstates is None:
+ if message['chat_state']:
+ conversation.remote_wants_chatstates = True
+ else:
+ conversation.remote_wants_chatstates = False
+ if 'private' in config.get('beep_on', 'highlight private').split():
+ if config.get_by_tabname('disable_beep', 'false', jid.full, False).lower() != 'true':
+ curses.beep()
+ logger.log_message(jid.full.replace('/', '\\'), nick_from, body)
+ if conversation is self.current_tab():
+ self.refresh_window()
+ else:
+ conversation.state = 'private'
+ self.refresh_tab_win()
- def add_timed_event(self, event):
- """Add a new timed event"""
- self.timed_events.add(event)
+ ### Chatstates ###
- def check_timed_events(self):
- """Check for the execution of timed events"""
- now = datetime.now()
- for event in self.timed_events:
- if event.has_timed_out(now):
- res = event()
- if not res:
- self.timed_events.remove(event)
- break
+ def on_chatstate_active(self, message):
+ self.on_chatstate(message, "active")
+
+ def on_chatstate_inactive(self, message):
+ self.on_chatstate(message, "inactive")
+
+ def on_chatstate_composing(self, message):
+ self.on_chatstate(message, "composing")
+
+ def on_chatstate_paused(self, message):
+ self.on_chatstate(message, "paused")
+
+ def on_chatstate_gone(self, message):
+ self.on_chatstate(message, "gone")
+
+ def on_chatstate(self, message, state):
+ if message['type'] == 'chat':
+ if not self.on_chatstate_normal_conversation(message, state):
+ tab = self.get_tab_by_name(message['from'].full, tabs.PrivateTab)
+ if not tab:
+ return
+ self.on_chatstate_private_conversation(message, state)
+ elif message['type'] == 'groupchat':
+ self.on_chatstate_groupchat_conversation(message, state)
- def execute(self,line):
+ def on_chatstate_normal_conversation(self, message, state):
+ tab = self.get_tab_of_conversation_with_jid(message['from'], False)
+ if not tab:
+ return False
+ self.events.trigger('normal_chatstate', message, tab)
+ tab.chatstate = state
+ if tab == self.current_tab():
+ tab.refresh_info_header()
+ self.doupdate()
+ return True
+
+ def on_chatstate_private_conversation(self, message, state):
"""
- Execute the /command or just send the line on the current room
+ Chatstate received in a private conversation from a MUC
"""
- if line == "":
+ tab = self.get_tab_by_name(message['from'].full, tabs.PrivateTab)
+ if not tab:
return
- if line.startswith('/'):
- command = line.strip()[:].split()[0][1:]
- arg = line[2+len(command):] # jump the '/' and the ' '
- # example. on "/link 0 open", command = "link" and arg = "0 open"
- if command in self.commands:
- func = self.commands[command][0]
- func(arg)
- return
+ self.events.trigger('private_chatstate', message, tab)
+ tab.chatstate = state
+ if tab == self.current_tab():
+ tab.refresh_info_header()
+ self.doupdate()
+ return True
+
+ def on_chatstate_groupchat_conversation(self, message, state):
+ """
+ Chatstate received in a MUC
+ """
+ nick = message['mucnick']
+ room_from = message.getMucroom()
+ tab = self.get_tab_by_name(room_from, tabs.MucTab)
+ if tab and tab.get_user_by_name(nick):
+ self.events.trigger('muc_chatstate', message, tab)
+ tab.get_user_by_name(nick).chatstate = state
+ if tab == self.current_tab():
+ tab.user_win.refresh(tab.users)
+ tab.input.refresh()
+ self.doupdate()
+
+ ### subscription-related handlers ###
+
+ def on_roster_update(self, iq):
+ """
+ The roster was received.
+ """
+ for item in iq['roster']:
+ jid = item['jid']
+ if item['subscription'] == 'remove':
+ del roster[jid]
else:
- self.information(_("Unknown command (%s)") % (command), _('Error'))
+ roster.update_contact_groups(jid)
+ if isinstance(self.current_tab(), tabs.RosterInfoTab):
+ self.refresh_window()
- def doupdate(self):
- if not self.running or self.background is True:
+ def on_subscription_request(self, presence):
+ """subscribe received"""
+ jid = presence['from'].bare
+ contact = roster[jid]
+ if contact.subscription in ('from', 'both'):
return
- curses.doupdate()
+ elif contact.subscription == 'to':
+ self.xmpp.sendPresence(pto=jid, ptype='subscribed')
+ self.xmpp.sendPresence(pto=jid)
+ else:
+ roster.update_contact_groups(contact)
+ contact.pending_in = True
+ self.information('%s wants to subscribe to your presence' % jid, 'Roster')
+ self.get_tab_by_number(0).state = 'highlight'
+ if isinstance(self.current_tab(), tabs.RosterInfoTab):
+ self.refresh_window()
- def send_message(self, msg):
+ def on_subscription_authorized(self, presence):
+ """subscribed received"""
+ jid = presence['from'].bare
+ contact = roster[jid]
+ if contact.subscription not in ('both', 'from'):
+ self.information('%s accepted your contact proposal' % jid, 'Roster')
+ if contact.pending_out:
+ contact.pending_out = False
+ if isinstance(self.current_tab(), tabs.RosterInfoTab):
+ self.refresh_window()
+
+ def on_subscription_remove(self, presence):
+ """unsubscribe received"""
+ jid = presence['from'].bare
+ contact = roster[jid]
+ if not contact:
+ return
+ self.information('%s does not want to receive your status anymore.' % jid, 'Roster')
+ self.get_tab_by_number(0).state = 'highlight'
+ if isinstance(self.current_tab(), tabs.RosterInfoTab):
+ self.refresh_window()
+
+ def on_subscription_removed(self, presence):
+ """unsubscribed received"""
+ jid = presence['from'].bare
+ contact = roster[jid]
+ if not contact:
+ return
+ if contact.pending_out:
+ self.information('%s rejected your contact proposal' % jid, 'Roster')
+ contact.pending_out = False
+ else:
+ self.information('%s does not want you to receive his/her/its status anymore.'%jid, 'Roster')
+ self.get_tab_by_number(0).state = 'highlight'
+ if isinstance(self.current_tab(), tabs.RosterInfoTab):
+ self.refresh_window()
+
+ ### Presence-related handlers ###
+
+ def on_presence(self, presence):
+ jid = presence['from']
+ contact = roster[jid.bare]
+ if contact is None:
+ return
+ self.events.trigger('normal_presence', presence, contact[jid.full])
+ tab = self.get_tab_of_conversation_with_jid(jid, create=False)
+ if isinstance(self.current_tab(), tabs.RosterInfoTab):
+ self.refresh_window()
+ elif self.current_tab() == tab:
+ tab.refresh()
+ self.doupdate()
+
+ def on_got_offline(self, presence):
"""
- Function to use in plugins to send a message in the current conversation.
- Returns False if the current tab is not a conversation tab
+ A JID got offline
"""
- if not isinstance(self.current_tab(), tabs.ChatTab):
- return False
- self.current_tab().command_say(msg)
- return True
+ jid = presence['from']
+ logger.log_roster_change(jid.bare, 'got offline')
+ # If a resource got offline, display the message in the conversation with this
+ # precise resource.
+ if jid.resource:
+ self.add_information_message_to_conversation_tab(jid.full, '\x195}%s is \x191}offline' % (jid.full))
+ if jid.server in roster.blacklist:
+ return
+ self.add_information_message_to_conversation_tab(jid.bare, '\x195}%s is \x191}offline' % (jid.bare))
+ self.information('\x193}%s \x195}is \x191}offline' % (jid.bare), 'Roster')
+ if isinstance(self.current_tab(), tabs.RosterInfoTab):
+ self.refresh_window()
- def exec_command(self, command):
+ def on_got_online(self, presence):
+ jid = presence['from']
+ contact = roster[jid.bare]
+ if contact is None:
+ # Todo, handle presence coming from contacts not in roster
+ return
+ logger.log_roster_change(jid.bare, 'got online')
+ resource = Resource(jid.full, {
+ 'priority': presence.get_priority() or 0,
+ 'status': presence['status'],
+ 'show': presence['show'],
+ })
+ self.events.trigger('normal_presence', presence, resource)
+ self.add_information_message_to_conversation_tab(jid.full, '\x195}%s is \x194}online' % (jid.full))
+ if time.time() - self.connection_time > 20:
+ # We do not display messages if we recently logged in
+ if presence['status']:
+ self.information("\x193}%s \x195}is \x194}online\x195} (\x19o%s\x195})" % (resource.jid.bare, presence['status']), "Roster")
+ else:
+ self.information("\x193}%s \x195}is \x194}online\x195}" % resource.jid.bare, "Roster")
+ self.add_information_message_to_conversation_tab(jid.bare, '\x195}%s is \x194}online' % (jid.bare))
+ if isinstance(self.current_tab(), tabs.RosterInfoTab):
+ self.refresh_window()
+
+ def on_groupchat_presence(self, presence):
"""
- Execute an external command on the local or a remote
- machine, depending on the conf. For example, to open a link in a
- browser, do exec_command("firefox http://poezio.eu"),
- and this will call the command on the correct computer.
- The remote execution is done by writing the command on a fifo.
- That fifo has to be on the machine where poezio is running, and
- accessible (through sshfs for example) from the local machine (where
- poezio is not running). A very simple daemon reads on that fifo,
- and executes any command that is read in it.
+ Triggered whenever a presence stanza is received from a user in a multi-user chat room.
+ Display the presence on the room window and update the
+ presence information of the concerned user
"""
- command = '%s\n' % (command,)
- if config.get('exec_remote', 'false') == 'true':
- # We just write the command in the fifo
- if not self.remote_fifo:
- try:
- self.remote_fifo = Fifo(os.path.join(config.get('remote_fifo_path', './'), 'poezio.fifo'), 'w')
- except (OSError, IOError) as e:
- self.information('Could not open fifo file for writing: %s' % (e,), 'Error')
- return
- try:
- self.remote_fifo.write(command)
- except (IOError) as e:
- self.information('Could not execute [%s]: %s' % (command.strip(), e,), 'Error')
- self.remote_fifo = None
+ from_room = presence['from'].bare
+ tab = self.get_tab_by_name(from_room, tabs.MucTab)
+ if tab:
+ self.events.trigger('muc_presence', presence, tab)
+ tab.handle_presence(presence)
+
+
+ ### Connection-related handlers ###
+
+ def on_failed_connection(self):
+ """
+ We cannot contact the remote server
+ """
+ self.information(_("Connection to remote server failed"))
+
+ def on_disconnected(self, event):
+ """
+ When we are disconnected from remote server
+ """
+ for tab in self.tabs:
+ if isinstance(tab, tabs.MucTab):
+ tab.disconnect()
+ self.information(_("Disconnected from server."))
+
+ def on_failed_auth(self, event):
+ """
+ Authentication failed
+ """
+ self.information(_("Authentication failed."))
+
+ def on_connected(self, event):
+ """
+ Remote host responded, but we are not yet authenticated
+ """
+ self.information(_("Connected to server."))
+
+ def on_session_start(self, event):
+ """
+ Called when we are connected and authenticated
+ """
+ self.connection_time = time.time()
+ if not self.plugins_autoloaded: # Do not reload plugins on reconnection
+ self.autoload_plugins()
+ self.information(_("Authentication success."))
+ self.information(_("Your JID is %s") % self.xmpp.boundjid.full)
+ if not self.xmpp.anon:
+ # request the roster
+ self.xmpp.get_roster()
+ # send initial presence
+ if config.get('send_initial_presence', 'true').lower() == 'true':
+ pres = self.xmpp.make_presence()
+ self.events.trigger('send_normal_presence', pres)
+ pres.send()
+ bookmark.get_local()
+ if not self.xmpp.anon and not config.get('use_remote_bookmarks', 'true').lower() == 'false':
+ bookmark.get_remote(self.xmpp)
+ for bm in [item for item in bookmark.bookmarks if item.autojoin]:
+ tab = self.get_tab_by_name(bm.jid, tabs.MucTab)
+ if not tab:
+ self.open_new_room(bm.jid, bm.nick, False)
+ nick = bm.nick if bm.nick else self.own_nick
+ self.initial_joins.append(bm.jid)
+ muc.join_groupchat(self.xmpp, bm.jid, nick)
+
+ ### Other handlers ###
+
+ def on_status_codes(self, message):
+ """
+ Handle groupchat messages with status codes.
+ Those are received when a room configuration change occurs.
+ """
+ room_from = message['from']
+ tab = self.get_tab_by_name(room_from, tabs.MucTab)
+ status_codes = set([s.attrib['code'] for s in message.findall('{%s}x/{%s}status' % (tabs.NS_MUC_USER, tabs.NS_MUC_USER))])
+ if '101' in status_codes:
+ self.information('Your affiliation in the room %s changed' % room_from, 'Info')
+ elif tab and status_codes:
+ show_unavailable = '102' in status_codes
+ hide_unavailable = '103' in status_codes
+ non_priv = '104' in status_codes
+ logging_on = '170' in status_codes
+ logging_off= '171' in status_codes
+ non_anon = '172' in status_codes
+ semi_anon = '173' in status_codes
+ full_anon = '174' in status_codes
+ modif = False
+ if show_unavailable or hide_unavailable or non_priv or logging_off\
+ or non_anon or semi_anon or full_anon:
+ tab.add_message('\x19%(info_col)s}Info: A configuration change not privacy-related occured.' % {'info_col': get_theme().COLOR_INFORMATION_TEXT[0]})
+ modif = True
+ if show_unavailable:
+ tab.add_message('\x19%(info_col)s}Info: The unavailable members are now shown.' % {'info_col': get_theme().COLOR_INFORMATION_TEXT[0]})
+ elif hide_unavailable:
+ tab.add_message('\x19%(info_col)s}Info: The unavailable members are now hidden.' % {'info_col': get_theme().COLOR_INFORMATION_TEXT[0]})
+ if non_anon:
+ tab.add_message('\x191}Warning:\x19%(info_col)s} The room is now not anonymous. (public JID)' % {'info_col': get_theme().COLOR_INFORMATION_TEXT[0]})
+ elif semi_anon:
+ tab.add_message('\x19%(info_col)s}Info: The room is now semi-anonymous. (moderators-only JID)' % {'info_col': get_theme().COLOR_INFORMATION_TEXT[0]})
+ elif full_anon:
+ tab.add_message('\x19%(info_col)s}Info: The room is now fully anonymous.' % {'info_col': get_theme().COLOR_INFORMATION_TEXT[0]})
+ if logging_on:
+ tab.add_message('\x191}Warning: \x19%(info_col)s}This room is publicly logged' % {'info_col': get_theme().COLOR_INFORMATION_TEXT[0]})
+ elif logging_off:
+ tab.add_message('\x19%(info_col)s}Info: This room is not logged anymore.' % {'info_col': get_theme().COLOR_INFORMATION_TEXT[0]})
+ if modif:
+ self.refresh_window()
+
+ def on_groupchat_subject(self, message):
+ """
+ Triggered when the topic is changed.
+ """
+ nick_from = message['mucnick']
+ room_from = message.getMucroom()
+ tab = self.get_tab_by_name(room_from, tabs.MucTab)
+ subject = message['subject']
+ if not subject or not tab:
+ return
+ if nick_from:
+ self.add_message_to_text_buffer(tab._text_buffer,
+ _("\x19%(info_col)s}%(nick)s set the subject to: %(subject)s") %
+ {'info_col': get_theme().COLOR_INFORMATION_TEXT[0], 'nick':nick_from, 'subject':subject},
+ time=None)
else:
- e = Executor(command.strip())
- try:
- e.start()
- except ValueError as e: # whenever shlex fails
- self.information('%s' % (e,), 'Error')
+ self.add_message_to_text_buffer(tab._text_buffer, _("\x19%(info_col)s}The subject is: %(subject)s") %
+ {'subject':subject, 'info_col': get_theme().COLOR_INFORMATION_TEXT[0]},
+ time=None)
+ tab.topic = subject
+ if self.get_tab_by_name(room_from, tabs.MucTab) is self.current_tab():
+ self.refresh_window()
- def get_conversation_messages(self):
+ def on_data_form(self, message):
"""
- Returns a list of all the messages in the current chat.
- If the current tab is not a ChatTab, returns None.
+ When a data form is received
+ """
+ self.information('%s' % message)
- Messages are namedtuples of the form
- ('txt nick_color time str_time nickname user')
+ def on_attention(self, message):
"""
- if not isinstance(self.current_tab(), tabs.ChatTab):
- return None
- return self.current_tab().get_conversation_messages()
+ Attention probe received.
+ """
+ jid_from = message['from']
+ self.information('%s requests your attention!' % jid_from, 'Info')
+ for tab in self.tabs:
+ if tab.get_name() == jid_from:
+ tab.state = 'attention'
+ self.refresh_tab_win()
+ return
+ for tab in self.tabs:
+ if tab.get_name() == jid_from.bare:
+ tab.state = 'attention'
+ self.refresh_tab_win()
+ return
+ self.information('%s tab not found.' % jid_from, 'Error')
- def insert_input_text(self, text):
+ def room_error(self, error, room_name):
"""
- Insert the given text into the current input
+ Display the error in the tab
"""
- self.do_command(text, True)
+ tab = self.get_tab_by_name(room_name)
+ error_message = self.get_error_message(error)
+ self.add_message_to_text_buffer(tab._text_buffer, error_message)
+ code = error['error']['code']
+ if code == '401':
+ msg = _('To provide a password in order to join the room, type "/join / password" (replace "password" by the real password)')
+ self.add_message_to_text_buffer(tab._text_buffer, msg)
+ if code == '409':
+ if config.get('alternative_nickname', '') != '':
+ self.command_join('%s/%s'% (tab.name, tab.own_nick+config.get('alternative_nickname', '')))
+ else:
+ self.add_message_to_text_buffer(tab._text_buffer, _('You can join the room with an other nick, by typing "/join /other_nick"'))
+ self.refresh_window()
- def try_execute(self, line):
+ def outgoing_stanza(self, stanza):
"""
- Try to execute a command in the current tab
+ We are sending a new stanza, write it in the xml buffer if needed.
"""
- line = '/' + line
- try:
- self.current_tab().execute_command(line)
- except:
- import traceback
- log.debug('Execute failed:\n%s', traceback.format_exc())
+ if self.xml_tabs:
+ self.add_message_to_text_buffer(self.xml_buffer, '\x191}<--\x19o %s' % stanza)
+ if isinstance(self.current_tab(), tabs.XMLTab):
+ self.current_tab().refresh()
+ self.doupdate()
+
+ def incoming_stanza(self, stanza):
+ """
+ We are receiving a new stanza, write it in the xml buffer if needed.
+ """
+ if self.xml_tabs:
+ self.add_message_to_text_buffer(self.xml_buffer, '\x192}-->\x19o %s' % stanza)
+ if isinstance(self.current_tab(), tabs.XMLTab):
+ self.current_tab().refresh()
+ self.doupdate()
+
+ def validate_ssl(self, pem):
+ """
+ Check the server certificate using the sleekxmpp ssl_cert event
+ """
+ if config.get('ignore_certificate', 'false').lower() == 'true':
+ return
+ cert = config.get('certificate', '')
+ der = ssl.PEM_cert_to_DER_cert(pem)
+ found_cert = sha1(der).hexdigest()
+ if cert:
+ if found_cert == cert:
+ log.debug('Cert %s OK', found_cert)
+ return
+ else:
+ saved_input = self.current_tab().input
+ log.debug('\nWARNING: CERTIFICATE CHANGED old: %s, new: %s\n', cert, found_cert)
+ input = windows.YesNoInput(text="WARNING! Certificate hash changed to %s. Accept? (y/n)" % found_cert)
+ self.current_tab().input = input
+ input.resize(1, self.current_tab().width, self.current_tab().height-1, 0)
+ input.refresh()
+ self.doupdate()
+ self.paused = True
+ while input.value is None:
+ pass
+ self.current_tab().input = saved_input
+ self.paused = False
+ if input.value:
+ self.information('Setting new certificate: old: %s, new: %s' % (cert, found_cert), 'Info')
+ log.debug('Setting certificate to %s', found_cert)
+ config.set_and_save('certificate', found_cert)
+ else:
+ self.information('You refused to validate the certificate. You are now disconnected', 'Info')
+ self.xmpp.disconnect()
+ else:
+ log.debug('First time. Setting certificate to %s', found_cert)
+ config.set_and_save('certificate', found_cert)
+
+
+
+
class KeyDict(dict):
"""