profile.py 13.7 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
# Copyright (C) 2003-2014 Yann Leboulanger <asterix AT lagaule.org>
# Copyright (C) 2005-2006 Nikos Kouremenos <kourem AT gmail.com>
# Copyright (C) 2006-2008 Jean-Marie Traissard <jim AT lapin.org>
#
# This file is part of Gajim.
#
# 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.
#
# Gajim is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# 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/>.
Yann Leboulanger's avatar
Yann Leboulanger committed
18 19

import base64
20
import time
Philipp Hörist's avatar
Philipp Hörist committed
21
import logging
22
import hashlib
Yann Leboulanger's avatar
Yann Leboulanger committed
23

24 25 26 27
from gi.repository import Gtk
from gi.repository import Gdk
from gi.repository import GLib

28 29 30 31
from gajim.common import app
from gajim.common import ged
from gajim.common.i18n import _
from gajim.common.const import AvatarSize
Philipp Hörist's avatar
Philipp Hörist committed
32
from gajim.common.helpers import event_filter
33

André's avatar
André committed
34
from gajim import gtkgui_helpers
35

36 37
from gajim.gtk.dialogs import ErrorDialog
from gajim.gtk.dialogs import InformationDialog
38
from gajim.gtk.util import get_builder
39
from gajim.gtk.filechoosers import AvatarChooserDialog
40

41

Philipp Hörist's avatar
Philipp Hörist committed
42
log = logging.getLogger('gajim.profile')
Yann Leboulanger's avatar
Yann Leboulanger committed
43

44

45 46 47 48 49 50 51 52 53 54 55
class ProfileWindow(Gtk.ApplicationWindow):
    def __init__(self, account):
        Gtk.ApplicationWindow.__init__(self)
        self.set_application(app.app)
        self.set_position(Gtk.WindowPosition.CENTER)
        self.set_show_menubar(False)
        self.set_title(_('Profile'))

        self.connect('destroy', self.on_profile_window_destroy)
        self.connect('key-press-event', self.on_profile_window_key_press_event)

56
        self.xml = get_builder('profile_window.ui')
57
        self.add(self.xml.get_object('profile_box'))
58 59 60 61 62
        self.progressbar = self.xml.get_object('progressbar')
        self.statusbar = self.xml.get_object('statusbar')
        self.context_id = self.statusbar.get_context_id('profile')

        self.account = account
63
        self.jid = app.get_jid_from_account(account)
64 65
        account_label = app.config.get_per(
            'accounts', account, 'account_label')
Sophie Herold's avatar
Sophie Herold committed
66
        self.set_value('account_label', account_label)
67 68 69 70

        self.dialog = None
        self.avatar_mime_type = None
        self.avatar_encoded = None
Philipp Hörist's avatar
Philipp Hörist committed
71
        self.avatar_sha = None
72
        self.message_id = self.statusbar.push(self.context_id,
73 74 75
                                              _('Retrieving profile…'))
        self.update_progressbar_timeout_id = GLib.timeout_add(
            100, self.update_progressbar)
76 77 78
        self.remove_statusbar_timeout_id = None

        self.xml.connect_signals(self)
79
        app.ged.register_event_handler('vcard-published', ged.GUI1,
80
                                       self._nec_vcard_published)
81
        app.ged.register_event_handler('vcard-not-published', ged.GUI1,
82 83 84
                                       self._nec_vcard_not_published)

        self.show_all()
85
        self.xml.get_object('ok_button').grab_focus()
86
        app.connections[account].get_module('VCardTemp').request_vcard(
Philipp Hörist's avatar
Philipp Hörist committed
87
            self._nec_vcard_received, self.jid)
88 89

    def on_information_notebook_switch_page(self, widget, page, page_num):
Yann Leboulanger's avatar
Yann Leboulanger committed
90
        GLib.idle_add(self.xml.get_object('ok_button').grab_focus)
91 92 93

    def update_progressbar(self):
        self.progressbar.pulse()
94
        return True
95 96

    def remove_statusbar(self, message_id):
Dicson's avatar
Dicson committed
97
        self.statusbar.remove(self.context_id, message_id)
98 99 100 101
        self.remove_statusbar_timeout_id = None

    def on_profile_window_destroy(self, widget):
        if self.update_progressbar_timeout_id is not None:
