server_info.py 11.8 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

Philipp Hörist's avatar
Philipp Hörist committed
19
import nbxmpp
Philipp Hörist's avatar
Philipp Hörist committed
20
from nbxmpp.util import is_error_result
21
from gi.repository import Gtk
22
from gi.repository import Gdk
23
from gi.repository import Pango
Philipp Hörist's avatar
Philipp Hörist committed
24

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

Philipp Hörist's avatar
Philipp Hörist committed
29 30
from gajim.gtk.util import ensure_not_destroyed

31
log = logging.getLogger('gajim.gtk.serverinfo')
32 33


Philipp Hörist's avatar
Philipp Hörist committed
34 35
class ServerInfoDialog(Gtk.Dialog):
    def __init__(self, account):
36 37 38
        super().__init__(title=_('Server Info'),
                         transient_for=None,
                         destroy_with_parent=True)
Philipp Hörist's avatar
Philipp Hörist committed
39 40

        self.account = account
Philipp Hörist's avatar
Philipp Hörist committed
41
        self._destroyed = False
42
        self.set_transient_for(app.interface.roster.window)
43 44
        self.set_resizable(True)
        self.set_size_request(300, 500)
Philipp Hörist's avatar
Philipp Hörist committed
45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60

        grid = Gtk.Grid()
        grid.set_name('ServerInfoGrid')
        grid.set_row_spacing(10)
        grid.set_hexpand(True)

        self.info_listbox = Gtk.ListBox()
        self.info_listbox.set_selection_mode(Gtk.SelectionMode.NONE)
        self.info_listbox.set_header_func(self.header_func, 'Information')
        grid.attach(self.info_listbox, 0, 0, 1, 1)

        self.feature_listbox = Gtk.ListBox()
        self.feature_listbox.set_selection_mode(Gtk.SelectionMode.NONE)
        self.feature_listbox.set_header_func(self.header_func, 'Features')
        grid.attach(self.feature_listbox, 0, 1, 1, 1)

61 62 63 64 65 66 67 68
        self.clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
        clipboard_button = Gtk.Button(halign=Gtk.Align.END)
        clp_image = Gtk.Image.new_from_icon_name('edit-copy-symbolic',
                                                 Gtk.IconSize.BUTTON)
        clipboard_button.set_image(clp_image)
        clipboard_button.set_tooltip_text(_('Copy info to clipboard'))
        clipboard_button.connect('clicked', self.on_clipboard_button_clicked)

Philipp Hörist's avatar
Philipp Hörist committed
69
        box = self.get_content_area()
70 71 72 73 74
        scrolled = Gtk.ScrolledWindow()
        scrolled.set_max_content_height(500)
        scrolled.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC)
        scrolled.add(grid)
        box.pack_start(scrolled, True, True, 0)
75
        box.pack_start(clipboard_button, False, True, 0)
Philipp Hörist's avatar
Philipp Hörist committed
76 77 78 79 80 81
        box.set_property('margin', 12)
        box.set_spacing(18)

        self.connect('response', self.on_response)
        self.connect('destroy', self.on_destroy)

82
        app.ged.register_event_handler('server-disco-received',
Philipp Hörist's avatar
Philipp Hörist committed
83
                                       ged.GUI1,
84
                                       self._server_disco_received)
Philipp Hörist's avatar
Philipp Hörist committed
85 86

        self.version = ''
87
        self.uptime = ''
88
        self.hostname = app.get_hostname_from_account(account)
89
        con = app.connections[account]
Philipp Hörist's avatar
Philipp Hörist committed
90 91
        con.get_module('SoftwareVersion').request_software_version(
            self.hostname, callback=self._software_version_received)
92
        self.request_last_activity()
Philipp Hörist's avatar
Philipp Hörist committed
93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115

        for feature in self.get_features():
            self.add_feature(feature)

        for info in self.get_infos():
            self.add_info(info)

        self.show_all()

    @staticmethod
    def header_func(row, before, user_data):
        if before:
            row.set_header(None)
        else:
            label = Gtk.Label(label=user_data)
            label.set_halign(Gtk.Align.START)
            row.set_header(label)

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

