diff --git a/lib/ext/wocky b/lib/ext/wocky index 97faaaf..352247c 160000 --- a/lib/ext/wocky +++ b/lib/ext/wocky @@ -1 +1 @@ -Subproject commit 97faaafa088f496815ca7ca833a52da2ab8143ee +Subproject commit 352247c52b89b5a58c02f8675a95ea64cc3724d2 diff --git a/plugins/telepathy-gabble-xmpp-console b/plugins/telepathy-gabble-xmpp-console index cc25a9d..b8bffed 100755 --- a/plugins/telepathy-gabble-xmpp-console +++ b/plugins/telepathy-gabble-xmpp-console @@ -3,10 +3,10 @@ """ The world's worst XMPP console user interface. -Pass it a Gabble account name; type some words; get minimalistic +Pass it the bus name of a Gabble connection; type some words; get minimalistic error reporting. -Copyright © 2011–2013 Collabora Ltd. +Copyright © 2011 Collabora Ltd. This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public @@ -22,17 +22,49 @@ You should have received a copy of the GNU Lesser General Public License along with this library; if not, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA """ +from __future__ import print_function +import collections +import os import sys +import re from xml.dom import minidom +from xml.etree import ElementTree -from gi.repository import Gtk, GLib, Gio, GtkSource -from gi.repository import TelepathyGLib as Tp +import gi +gi.require_version("Gtk", "3.0") +gi.require_version("GtkSource", "3.0") + +from gi.repository import Gtk +from gi.repository import GLib +from gi.repository import Gio +from gi.repository import GtkSource +from gi.repository import GObject PADDING = 6 +from pprint import pprint,pformat + +def pathify(name): + return '/' + name.replace('.', '/') + +def nameify(path): + return (path[1:]).replace('/', '.') + +CONN_FUTURE_IFACE = "org.freedesktop.Telepathy.Connection.FUTURE" CONSOLE_IFACE = "org.freedesktop.Telepathy.Gabble.Plugin.Console" +DISCO_INFO_NS = 'http://jabber.org/protocol/disco#info' +DISCO_ITEMS_NS = 'http://jabber.org/protocol/disco#items' +COMMANDS_NS = 'http://jabber.org/protocol/commands' +JABBER_X_DATA_NS = 'jabber:x:data' +MUC_NS = 'http://jabber.org/protocol/muc' + +def NS(namespace, term): + """Format namespace+terms to match element tree tags + """ + return '{' + namespace + '}' + term + class StanzaViewer(Gtk.ScrolledWindow): def __init__(self): Gtk.ScrolledWindow.__init__(self) @@ -89,6 +121,7 @@ class SpinWrapper(Gtk.Notebook): self.spinner.stop() self.set_current_page(self.PRIMARY_PAGE) + class Page(Gtk.Grid): def __init__(self, console_proxy): Gtk.Grid.__init__(self) @@ -287,38 +320,700 @@ class SnoopyPage(Page): self.stanza_viewer.append_comment('sent' if outgoing else 'received') self.stanza_viewer.append_stanza(xml) +class AdHocPage(Page): + __gsignals__ = { + 'receive-iq': (GObject.SignalFlags.RUN_FIRST, None, (object, object, object)) + } + + def __init__(self, console_proxy): + Page.__init__(self, console_proxy) + + self._jid_heading = {} + self.session_id = None + self.form = JabberXForm() + #self.note = CommandNote() + self.node = None + self.status = None + self.sessionid = None + self.commands_model = Gtk.TreeStore(str, str) + self.commands_model.append(None, ["", ""]) + + # Title + request_label = self.add_title("Ad-Hoc Commands") + + # Jid to Query + recipient_label, recipient_entry = self.add_label_entry_pair( + 'To:', below=request_label) + recipient_entry.set_icon_from_stock( + Gtk.EntryIconPosition.SECONDARY, Gtk.STOCK_GO_FORWARD) + recipient_entry.set_icon_tooltip_text( + Gtk.EntryIconPosition.SECONDARY, "Request Ad-Hoc Commands") + recipient_entry.set_text('ghic.org') + self.recipient_label = recipient_label + self.recipient_entry = recipient_entry + #recipient_entry.connect('activate', self.request_commands) + #recipient_entry.connect('icon-release', self.request_commands) + recipient_entry.connect('activate', self.start_browsing) + recipient_entry.connect('icon-release', self.start_browsing) + + # List commands + self.commands_list = Gtk.TreeView(self.commands_model) + self.commands_list.set_property('activate-on-single-click', False) + self.commands_list.connect('row-activated', self.send_command) + renderer = Gtk.CellRendererText() + column = Gtk.TreeViewColumn("Command", renderer, text=0) + self.commands_list.append_column(column) + self.commands_stack = Gtk.Stack() + self.commands_stack.add_named(self.commands_list, 'browser') + self.commands_stack.add_named(self.form, 'form') + #self.commands_stack.add_named(self.notes, 'note') + + self.commands_window = Gtk.ScrolledWindow() + self.commands_window.set_property('expand', True) + self.commands_window.add(self.commands_stack) +# self.attach_next_to(self.commands_window, recipient_label, Gtk.PositionType.BOTTOM, 2, 1) + + self.result_nb = SpinWrapper(self.commands_window) + + self.attach_next_to(self.result_nb, self.recipient_label, Gtk.PositionType.BOTTOM, 2, 1) + + box = Gtk.HBox() + self.back = Gtk.Button.new_from_stock(Gtk.STOCK_GO_BACK) + self.back.set_sensitive(False) + self.cancel = Gtk.Button.new_from_stock(Gtk.STOCK_CANCEL) + self.forward = Gtk.Button.new_from_stock(Gtk.STOCK_GO_FORWARD) + #self.forward.set_sensitive(False) + box.pack_start(self.back, True, True, 0) + box.pack_start(self.cancel, True, True, 0) + box.pack_start(self.forward, True, True, 0) + self.attach_next_to(box, self.result_nb, Gtk.PositionType.BOTTOM, 2, 1) + + self.cancel.connect('clicked', self.cancel_command) + self.forward.connect('clicked', self.forward_clicked) + + self.start_browsing() + #self.request_commands() + + def add_label_entry_pair(self, title, below): + label = self.add_label(title, below) + + entry = Gtk.Entry() + entry.set_property('margin-right', PADDING) + entry.set_property('hexpand', True) + + self.attach_next_to(entry, label, Gtk.PositionType.RIGHT, 1, 1) + + return label, entry + + def start_browsing(self, *misc): + self.commands_stack.set_visible_child_name('browser') + self.commands_model.clear() + to = self.recipient_entry.get_text() + print('hi', to) + self._jid_heading[to] = self.commands_model.append(None, [to, None]) + self.start_disco_info() + + def forward_clicked(self, button): + visible = self.commands_stack.get_visible_child_name() + print('visible', visible) + if visible == 'browser': + selection = self.commands_list.get_selection() + if selection: + model, selections = selection.get_selected_rows() + print (selections) + if len(selections) == 1: + self.send_command(self.commands_list, selections[0], None) + else: + print("Can't select multiple commands") + elif visible == 'form': + self.send_form() + + def start_disco_info(self, to=None): + if to is None: + to = self.recipient_entry.get_text() + body = "".format(DISCO_INFO_NS) + self.console_proxy.SendIQ('(sss)', 'get', to, body, + result_handler=self.disco_info_cb, + user_data=to) + self.result_nb.start_spinning() + + def disco_info_cb(self, proxy, result, user_data): + to = user_data + print('disco_info_cb', to) + self.emit('receive-iq', proxy, result, user_data) + if isinstance(result, Exception): + return + + iq = self.parse_iq(result) + for child in iq.getchildren(): + if child.tag == NS(DISCO_INFO_NS, 'query'): + for item in child.getchildren(): + if item.tag == NS(DISCO_INFO_NS, 'feature'): + var = item.attrib['var'] + if var == COMMANDS_NS: + self.request_commands(to=to) + if var == DISCO_ITEMS_NS: + self.start_disco_items(to=to) + if item.tag == NS(DISCO_INFO_NS, 'identity'): + print(item.attrib) + + self.result_nb.stop_spinning() + + def start_disco_items(self, to=None): + print('start_disco_items', to) + if to is None: + to = self.recipient_entry.get_text() + body = "".format(DISCO_ITEMS_NS) + self.console_proxy.SendIQ('(sss)', 'get', to, body, + result_handler=self.disco_items_cb, + user_data=to) + + def disco_items_cb(self, proxy, results, user_data): + to = user_data + print('disco_items_cb', to) + iq = self.parse_iq(results) + for query in iq.getchildren(): + if query.tag == NS(DISCO_ITEMS_NS, 'query'): + for child in query.getchildren(): + if child.tag == NS(DISCO_ITEMS_NS, 'item'): + jid = child.attrib['jid'] + #self.start_disco_info(to=jid) + i = self.commands_model.append(None, [jid, None]) + self._jid_heading[jid] = i + + def request_commands(self, to=None, *misc): + print('request') + body = "".format(DISCO_ITEMS_NS, COMMANDS_NS) + print(body) + if to is None: + to = self.recipient_entry.get_text() + + self.console_proxy.SendIQ('(sss)', 'get', to, body, + result_handler=self.receive_commands_cb, + user_data=to) + #self.result_nb.start_spinning() + + def receive_commands_cb(self, proxy, result, user_data): + to = user_data + print('recieve commands', to) + self.emit('receive-iq', proxy, result, user_data) + if isinstance(result, Exception): + return + + reply_type, reply = result + iq = ElementTree.fromstring(reply) + print(reply) + query = iq.getchildren() + if (query[0].tag == NS(DISCO_ITEMS_NS, 'query') and + query[0].attrib['node'] == COMMANDS_NS): + # we have command list? + for item in query[0].getchildren(): + if item.tag == NS(DISCO_ITEMS_NS, 'item'): + name = item.attrib['name'] + command = item.attrib['node'] + print(name, command) + i = self._jid_heading[to] + self.commands_model.append(i, [name, command]) + + self.emit('receive-iq', proxy, result, user_data) + self.result_nb.stop_spinning() + + def send_command(self, tree_view, path, column): + row_iter = self.commands_model.get_iter(path) + self.node = self.commands_model.get_value(row_iter, 1) + body = "".format( + COMMANDS_NS, + self.node) + to = self.recipient_entry.get_text() + self.console_proxy.SendIQ('(sss)', 'set', to, body, + result_handler=self.sent_command_cb) + + def sent_command_cb(self, proxy, result, user_data): + if not isinstance(result, Exception): + reply_type, reply = result + self.parse_command_reply(reply) + + self.emit('receive-iq', proxy, result, user_data) + + def send_form(self): + if self.form: + to = self.recipient_entry.get_text() + x = self.form.get_submit_form() + command = ElementTree.Element('command') + command.attrib['sessionid'] = self.sessionid + command.attrib['node'] = self.node + command.attrib['xmlns'] = COMMANDS_NS + command.append(x) + body = ElementTree.tostring(command) + self.console_proxy.SendIQ('(sss)', 'set', to, body, + result_handler=self.send_form_cb) + + def send_form_cb(self, proxy, result, user_data): + if not isinstance(result, Exception): + reply_type, reply = result + self.parse_command_reply(reply) + + self.emit('receive-iq', proxy, result, user_data) + + def cancel_command(self, button): + if self.sessionid is None: + return + + body = "".format( + COMMANDS_NS, + self.sessionid) + to = self.recipient_entry.get_text() + self.console_proxy.SendIQ('(sss)', 'set', to, body, + result_handler=self.cancel_command_cb) + + def cancel_command_cb(self, proxy, result, user_data): + if not isinstance(result, Exception): + reply_type, reply = result + + iq = ElementTree.fromstring(reply) + commands = iq.getchildren() + + self.sessionid = None + self.node = None + self.commands_stack.set_visible_child_name('browser') + + self.emit('receive-iq', proxy, result, user_data) + + def parse_command_reply(self, stanza): + iq = ElementTree.fromstring(stanza) + query = iq.getchildren() + notes = [] + if query[0].tag == NS(COMMANDS_NS, 'command'): + self.sessionid = query[0].attrib['sessionid'] + self.status = query[0].attrib.get('status') + for child in query[0].getchildren(): + if child.tag == NS(JABBER_X_DATA_NS, 'x'): + # data + self.form.clear() + self.form.parse_form(child) + self.form.show_all() + self.commands_stack.set_visible_child_name('form') + + elif child.tag == '{http://jabber.org/protocol/commands}actions': + # next steps + self.parse_actions(child) + elif child.tag == '{http://jabber.org/protocol/commands}note': + print('note', child.text) + + def parse_actions(self, actions): + for allowed_action in actions.getchildren(): + if allowed_action.tag == '{http://jabber.org/protocol/commands}prev': + self.back.set_sensitive(True) + elif allowed_action.tag == '{http://jabber.org/protocol/commands}next': + self.forward.set_sensitive(True) + elif allowed_action.tag == '{http://jabber.org/protocol/commands}complete': + self.forward.set_sensitive(True) + + def parse_iq(self, result): + reply_type, reply = result + return ElementTree.fromstring(reply) + +class JabberXForm(Gtk.ListBox): + def __init__(self, form=None): + super(JabberXForm, self).__init__() + + self.title = None + self.reported = None + self.notes = [] + self.form_xml= form + self.fields = {} + + if form is not None: + self.parse_form(form) + + def clear(self): + self.foreach(self.remove) + self.fields = {} + + def parse_form(self, form): + self.form_xml = form + for child in form.getchildren(): + if child.tag == '{jabber:x:data}instructions': + print('instructions', child.text) + elif child.tag == '{jabber:x:data}title': + print('title', child.text) + self.title = self.add_title(child.text) + elif child.tag == '{jabber:x:data}field': + self.parse_form_field(child) + elif child.tag == '{jabber:x:data}reported': + # column heading + print('reported', child.text) + self.reported = self.append_title(child.text) + elif child.tag == '{jabber:x:data}item': + self.parse_form_item(child) + else: + print("Unrecognized tag %s" % (child.tag,)) + + def parse_form_item(self, item): + for field in item.getchildren(): + self.parse_form_field(field) + + def parse_form_field(self, field): + if field.tag != '{jabber:x:data}field': + raise ValueError("Unrecognized tag %s, expected field" % (field.tag,)) + + data_field = DataFormField(field) + print(data_field._repr_pretty_()) + + self.fields[data_field.var] = data_field + type_to_widgets = { + 'boolean': XFormBoolean, + 'fixed': XFormFixed, + 'hidden': None, + 'jid-multi': XFormJidMulti, + 'jid-single': XFormJidSingle, + 'list-multi': XFormListMulti, + 'list-single': XFormListSingle, + 'text-multi': XFormTextMulti, + 'text-private': XFormTextPrivate, + 'text-single': XFormTextSingle, + } + print(data_field.type) + widget = type_to_widgets.get(data_field.type, XFormTextSingle) + if widget is not None: + print(widget) + self.add(widget(data_field)) + + def add_title(self, title): + label = Gtk.Label() + label.set_markup("%s" % title) + label.set_property('xalign', 0) + self.add(label) + return label + + def get_submit_form(self): + self.form_xml.attrib['type'] = 'submit' + return self.form_xml + +class XFormBoolean(Gtk.HBox): + def __init__(self, data_field): + super(XFormBoolean, self).__init__() + self.data_field = data_field + self.label = Gtk.Label(self.data_field.label) + self.switch = Gtk.Switch() + self.value = self.data_field.value + # force setting default value + #self.state_set(self.switch, self.value) + + self.switch.connect('state-set', self.state_set) + self.pack_start(self.label, True, True, 0) + self.pack_start(self.switch, True, True, 0) + + @property + def value(self): + return self.switch.get_state() + + @value.setter + def value(self, value): + self.switch.set_state(value) + + def state_set(self, switch, value): + self.data_field.value = '1' if value else '0' + +class XFormFixed(Gtk.Label): + def __init__(self, data_field): + super(XFormFixed, self).__init__() + self.data_field = data_field + + self.set_markup("%s" % self.data_field.label) + self.set_property('xalign', 0) + +class XFormJidMulti(Gtk.VBox): + def __init__(self, data_field): + super(XFormJidMulti, self).__init__() + self.data_field = data_field + self.label = Gtk.Label(self.data_field.label) + self.text = Gtk.TextView() + self.value = self.data_field.value + + self.text.connect('insert-at-cursor', self.insert_at_cursor) + self.pack_start(self.label, False, False, 0) + self.pack_start(self.text, True, True, 0) + + @property + def value(self): + start, end = self.text.get_buffer().get_bounds() + data = self.text.get_buffer().get_text(start, end, False) + return data.split('\n') + + @value.setter + def value(self, value): + if value != self.value: + self.text.get_buffer().set_text('\n'.join(value)) + self.data_field.value = value + + def insert_at_cursor(self, entry, string): + self.data_field.value = self.value + + +class XFormJidSingle(Gtk.HBox): + def __init__(self, data_field): + super(XFormJidSingle, self).__init__() + self.data_field = data_field + self.label = Gtk.Label(self.data_field.label) + self.entry = Gtk.Entry() + self.entry.set_property('margin-right', PADDING) + self.entry.set_property('hexpand', True) + + self.entry.connect('changed', self.changed) + self.pack_start(self.label, False, False, 0) + self.pack_start(self.entry, True, True, 0) + + @property + def value(self): + return self.entry.get_buffer().get_text() + + @value.setter + def value(self, value): + if value != self.value: + self.entry.get_buffer().set_text(value) + self.data_field.value = value + + def changed(self, entry): + self.data_field.value = self.value + +class XFormListMulti(Gtk.VBox): + def __init__(self, data_field): + super(XFormListSingle, self).__init__() + self.data_field = data_field + self.label = Gtk.Label(self.data_field.label) + self.model = Gtk.ListStore((str, str)) + self.entry = Gtk.TreeView(self.model) + + for o in self.data_field.options: + # FIXME: would be a great use for a named tuple + self.model.append(o[0], o[1]) + + if self.data_field.value: + self.value = self.data_field.value + + self.entry.connect('changed', self.changed) + self.pack_start(self.label, False, False, 0) + self.pack_start(self.entry, True, True, 0) + + @property + def value(self): + return self.entry.get_active_id() + + @value.setter + def value(self, value): + self.entry.set_active_id(value) + + def changed(self, entry): + self.data_field.value = entry.get_active_id() + +class XFormListSingle(Gtk.HBox): + def __init__(self, data_field): + super(XFormListSingle, self).__init__() + self.data_field = data_field + self.label = Gtk.Label(self.data_field.label) + self.entry = Gtk.ComboBoxText() + + for o in self.data_field.options: + # FIXME: would be a great use for a named tuple + self.entry.append(o[0], o[1]) + if self.data_field.value: + self.value = self.data_field.value + + self.entry.connect('changed', self.changed) + self.pack_start(self.label, False, False, 0) + self.pack_start(self.entry, True, True, 0) + + @property + def value(self): + return self.entry.get_active_id() + + @value.setter + def value(self, value): + self.entry.set_active_id(value) + + def changed(self, entry): + self.data_field.value = entry.get_active_id() + +class XFormTextMulti(Gtk.VBox): + def __init__(self, data_field): + super(XFormTextMulti, self).__init__() + self.data_field = data_field + self.label = Gtk.Label(self.data_field.label) + self.text = Gtk.TextView() + self.value = self.data_field.value + + self.text.connect('insert-at-cursor', self.insert_at_cursor) + self.pack_start(self.label, False, False, 0) + self.pack_start(self.text, True, True, 0) + + @property + def value(self): + start, end = self.text.get_buffer().get_bounds() + data = self.text.get_buffer().get_text(start, end, False) + return data.split('\n') + + @value.setter + def value(self, value): + if value != self.value: + self.text.get_buffer().set_text('\n'.join(value)) + self.data_field.value = value + + def insert_at_cursor(self, entry, string): + self.data_field.value = self.value + +class XFormTextSingle(Gtk.HBox): + def __init__(self, data_field): + super(XFormTextSingle, self).__init__() + self.data_field = data_field + self.label = Gtk.Label(self.data_field.label) + self.entry = Gtk.Entry() + self.entry.set_property('margin-right', PADDING) + self.entry.set_property('hexpand', True) + + self.entry.connect('changed', self.changed) + self.pack_start(self.label, False, False, 0) + self.pack_start(self.entry, True, True, 0) + + @property + def value(self): + return self.entry.get_buffer().get_text() + + @value.setter + def value(self, value): + if value != self.value: + self.entry.get_buffer().set_text(value) + self.data_field.value = value + + def changed(self, entry): + self.data_field.value = self.value + +class XFormTextPrivate(XFormTextSingle): + def __init__(self, data_field): + super(XFormTextPrivate, self).__init__() + self.entry.set_input_purpose(Gtk.InputPurpose.PASSWORD) + +class DataFormField(collections.Mapping): + PUBLIC_FIELDS = ['type', 'var', 'label', 'description', 'required', 'default_value', 'value', 'options'] + def __init__(self, node): + if node.tag != '{jabber:x:data}field': + raise ValueError("Unrecognized tag %s, expected field" % (field.tag,)) + + self.type = node.attrib.get('type') + self.var = node.attrib.get('var') + self.label = node.attrib.get('label') + self.raw_value_contents = node + self.description = None + self.required = False + self.default_value = [] + self.options = [] + for child in node.getchildren(): + if child.tag == '{jabber:x:data}desc': + self.description = node.text + elif child.tag == '{jabber:x:data}required': + self.required = True + elif child.tag == '{jabber:x:data}value': + if self.type in ('list-multi', 'jid-multi', 'text-multi'): + self.default_value.append(child.text) + else: + self.default_value = node.text + elif child.tag == '{jabber:x:data}option': + option_label = child.attrib.get('label') + option_value = child.getchildren()[0].text + self.options.append((option_value, option_label)) + self._value = self.default_value + + def __iter__(self): + for k in DataFormField.PUBLIC_FIELDS: + yield k + + def __getitem__(self, key): + if key in DataFormField.PUBLIC_FIELDS: + return getattr(self, key) + else: + raise KeyError("Invalid key") + + def __len__(self): + return len(DataFormField.PUBLIC_FIELDS) + + @property + def value(self): + return self._value + + @value.setter + def value(self, value): + self._value = value + + node = self.raw_value_contents.find('value') + if node is None: + node = ElementTree.Element('value') + node.attrib['xmlns'] = 'jabber:x:data' + self.raw_value_contents.append(node) + + node.text = self._value + print('set {var}: {value}'.format(**self)) + + def _repr_pretty_(self): + for key in self: + print(key, self[key]) + r = ['Field {type}: {var}: {value}'.format(**self)] + if self.label: + r.append('label: {label}'.format(**self)) + if self.description: + r.append('description: {description}'.format(**self)) + if self.options: + r.append(', '.join(('{}: {}'.format(x[0], x[1]) for x in self.options))) + return os.linesep.join(r) + + def _repr_etree_(self): + return self.raw_value_contents + + def _repr_xml_(self): + return ElementTree.tostring(self._repr_etree_) + class Window(Gtk.Window): IQ_PAGE = 0 STANZA_PAGE = 1 SNOOPY_PAGE = 2 + AD_HOC_PAGE = 3 - def __init__(self, account): + def __init__(self, bus, connection_bus_name): Gtk.Window.__init__(self) self.set_title('XMPP Console') self.set_default_size(600, 371) - request = Tp.AccountChannelRequest.new( - account, - { Tp.PROP_CHANNEL_CHANNEL_TYPE: CONSOLE_IFACE }, - 0) - request.create_and_handle_channel_async(None, self.__create_cb, None) + conn_future_proxy = Gio.DBusProxy.new_sync(bus, 0, None, + connection_bus_name, pathify(connection_bus_name), + CONN_FUTURE_IFACE, None) + try: + sidecar_path, _ = conn_future_proxy.EnsureSidecar('(s)', CONSOLE_IFACE) + except Exception as e: + print(""" +Couldn't connect to the XMPP console interface on '%(connection_bus_name)s': + %(e)s +Check that it's a running Jabber connection, and that you have the console +plugin installed.""" % locals()) - self.connect('destroy', Window.__destroy_cb) + raise SystemExit(2) - def __build_ui(self): - # Build up the UI - self.grid = Gtk.Grid() - self.add(self.grid) + self.console_proxy = Gio.DBusProxy.new_sync(bus, 0, None, + connection_bus_name, sidecar_path, CONSOLE_IFACE, None) + # Build up the UI self.nb = Gtk.Notebook() - self.grid.attach(self.nb, 0, 0, 1, 1) + self.add(self.nb) self.iq = IQPage(self.console_proxy) self.nb.insert_page(self.iq, Gtk.Label.new_with_mnemonic("_IQ console"), self.IQ_PAGE) + self.adhoc = AdHocPage(self.console_proxy) + self.nb.insert_page(self.adhoc, + Gtk.Label.new_with_mnemonic("_AdHoc console"), + self.AD_HOC_PAGE) + self.stanza = StanzaPage(self.console_proxy) self.nb.insert_page(self.stanza, Gtk.Label.new_with_mnemonic("Send a s_tanza"), @@ -329,103 +1024,61 @@ class Window(Gtk.Window): Gtk.Label.new_with_mnemonic("_Monitor network traffic"), self.SNOOPY_PAGE) - self.infobar = Gtk.InfoBar() - self.infobar.set_message_type(Gtk.MessageType.WARNING) - self.infobar.set_no_show_all(True) - label = Gtk.Label("The connection went away! Time to leave.") - label.show() - self.infobar.get_content_area().add(label) - self.infobar_close_button = self.infobar.add_button("Close", Gtk.ResponseType.CLOSE) - self.infobar.connect('response', lambda infobar, response: Gtk.main_quit()) - self.infobar.connect('close', lambda infobar: Gtk.main_quit()) - - self.grid.attach_next_to(self.infobar, self.nb, - Gtk.PositionType.BOTTOM, 1, 1) - - def __create_cb(self, request, result, _): - try: - channel, context = request.create_and_handle_channel_finish(result) - channel.connect('invalidated', self.__channel_invalidated_cb) - - bus_name = channel.get_bus_name() - sidecar_path = channel.get_object_path() - - bus = Gio.bus_get_sync(Gio.BusType.SESSION, None) - self.console_proxy = Gio.DBusProxy.new_sync(bus, 0, None, - bus_name, sidecar_path, CONSOLE_IFACE, None) - - except GLib.GError as e: - print(""" -Couldn't connect to the XMPP console interface on '%(name)s': -%(e)s -Check that you have the console plugin installed.""" % { - 'name': request.get_account().get_path_suffix(), - 'e': e, - }) - raise SystemExit(2) - - self.__build_ui() - self.show_all() - - def __channel_invalidated_cb(self, channel, domain, code, message): - self.infobar.show() - self.infobar_close_button.grab_focus() - self.nb.set_sensitive(False) - # TODO: try to reconnect? + self.connect('destroy', Window.__destroy_cb) + self.adhoc.connect( + 'receive-iq', + lambda _, proxy, result, user_data: + IQPage.send_iq_cb(self.iq, proxy, result, user_data)) def __destroy_cb(self): - try: - self.snoopy.teardown() - except GLib.GError as e: - print("Couldn't turn off the monitor (maybe the connection went away?)") - print(e) + self.snoopy.teardown() Gtk.main_quit() -def usage(am): - xmpp_accounts = sorted( - account.get_path_suffix() - for account in am.dup_valid_accounts() - if account.get_cm_name() == 'gabble') +GABBLE_PREFIX = 'org.freedesktop.Telepathy.Connection.gabble.jabber.' +AM_BUS_NAME = 'org.freedesktop.Telepathy.AccountManager' +ACCOUNT_PREFIX = '/org/freedesktop/Telepathy/Account' +ACCOUNT_IFACE = 'org.freedesktop.Telepathy.Account' + +def usage(): print(""" Usage: %(arg0)s gabble/jabber/blahblah + %(arg0)s %(prefix)sblahblah -Here are some account identifiers: - - %(accounts)s +List account identifiers using `mc-tool list | grep gabble`. +List connection bus names using `qdbus | grep gabble`. """ % { 'arg0': sys.argv[0], - 'accounts': '\n '.join(xmpp_accounts), + 'prefix': GABBLE_PREFIX, }) raise SystemExit(1) -def am_prepared_cb(am, result, account_suffix): - try: - am.prepare_finish(result) - except GLib.GError as e: - print(e) - raise SystemExit(2) - - if account_suffix is None: - usage(am) - - for account in am.dup_valid_accounts(): - if account.get_path_suffix() == account_suffix: - if account.get_connection() is None: - print("%s is not online." % account_suffix) - raise SystemExit(2) - else: - win = Window(account) - return - - usage(am) - if __name__ == '__main__': - account_suffix = sys.argv[1] if len(sys.argv) == 2 else None + bus = Gio.bus_get_sync(Gio.BusType.SESSION, None) + + if len(sys.argv) != 2: + usage() + + thing = sys.argv[1] + + if re.match('^gabble/jabber/[a-zA-Z0-9_]+$', thing): + # Looks like an account path to me. + account_proxy = Gio.DBusProxy.new_sync(bus, 0, None, + AM_BUS_NAME, '%s/%s' % (ACCOUNT_PREFIX, thing), + ACCOUNT_IFACE, None) + path = account_proxy.get_cached_property('Connection').get_string() + if path == '/': + print("%s is not online" % thing) + raise SystemExit(1) + else: + thing = nameify(path) + + if not re.match('^%s[a-zA-Z0-9_]+$' % GABBLE_PREFIX, thing): + usage() - am = Tp.AccountManager.dup() - am.prepare_async([], am_prepared_cb, account_suffix) + win = Window(bus, thing) + win.show_all() Gtk.main()