server_info.py 13.1 KB
Newer Older
Philipp Hörist's avatar
Philipp Hörist committed
1
2
# This file is part of Gajim.
#
3
4
5
# Gajim is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published
# by the Free Software Foundation; version 3 only.
Philipp Hörist's avatar
Philipp Hörist committed
6
7
8
#
# Gajim is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
9
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
Philipp Hörist's avatar
Philipp Hörist committed
10
11
12
13
14
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Gajim. If not, see <http://www.gnu.org/licenses/>.

15
import logging
Philipp Hörist's avatar
Philipp Hörist committed
16
from collections import namedtuple
17
18
from datetime import timedelta

19
from nbxmpp.errors import StanzaError
20
from nbxmpp.errors import MalformedStanzaError
Philipp Hörist's avatar
Philipp Hörist committed
21
from nbxmpp.namespaces import Namespace
22
from gi.repository import Gtk
23
from gi.repository import Gdk
24
from gi.repository import Pango
Philipp Hörist's avatar
Philipp Hörist committed
25

26
from gajim.common import app
André's avatar
André committed
27
from gajim.common import ged
28
from gajim.common.helpers import open_uri
29
from gajim.common.i18n import _
Philipp Hörist's avatar
Philipp Hörist committed
30

Philipp Hörist's avatar
Philipp Hörist committed
31
32
33
from .util import get_builder
from .util import EventHelper
from .util import open_window
Philipp Hörist's avatar
Philipp Hörist committed
34

Philipp Hörist's avatar
Philipp Hörist committed
35
log = logging.getLogger('gajim.gui.server_info')
36
37


38
class ServerInfo(Gtk.ApplicationWindow, EventHelper):
Philipp Hörist's avatar
Philipp Hörist committed
39
    def __init__(self, account):
Daniel Brötzmann's avatar
Daniel Brötzmann committed
40
        Gtk.ApplicationWindow.__init__(self)
41
        EventHelper.__init__(self)
Daniel Brötzmann's avatar
Daniel Brötzmann committed
42
43
44
45
46
47
        self.set_name('ServerInfo')
        self.set_application(app.app)
        self.set_position(Gtk.WindowPosition.CENTER)
        self.set_default_size(400, 600)
        self.set_show_menubar(False)
        self.set_title(_('Server Info'))
48
        self.set_type_hint(Gdk.WindowTypeHint.DIALOG)
Philipp Hörist's avatar
Philipp Hörist committed
49
50

        self.account = account
Philipp Hörist's avatar
Philipp Hörist committed
51
        self._destroyed = False
Philipp Hörist's avatar
Philipp Hörist committed
52

Daniel Brötzmann's avatar
Daniel Brötzmann committed
53
54
        self._ui = get_builder('server_info.ui')
        self.add(self._ui.server_info_notebook)
Philipp Hörist's avatar
Philipp Hörist committed
55
56

        self.connect('destroy', self.on_destroy)
Daniel Brötzmann's avatar
Daniel Brötzmann committed
57
58
        self.connect('key-press-event', self._on_key_press)
        self._ui.connect_signals(self)
Philipp Hörist's avatar
Philipp Hörist committed
59

60
61
62
        self.register_events([
            ('server-disco-received', ged.GUI1, self._server_disco_received),
        ])
Philipp Hörist's avatar
Philipp Hörist committed
63
64

        self.version = ''
65
        self.hostname = app.get_hostname_from_account(account)
Daniel Brötzmann's avatar
Daniel Brötzmann committed
66
        self._ui.server_hostname.set_text(self.hostname)
67
        con = app.connections[account]
Philipp Hörist's avatar
Philipp Hörist committed
68
69
        con.get_module('SoftwareVersion').request_software_version(
            self.hostname, callback=self._software_version_received)
70
71
72

        con.get_module('LastActivity').request_last_activity(
            self.hostname, callback=self._on_last_activity)
Philipp Hörist's avatar
Philipp Hörist committed
73

74
75
76
        server_info = con.get_module('Discovery').server_info
        self._add_contact_addresses(server_info.dataforms)

Philipp Hörist's avatar
Philipp Hörist committed
77
        self.cert = con.certificate
Daniel Brötzmann's avatar
Daniel Brötzmann committed
78
79
80
81
82
83
        self._add_connection_info()

        self.feature_listbox = Gtk.ListBox()
        self.feature_listbox.set_name('ServerInfo')
        self.feature_listbox.set_selection_mode(Gtk.SelectionMode.NONE)
        self._ui.features_scrolled.add(self.feature_listbox)