118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143
    def request_last_activity(self):
        if not app.account_is_connected(self.account):
            return
        con = app.connections[self.account]
        iq = nbxmpp.Iq(to=self.hostname, typ='get', queryNS=nbxmpp.NS_LAST)
        con.connection.SendAndCallForResponse(iq, self._on_last_activity)

    def _on_last_activity(self, stanza):
        if 'server_info' not in app.interface.instances[self.account]:
            # Window got closed in the meantime
            return
        if not nbxmpp.isResultNode(stanza):
            log.warning('Received malformed result: %s', stanza)
            return
        if stanza.getQueryNS() != nbxmpp.NS_LAST:
            log.warning('Wrong namespace on result: %s', stanza)
            return
        try:
            seconds = int(stanza.getQuery().getAttr('seconds'))
        except (ValueError, TypeError, AttributeError):
            log.exception('Received malformed last activity result')
        else:
            delta = timedelta(seconds=seconds)
            hours = 0
            if seconds >= 3600:
                hours = delta.seconds // 3600
André's avatar
André committed
144 145
            self.uptime = _('%(days)s days, %(hours)s hours') % {
                'days': delta.days, 'hours': hours}
146 147
            self.update(self.get_infos, self.info_listbox)

Philipp Hörist's avatar
Philipp Hörist committed
148 149 150 151 152 153
    @ensure_not_destroyed
    def _software_version_received(self, result):
        if is_error_result(result):
            self.version = _('Unknown')
        else:
            self.version = '%s %s' % (result.name, result.version)
Philipp Hörist's avatar
Philipp Hörist committed
154 155
        self.update(self.get_infos, self.info_listbox)

156
    def _server_disco_received(self, obj):
Philipp Hörist's avatar
Philipp Hörist committed
157 158 159 160 161
        self.update(self.get_features, self.feature_listbox)

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

    def get_features(self):
165
        con = app.connections[self.account]
166 167
        Feature = namedtuple('Feature',
                             ['name', 'available', 'tooltip', 'enabled'])
168
        Feature.__new__.__defaults__ = (None, None)  # type: ignore
169

170
        # HTTP File Upload
171 172 173 174 175 176 177
        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
178

Philipp Hörist's avatar
Philipp Hörist committed
179
        return [
Philipp Hörist's avatar
Philipp Hörist committed
180
            Feature('XEP-0016: Privacy Lists',
181 182
                    con.get_module('PrivacyLists').supported),
            Feature('XEP-0045: Multi-User Chat', con.muc_jid),
183
            Feature('XEP-0054: vcard-temp',
184
                    con.get_module('VCardTemp').supported),
Philipp Hörist's avatar
Philipp Hörist committed
185
            Feature('XEP-0163: Personal Eventing Protocol',
186
                    con.get_module('PEP').supported),
Philipp Hörist's avatar
Philipp Hörist committed
187
            Feature('XEP-0163: #publish-options',
188
                    con.get_module('PubSub').publish_options),
Philipp Hörist's avatar
Philipp Hörist committed
189
            Feature('XEP-0191: Blocking Command',
190
                    con.get_module('Blocking').supported,
191
                    nbxmpp.NS_BLOCKING),
Philipp Hörist's avatar
Philipp Hörist committed
192
            Feature('XEP-0198: Stream Management',
193
                    con.connection.sm_enabled, nbxmpp.NS_STREAM_MGMT),
194 195
            Feature('XEP-0258: Security Labels in XMPP',
                    con.get_module('SecLabels').supported,
196
                    nbxmpp.NS_SECLABEL),
Philipp Hörist's avatar
Philipp Hörist committed
197
            Feature('XEP-0280: Message Carbons',
198
                    con.get_module('Carbons').supported,
199
                    nbxmpp.NS_CARBONS),
Philipp Hörist's avatar
Philipp Hörist committed
200
            Feature('XEP-0313: Message Archive Management',
201 202
                    con.get_module('MAM').available,
                    con.get_module('MAM').archiving_namespace),
Philipp Hörist's avatar
Philipp Hörist committed
203
            Feature('XEP-0363: HTTP File Upload',
Philipp Hörist's avatar
Philipp Hörist committed
204
                    con.get_module('HTTPUpload').available,
205
                    http_upload_info),
Philipp Hörist's avatar
Philipp Hörist committed
206
            Feature('XEP-0398: Avatar Conversion',
207
                    con.avatar_conversion),
208
            Feature('XEP-0411: Bookmarks Conversion',
209
                    con.get_module('Bookmarks').conversion)
Philipp Hörist's avatar
Philipp Hörist committed
210
        ]