Yann Leboulanger's avatar
Yann Leboulanger committed
102
            GLib.source_remove(self.update_progressbar_timeout_id)
103
        if self.remove_statusbar_timeout_id is not None:
Yann Leboulanger's avatar
Yann Leboulanger committed
104
            GLib.source_remove(self.remove_statusbar_timeout_id)
105 106 107 108
        app.ged.remove_event_handler(
            'vcard-published', ged.GUI1, self._nec_vcard_published)
        app.ged.remove_event_handler(
            'vcard-not-published', ged.GUI1, self._nec_vcard_not_published)
109

110
        if self.dialog:  # Image chooser dialog
111 112 113
            self.dialog.destroy()

    def on_profile_window_key_press_event(self, widget, event):
114
        if event.keyval == Gdk.KEY_Escape:
115
            self.destroy()
116

Philipp Hörist's avatar
Philipp Hörist committed
117
    def _clear_photo(self, widget):
118 119
        # empty the image
        button = self.xml.get_object('PHOTO_button')
Sophie Herold's avatar
Sophie Herold committed
120
        image = self.xml.get_object('PHOTO_image')
121 122 123 124 125
        image.set_from_pixbuf(None)
        button.hide()
        text_button = self.xml.get_object('NOPHOTO_button')
        text_button.show()
        self.avatar_encoded = None
Philipp Hörist's avatar
Philipp Hörist committed
126
        self.avatar_sha = None
127 128
        self.avatar_mime_type = None

129
    def _on_set_avatar_clicked(self, _button):
Philipp Hörist's avatar
Philipp Hörist committed
130
        def on_ok(path_to_file):
Philipp Hörist's avatar
Philipp Hörist committed
131 132
            data, sha = app.interface.avatar_storage.prepare_for_publish(
                path_to_file)
Philipp Hörist's avatar
Philipp Hörist committed
133
            if sha is None:
134
                ErrorDialog(
135
                    _('Could not load image'), transient_for=self)
136
                return
Philipp Hörist's avatar
Philipp Hörist committed
137

138
            scale = self.get_scale_factor()
Philipp Hörist's avatar
Philipp Hörist committed
139 140
            surface = app.interface.avatar_storage.surface_from_filename(
                sha, AvatarSize.VCARD, scale)
Philipp Hörist's avatar
Philipp Hörist committed
141

142
            button = self.xml.get_object('PHOTO_button')
Sophie Herold's avatar
Sophie Herold committed
143
            image = self.xml.get_object('PHOTO_image')
144
            image.set_from_surface(surface)
145 146 147
            button.show()
            text_button = self.xml.get_object('NOPHOTO_button')
            text_button.hide()
Philipp Hörist's avatar
Philipp Hörist committed
148 149

            self.avatar_sha = sha
Philipp Hörist's avatar
Philipp Hörist committed
150
            self.avatar_encoded = base64.b64encode(data).decode('utf-8')
Philipp Hörist's avatar
Philipp Hörist committed
151
            self.avatar_mime_type = 'image/png'
152

153
        AvatarChooserDialog(on_ok, transient_for=self)
154

155 156 157 158 159 160 161 162 163
    def on_BDAY_entry_focus_out_event(self, widget, event):
        txt = widget.get_text()
        if not txt:
            return
        try:
            time.strptime(txt, '%Y-%m-%d')
        except ValueError:
            if not widget.is_focus():
                pritext = _('Wrong date format')
164 165 166 167
                ErrorDialog(
                    pritext,
                    _('Format of the date must be YYYY-MM-DD'),
                    transient_for=self)
Philipp Hörist's avatar
Philipp Hörist committed
168
                GLib.idle_add(widget.grab_focus)
169 170
            return True

171 172
    def set_value(self, entry_name, value):
        try:
173 174 175 176 177
            widget = self.xml.get_object(entry_name)
            val = widget.get_text()
            if val:
                value = val + ' / ' + value
            widget.set_text(value)
178 179 180 181 182
        except AttributeError:
            pass

    def set_values(self, vcard_):
        button = self.xml.get_object('PHOTO_button')
Sophie Herold's avatar
Sophie Herold committed
183
        image = self.xml.get_object('PHOTO_image')
184
        text_button = self.xml.get_object('NOPHOTO_button')
185
        if 'PHOTO' not in vcard_:
186 187 188 189 190 191
            # set default image
            image.set_from_pixbuf(None)
            button.hide()
            text_button.show()
        for i in vcard_.keys():
            if i == 'PHOTO':
Philipp Hörist's avatar
Philipp Hörist committed
192 193
                photo_encoded = vcard_[i]['BINVAL']
                if photo_encoded == '':
194
                    continue
195
                self.avatar_encoded = photo_encoded
Philipp Hörist's avatar
Philipp Hörist committed
196
                photo_decoded = base64.b64decode(photo_encoded.encode('utf-8'))
197 198 199
                self.avatar_sha = hashlib.sha1(photo_decoded).hexdigest()
                if 'TYPE' in vcard_[i]:
                    self.avatar_mime_type = vcard_[i]['TYPE']
Philipp Hörist's avatar
Philipp Hörist committed
200

201
                scale = self.get_scale_factor()
Philipp Hörist's avatar
Philipp Hörist committed
202
                surface = app.interface.avatar_storage.surface_from_filename(
Philipp Hörist's avatar
Philipp Hörist committed
203 204 205 206 207 208 209
                    self.avatar_sha, AvatarSize.VCARD, scale)
                if surface is None:
                    pixbuf = gtkgui_helpers.scale_pixbuf_from_data(
                        photo_decoded, AvatarSize.VCARD)
                    surface = Gdk.cairo_surface_create_from_pixbuf(pixbuf,
                                                                   scale)
                image.set_from_surface(surface)
210 211 212
                button.show()
                text_button.hide()
                continue
213
            if i in ('ADR', 'TEL', 'EMAIL'):
214 215 216 217 218 219 220 221 222 223 224 225
                for entry in vcard_[i]:
                    add_on = '_HOME'
                    if 'WORK' in entry:
                        add_on = '_WORK'
                    for j in entry.keys():
                        self.set_value(i + add_on + '_' + j + '_entry', entry[j])
            if isinstance(vcard_[i], dict):
                for j in vcard_[i].keys():
                    self.set_value(i + '_' + j + '_entry', vcard_[i][j])
            else:
                if i == 'DESC':
                    self.xml.get_object('DESC_textview').get_buffer().set_text(
Philipp Hörist's avatar
Philipp Hörist committed
226
                        vcard_[i], len(vcard_[i].encode('utf-8')))
227 228 229 230
                else:
                    self.set_value(i + '_entry', vcard_[i])
        if self.update_progressbar_timeout_id is not None:
            if self.message_id:
Dicson's avatar
Dicson committed
231
                self.statusbar.remove(self.context_id, self.message_id)
232 233 234 235
            self.message_id = self.statusbar.push(
                self.context_id, _('Information received'))
            self.remove_statusbar_timeout_id = GLib.timeout_add_seconds(
                3, self.remove_statusbar, self.message_id)
Yann Leboulanger's avatar
Yann Leboulanger committed
236
            GLib.source_remove(self.update_progressbar_timeout_id)
237 238 239 240
            self.progressbar.hide()
            self.progressbar.set_fraction(0)
            self.update_progressbar_timeout_id = None

241
    def _nec_vcard_received(self, jid, resource, room, vcard_, *args):
Philipp Hörist's avatar
Philipp Hörist committed
242
        self.set_values(vcard_)
243

244 245 246 247 248 249
    def add_to_vcard(self, vcard_, entry, txt):
        """
        Add an information to the vCard dictionary
        """
        entries = entry.split('_')
        loc = vcard_
250
        if len(entries) == 3:  # We need to use lists
251 252
            if entries[0] not in loc:
                loc[entries[0]] = []
253

254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272
            for e in loc[entries[0]]:
                if entries[1] in e:
                    e[entries[2]] = txt
                    break
            else:
                loc[entries[0]].append({entries[1]: '', entries[2]: txt})
            return vcard_
        while len(entries) > 1:
            if entries[0] not in loc:
                loc[entries[0]] = {}
            loc = loc[entries[0]]
            del entries[0]
        loc[entries[0]] = txt
        return vcard_

    def make_vcard(self):
        """
        Make the vCard dictionary
        """