Philipp Hörist's avatar
Philipp Hörist committed
84
85
        for feature in self.get_features():
            self.add_feature(feature)
Daniel Brötzmann's avatar
Daniel Brötzmann committed
86
        self.clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
Philipp Hörist's avatar
Philipp Hörist committed
87
88
89

        self.show_all()

Daniel Brötzmann's avatar
Daniel Brötzmann committed
90
91
92
93
94
95
    def _on_key_press(self, _widget, event):
        if event.keyval == Gdk.KEY_Escape:
            self.destroy()

    def _add_connection_info(self):
        # Connection type
96
97
        nbxmpp_client = app.connections[self.account].connection
        address = nbxmpp_client.current_address
Daniel Brötzmann's avatar
Daniel Brötzmann committed
98

99
100
        self._ui.connection_type.set_text(address.type.value)
        if address.type.is_plain:
101
102
            self._ui.connection_type.get_style_context().add_class(
                'error-color')
Daniel Brötzmann's avatar
Daniel Brötzmann committed
103
104

        # Connection proxy
105
        proxy = address.proxy
Daniel Brötzmann's avatar
Daniel Brötzmann committed
106
107
108
        if proxy is not None:
            self._ui.proxy_type.set_text(proxy.type)
            self._ui.proxy_host.set_text(proxy.host)
Daniel Brötzmann's avatar
Daniel Brötzmann committed
109
110
111

        self._ui.cert_button.set_sensitive(self.cert)

112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
        self._ui.domain.set_text(address.domain)

        visible = address.service is not None
        self._ui.dns_label.set_visible(visible)
        self._ui.dns.set_visible(visible)
        self._ui.dns.set_text(address.service or '')

        visible = nbxmpp_client.remote_address is not None
        self._ui.ip_port_label.set_visible(visible)
        self._ui.ip_port.set_visible(visible)
        self._ui.ip_port.set_text(nbxmpp_client.remote_address or '')

        visible = address.uri is not None
        self._ui.websocket_label.set_visible(visible)
        self._ui.websocket.set_visible(visible)
        self._ui.websocket.set_text(address.uri or '')

129
    def _on_cert_button_clicked(self, _button):
Philipp Hörist's avatar
Philipp Hörist committed
130
131
132
133
        open_window('CertificateDialog',
                    account=self.account,
                    transient_for=self,
                    cert=self.cert)
Philipp Hörist's avatar
Philipp Hörist committed
134

135
136
137
    def _add_contact_addresses(self, dataforms):
        fields = {
            'admin-addresses': _('Admin'),
138
            'status-addresses': _('Status'),
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
            'support-addresses': _('Support'),
            'security-addresses': _('Security'),
            'feedback-addresses': _('Feedback'),
            'abuse-addresses': _('Abuse'),
            'sales-addresses': _('Sales'),
        }

        addresses = self._get_addresses(fields, dataforms)
        if addresses is None:
            self._ui.no_addresses_label.set_visible(True)
            return

        row_count = 4
        for address_type, values in addresses.items():
            label = self._get_address_type_label(fields[address_type])
            self._ui.server.attach(label, 0, row_count, 1, 1)
            for index, value in enumerate(values):
                last = index == len(values) - 1
                label = self._get_address_label(value, last=last)
                self._ui.server.attach(label, 1, row_count, 1, 1)
                row_count += 1

    @staticmethod
    def _get_addresses(fields, dataforms):
        addresses = {}
        for form in dataforms:
165
166
            field = form.vars.get('FORM_TYPE')
            if field.value != 'http://jabber.org/network/serverinfo':
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
                continue

            for address_type in fields:
                field = form.vars.get(address_type)
                if field is None:
                    continue

                if field.type_ != 'list-multi':
                    continue

                if not field.values:
                    continue
                addresses[address_type] = field.values

            return addresses or None
        return None

    @staticmethod
    def _get_address_type_label(text):
        label = Gtk.Label(label=text)
        label.set_halign(Gtk.Align.END)
        label.set_valign(Gtk.Align.START)
        label.get_style_context().add_class('dim-label')
        return label

    def _get_address_label(self, address, last=False):
        label = Gtk.Label()
        label.set_markup('<a href="%s">%s</a>' % (address, address))
        label.set_ellipsize(Pango.EllipsizeMode.END)
        label.set_xalign(0)
        label.set_halign(Gtk.Align.START)
        label.get_style_context().add_class('link-button')
        label.connect('activate-link', self._on_activate_link)
        if last:
            label.set_margin_bottom(6)
        return label

    def _on_activate_link(self, label, *args):
        open_uri(label.get_text(), account=self.account)
        return Gdk.EVENT_STOP