Philipp Hörist's avatar
Philipp Hörist committed
211 212 213 214 215 216 217 218

    def add_info(self, info):
        self.info_listbox.add(ServerInfoItem(info))

    def get_infos(self):
        Info = namedtuple('Info', ['name', 'value', 'tooltip'])
        return [
            Info(_('Hostname'), self.hostname, None),
219 220
            Info(_('Server Software'), self.version, None),
            Info(_('Server Uptime'), self.uptime, None)]
Philipp Hörist's avatar
Philipp Hörist committed
221

222 223 224 225 226 227 228 229 230
    def on_clipboard_button_clicked(self, widget):
        server_software = 'Server Software: %s\n' % self.get_infos()[1].value
        server_features = ''

        for feature in self.get_features():
            if feature.available:
                available = 'Yes'
            else:
                available = 'No'
231
            if feature.tooltip is not None:
232 233 234 235 236 237 238 239
                tooltip = '(%s)' % feature.tooltip
            else:
                tooltip = ''
            server_features += '%s: %s %s\n' % (feature.name, available, tooltip)

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

Philipp Hörist's avatar
Philipp Hörist committed
240 241 242 243 244
    def on_response(self, dialog, response):
        if response == Gtk.ResponseType.OK:
            self.destroy()

    def on_destroy(self, *args):
Philipp Hörist's avatar
Philipp Hörist committed
245
        self._destroyed = True
246
        del app.interface.instances[self.account]['server_info']
247
        app.ged.remove_event_handler('server-disco-received',
Philipp Hörist's avatar
Philipp Hörist committed
248
                                     ged.GUI1,
249
                                     self._server_disco_received)
Philipp Hörist's avatar
Philipp Hörist committed
250

Philipp Hörist's avatar
Philipp Hörist committed
251 252 253 254

class FeatureItem(Gtk.Grid):
    def __init__(self, feature):
        super().__init__()
255
        self.tooltip = feature.tooltip
Philipp Hörist's avatar
Philipp Hörist committed
256 257 258 259
        self.set_column_spacing(6)

        self.icon = Gtk.Image()
        self.feature_label = Gtk.Label(label=feature.name)
260
        self.set_feature(feature.available, feature.enabled)
Philipp Hörist's avatar
Philipp Hörist committed
261 262 263 264

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

265
    def set_feature(self, available, enabled):
266 267 268 269
        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')

270
        if not available:
271 272 273
            self.icon.set_from_icon_name('window-close-symbolic',
                                         Gtk.IconSize.MENU)
            self.icon.get_style_context().add_class('error-color')
274
        elif enabled is False:
275 276
            self.icon.set_from_icon_name('dialog-warning-symbolic',
                                         Gtk.IconSize.MENU)
277
            self.tooltip += _('\nDisabled in config')
278
            self.icon.get_style_context().add_class('warning-color')
Philipp Hörist's avatar
Philipp Hörist committed
279
        else:
280 281 282
            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
283 284

    def update(self, feature):
285 286
        self.tooltip = feature.tooltip
        self.set_feature(feature.available, feature.enabled)
Philipp Hörist's avatar
Philipp Hörist committed
287

Philipp Hörist's avatar
Philipp Hörist committed
288

Philipp Hörist's avatar
Philipp Hörist committed
289 290 291
class ServerInfoItem(Gtk.Grid):
    def __init__(self, info):
        super().__init__()
292
        self.tooltip = info.tooltip
Philipp Hörist's avatar
Philipp Hörist committed
293 294 295 296
        self.insert_column(0)

        self.info = Gtk.Label(label=info.name)
        self.info.set_halign(Gtk.Align.START)
297 298
        self.info.set_xalign(0)
        self.info.set_size_request(160, -1)
Philipp Hörist's avatar
Philipp Hörist committed
299 300
        self.value = Gtk.Label(label=info.value)
        self.value.set_halign(Gtk.Align.START)
301 302 303
        self.value.set_ellipsize(Pango.EllipsizeMode.END)
        self.value.set_width_chars(20)
        self.value.set_xalign(0)
Philipp Hörist's avatar
Philipp Hörist committed
304
        self.value.set_hexpand(True)
305
        self.value.set_selectable(True)
Philipp Hörist's avatar
Philipp Hörist committed
306 307 308 309 310 311

        self.add(self.info)
        self.add(self.value)

    def update(self, info):
        self.value.set_text(info.value)