273 274 275 276 277 278 279 280
        entries = [
            'FN', 'NICKNAME', 'BDAY', 'EMAIL_HOME_USERID', 'JABBERID', 'URL',
            'TEL_HOME_NUMBER', 'N_FAMILY', 'N_GIVEN', 'N_MIDDLE', 'N_PREFIX',
            'N_SUFFIX', 'ADR_HOME_STREET', 'ADR_HOME_EXTADR', 'ADR_HOME_LOCALITY',
            'ADR_HOME_REGION', 'ADR_HOME_PCODE', 'ADR_HOME_CTRY', 'ORG_ORGNAME',
            'ORG_ORGUNIT', 'TITLE', 'ROLE', 'TEL_WORK_NUMBER', 'EMAIL_WORK_USERID',
            'ADR_WORK_STREET', 'ADR_WORK_EXTADR', 'ADR_WORK_LOCALITY',
            'ADR_WORK_REGION', 'ADR_WORK_PCODE', 'ADR_WORK_CTRY']
281 282
        vcard_ = {}
        for e in entries:
283
            txt = self.xml.get_object(e + '_entry').get_text()
284 285 286 287 288 289 290
            if txt != '':
                vcard_ = self.add_to_vcard(vcard_, e, txt)

        # DESC textview
        buff = self.xml.get_object('DESC_textview').get_buffer()
        start_iter = buff.get_start_iter()
        end_iter = buff.get_end_iter()
291
        txt = buff.get_text(start_iter, end_iter, False)
292
        if txt != '':
293
            vcard_['DESC'] = txt
294 295 296 297 298 299

        # Avatar
        if self.avatar_encoded:
            vcard_['PHOTO'] = {'BINVAL': self.avatar_encoded}
            if self.avatar_mime_type:
                vcard_['PHOTO']['TYPE'] = self.avatar_mime_type
Philipp Hörist's avatar
Philipp Hörist committed
300
        return vcard_, self.avatar_sha
301 302 303 304 305

    def on_ok_button_clicked(self, widget):
        if self.update_progressbar_timeout_id:
            # Operation in progress
            return
306
        if not app.account_is_connected(self.account):
307 308 309 310 311
            ErrorDialog(
                _('You are not connected to the server'),
                _('Without a connection, you can not publish your contact '
                  'information.'),
                transient_for=self)
312
            return
Philipp Hörist's avatar
Philipp Hörist committed
313
        vcard_, sha = self.make_vcard()
314
        nick = vcard_.get('NICKNAME') or None
315
        app.connections[self.account].get_module('UserNickname').set_nickname(nick)
316
        if not nick:
317 318
            nick = app.config.get_per('accounts', self.account, 'name')
        app.nicks[self.account] = nick
319 320
        app.connections[self.account].get_module('VCardTemp').send_vcard(
            vcard_, sha)
321 322
        self.message_id = self.statusbar.push(
            self.context_id, _('Sending profile…'))
323
        self.progressbar.show()
324 325
        self.update_progressbar_timeout_id = GLib.timeout_add(
            100, self.update_progressbar)
326

Philipp Hörist's avatar
Philipp Hörist committed
327 328
    @event_filter(['account'])
    def _nec_vcard_published(self, _event):
329
        if self.update_progressbar_timeout_id is not None:
Yann Leboulanger's avatar
Yann Leboulanger committed
330
            GLib.source_remove(self.update_progressbar_timeout_id)
331
            self.update_progressbar_timeout_id = None
332
        self.destroy()
333

Philipp Hörist's avatar
Philipp Hörist committed
334 335
    @event_filter(['account'])
    def _nec_vcard_not_published(self, _event):
336
        if self.message_id:
Dicson's avatar
Dicson committed
337
            self.statusbar.remove(self.context_id, self.message_id)
338 339 340 341
        self.message_id = self.statusbar.push(
            self.context_id, _('Information NOT published'))
        self.remove_statusbar_timeout_id = GLib.timeout_add_seconds(
            3, self.remove_statusbar, self.message_id)
342
        if self.update_progressbar_timeout_id is not None:
Yann Leboulanger's avatar
Yann Leboulanger committed
343
            GLib.source_remove(self.update_progressbar_timeout_id)
344 345
            self.progressbar.set_fraction(0)
            self.update_progressbar_timeout_id = None
346 347
        InformationDialog(
            _('vCard publication failed'),
348
            _('There was an error while publishing your personal information, '
349
              'try again later.'), transient_for=self)
350 351

    def on_cancel_button_clicked(self, widget):
352
        self.destroy()