208
    def _on_last_activity(self, task):
209
        try:
210
211
212
213
214
215
216
217
218
219
220
221
            result = task.finish()
        except (StanzaError, MalformedStanzaError) as error:
            log.warning(error)
            return

        delta = timedelta(seconds=result.seconds)
        hours = 0
        if result.seconds >= 3600:
            hours = delta.seconds // 3600
        uptime = _('%(days)s days, %(hours)s hours') % {
            'days': delta.days, 'hours': hours}
        self._ui.server_uptime.set_text(uptime)
222

223
224
225
226
    def _software_version_received(self, task):
        try:
            result = task.finish()
        except StanzaError:
Philipp Hörist's avatar
Philipp Hörist committed
227
228
229
            self.version = _('Unknown')
        else:
            self.version = '%s %s' % (result.name, result.version)
230

Daniel Brötzmann's avatar
Daniel Brötzmann committed
231
232
233
234
235
236
237
238
        self._ui.server_software.set_text(self.version)

    @staticmethod
    def update(func, listbox):
        for index, item in enumerate(func()):
            row = listbox.get_row_at_index(index)
            row.get_child().update(item)
            row.set_tooltip_text(row.get_child().tooltip)
Philipp Hörist's avatar
Philipp Hörist committed
239

240
    def _server_disco_received(self, _event):
Philipp Hörist's avatar
Philipp Hörist committed
241
242
243
244
245
        self.update(self.get_features, self.feature_listbox)

    def add_feature(self, feature):
        item = FeatureItem(feature)
        self.feature_listbox.add(item)
246
        item.get_parent().set_tooltip_text(item.tooltip or '')
Philipp Hörist's avatar
Philipp Hörist committed
247
248

    def get_features(self):
249
        con = app.connections[self.account]
250
251
        Feature = namedtuple('Feature',
                             ['name', 'available', 'tooltip', 'enabled'])
252
        Feature.__new__.__defaults__ = (None, None)  # type: ignore
253

254
        # HTTP File Upload
255
256
257
258
259
260
261
        http_upload_info = con.get_module('HTTPUpload').httpupload_namespace
        if con.get_module('HTTPUpload').available:
            max_file_size = con.get_module('HTTPUpload').max_file_size
            if max_file_size is not None:
                max_file_size = max_file_size / (1024 * 1024)
                http_upload_info = http_upload_info + ' (max. %s MiB)' % \
                    max_file_size
262

Philipp Hörist's avatar
Philipp Hörist committed
263
        return [
264
265
            Feature('XEP-0045: Multi-User Chat',
                    con.get_module('MUC').supported),
266
            Feature('XEP-0054: vcard-temp',
267
                    con.get_module('VCardTemp').supported),
Philipp Hörist's avatar
Philipp Hörist committed
268
269
            Feature('XEP-0077: In-Band Registration',
                    con.get_module('Register').supported),
Philipp Hörist's avatar
Philipp Hörist committed
270
            Feature('XEP-0163: Personal Eventing Protocol',
271
                    con.get_module('PEP').supported),
Philipp Hörist's avatar
Philipp Hörist committed
272
            Feature('XEP-0163: #publish-options',
273
                    con.get_module('PubSub').publish_options),
Philipp Hörist's avatar
Philipp Hörist committed
274
            Feature('XEP-0191: Blocking Command',
275
                    con.get_module('Blocking').supported,
Philipp Hörist's avatar
Philipp Hörist committed
276
                    Namespace.BLOCKING),
Philipp Hörist's avatar
Philipp Hörist committed
277
            Feature('XEP-0198: Stream Management',
Philipp Hörist's avatar
Philipp Hörist committed
278
                    con.features.has_sm, Namespace.STREAM_MGMT),
279
280
            Feature('XEP-0258: Security Labels in XMPP',
                    con.get_module('SecLabels').supported,
Philipp Hörist's avatar
Philipp Hörist committed
281
                    Namespace.SECLABEL),
Philipp Hörist's avatar
Philipp Hörist committed
282
            Feature('XEP-0280: Message Carbons',
283
                    con.get_module('Carbons').supported,
Philipp Hörist's avatar
Philipp Hörist committed
284
                    Namespace.CARBONS),
Philipp Hörist's avatar
Philipp Hörist committed
285
            Feature('XEP-0313: Message Archive Management',
Philipp Hörist's avatar
Philipp Hörist committed
286
                    con.get_module('MAM').available),
Philipp Hörist's avatar
Philipp Hörist committed
287
            Feature('XEP-0363: HTTP File Upload',
Philipp Hörist's avatar
Philipp Hörist committed
288
                    con.get_module('HTTPUpload').available,
289
                    http_upload_info),
Philipp Hörist's avatar
Philipp Hörist committed
290
            Feature('XEP-0398: Avatar Conversion',
291
                    con.get_module('VCardAvatars').avatar_conversion_available),
292
            Feature('XEP-0411: Bookmarks Conversion',
293
294
295
296
297
                    con.get_module('Bookmarks').conversion),
            Feature('XEP-0402: Bookmarks Compat',
                    con.get_module('Bookmarks').compat),
            Feature('XEP-0402: Bookmarks Compat PEP',
                    con.get_module('Bookmarks').compat_pep)
Philipp Hörist's avatar
Philipp Hörist committed
298
        ]
Philipp Hörist's avatar
Philipp Hörist committed
299

300
    def _on_clipboard_button_clicked(self, _widget):
Daniel Brötzmann's avatar
Daniel Brötzmann committed
301
        server_software = 'Server Software: %s\n' % self.version
302
303
304
305
306
307
308
        server_features = ''

        for feature in self.get_features():
            if feature.available:
                available = 'Yes'
            else:
                available = 'No'
309
            if feature.tooltip is not None:
310
311
312
                tooltip = '(%s)' % feature.tooltip
            else:
                tooltip = ''
Daniel Brötzmann's avatar
Daniel Brötzmann committed
313
314
            server_features += '%s: %s %s\n' % (
                feature.name, available, tooltip)
315
316
317
318

        clipboard_text = server_software + server_features
        self.clipboard.set_text(clipboard_text, -1)

Philipp Hörist's avatar
Philipp Hörist committed
319
    def on_destroy(self, *args):
Philipp Hörist's avatar
Philipp Hörist committed
320
        self._destroyed = True
Philipp Hörist's avatar
Philipp Hörist committed
321

Philipp Hörist's avatar
Philipp Hörist committed
322
323
324
325

class FeatureItem(Gtk.Grid):
    def __init__(self, feature):
        super().__init__()
326
        self.tooltip = feature.tooltip
Philipp Hörist's avatar
Philipp Hörist committed
327
328
329
330
        self.set_column_spacing(6)

        self.icon = Gtk.Image()
        self.feature_label = Gtk.Label(label=feature.name)
331
        self.set_feature(feature.available, feature.enabled)
Philipp Hörist's avatar
Philipp Hörist committed
332
333
334
335

        self.add(self.icon)
        self.add(self.feature_label)

336
    def set_feature(self, available, enabled):
337
338
339
340
        self.icon.get_style_context().remove_class('error-color')
        self.icon.get_style_context().remove_class('warning-color')
        self.icon.get_style_context().remove_class('success-color')

341
        if not available:
342
343
344
            self.icon.set_from_icon_name('window-close-symbolic',
                                         Gtk.IconSize.MENU)
            self.icon.get_style_context().add_class('error-color')
345
        elif enabled is False:
346
347
            self.icon.set_from_icon_name('dialog-warning-symbolic',
                                         Gtk.IconSize.MENU)
Daniel Brötzmann's avatar
Daniel Brötzmann committed
348
            self.tooltip += _('\nDisabled in preferences')
349
            self.icon.get_style_context().add_class('warning-color')
Philipp Hörist's avatar
Philipp Hörist committed
350
        else:
351
352
353
            self.icon.set_from_icon_name('emblem-ok-symbolic',
                                         Gtk.IconSize.MENU)
            self.icon.get_style_context().add_class('success-color')
Philipp Hörist's avatar
Philipp Hörist committed
354
355

    def update(self, feature):
356
357
        self.tooltip = feature.tooltip
        self.set_feature(feature.available, feature.enabled)