profile.py 14.6 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

André's avatar
André committed
28
from gajim import gtkgui_helpers
29
30
from gajim.gtk import ErrorDialog
from gajim.gtk import InformationDialog
31
from gajim.gtk.util import get_builder
32
from gajim.gtk.filechoosers import AvatarChooserDialog
Philipp Hörist's avatar
Philipp Hörist committed
33
from gajim.common.const import AvatarSize
34
from gajim.common import app
André's avatar
André committed
35
from gajim.common import ged
36

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

39

40
41
42
43
44
45
46
47
48
49
50
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)

51
        self.xml = get_builder('profile_window.ui')
52
        self.add(self.xml.get_object('profile_box'))
53
54
55
56
57
        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
58
        self.jid = app.get_jid_from_account(account)
59
60
        account_label = app.config.get_per(
            'accounts', account, 'account_label')
Sophie Herold's avatar
Sophie Herold committed
61
        self.set_value('account_label', account_label)
62
63
64
65

        self.dialog = None
        self.avatar_mime_type = None
        self.avatar_encoded = None
Philipp Hörist's avatar
Philipp Hörist committed
66
        self.avatar_sha = None
67
        self.message_id = self.statusbar.push(self.context_id,
68
69
70
                                              _('Retrieving profile…'))
        self.update_progressbar_timeout_id = GLib.timeout_add(
            100, self.update_progressbar)
71
72
73
        self.remove_statusbar_timeout_id = None

        self.xml.connect_signals(self)
74
        app.ged.register_event_handler('vcard-published', ged.GUI1,
75
                                       self._nec_vcard_published)
76
        app.ged.register_event_handler('vcard-not-published', ged.GUI1,
77
78
79
                                       self._nec_vcard_not_published)

        self.show_all()
80
        self.xml.get_object('ok_button').grab_focus()
81
        app.connections[account].get_module('VCardTemp').request_vcard(
Philipp Hörist's avatar
Philipp Hörist committed
82
            self._nec_vcard_received, self.jid)
83
84

    def on_information_notebook_switch_page(self, widget, page, page_num):
Yann Leboulanger's avatar
Yann Leboulanger committed
85
        GLib.idle_add(self.xml.get_object('ok_button').grab_focus)
86
87
88

    def update_progressbar(self):
        self.progressbar.pulse()
89
        return True
90
91

    def remove_statusbar(self, message_id):
Dicson's avatar
Dicson committed
92
        self.statusbar.remove(self.context_id, message_id)
93
94
95
96
        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
97
            GLib.source_remove(self.update_progressbar_timeout_id)
98
        if self.remove_statusbar_timeout_id is not None:
Yann Leboulanger's avatar
Yann Leboulanger committed
99
            GLib.source_remove(self.remove_statusbar_timeout_id)
100
101
102
103
        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)
104

105
        if self.dialog:  # Image chooser dialog
106
107
108
            self.dialog.destroy()

    def on_profile_window_key_press_event(self, widget, event):
109
        if event.keyval == Gdk.KEY_Escape:
110
            self.destroy()
111

Philipp Hörist's avatar
Philipp Hörist committed
112
    def _clear_photo(self, widget):
113
114
        # empty the image
        button = self.xml.get_object('PHOTO_button')
Sophie Herold's avatar
Sophie Herold committed
115
        image = self.xml.get_object('PHOTO_image')
116
117
118
119
120
        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
121
        self.avatar_sha = None
122
123
124
        self.avatar_mime_type = None

    def on_set_avatar_button_clicked(self, widget):
Philipp Hörist's avatar
Philipp Hörist committed
125
        def on_ok(path_to_file):
Philipp Hörist's avatar
Philipp Hörist committed
126
            sha = app.interface.save_avatar(path_to_file, publish=True)
Philipp Hörist's avatar
Philipp Hörist committed
127
            if sha is None:
128
                ErrorDialog(
129
                    _('Could not load image'), transient_for=self)
130
                return
Philipp Hörist's avatar
Philipp Hörist committed
131

132
            scale = self.get_scale_factor()
133
            surface = app.interface.get_avatar(sha, AvatarSize.VCARD, scale)
Philipp Hörist's avatar
Philipp Hörist committed
134

135
            button = self.xml.get_object('PHOTO_button')
Sophie Herold's avatar
Sophie Herold committed
136
            image = self.xml.get_object('PHOTO_image')
137
            image.set_from_surface(surface)
138
139
140
            button.show()
            text_button = self.xml.get_object('NOPHOTO_button')
            text_button.hide()
Philipp Hörist's avatar
Philipp Hörist committed
141
142
143
144

            self.avatar_sha = sha
            publish = app.interface.get_avatar(sha, publish=True)
            self.avatar_encoded = base64.b64encode(publish).decode('utf-8')
Philipp Hörist's avatar
Philipp Hörist committed
145
            self.avatar_mime_type = 'image/png'
146

147
        AvatarChooserDialog(on_ok, transient_for=self)
148
149
150
151
152

    def on_PHOTO_button_press_event(self, widget, event):
        """
        If right-clicked, show popup
        """
153
154
155

        if event.button == 3:
            # right click
156
            menu = Gtk.Menu()
157

Philipp Hörist's avatar
Philipp Hörist committed
158
            nick = app.config.get_per('accounts', self.account, 'name')
159
160
            if self.avatar_sha is None:
                return
Philipp Hörist's avatar
Philipp Hörist committed
161
            menuitem = Gtk.MenuItem.new_with_mnemonic(_('Save _As'))
162
163
            menuitem.connect(
                'activate',
Philipp Hörist's avatar
Philipp Hörist committed
164
                gtkgui_helpers.on_avatar_save_as_menuitem_activate,
165
                self.avatar_sha, nick)
Philipp Hörist's avatar
Philipp Hörist committed
166
            menu.append(menuitem)
167
            menu.connect('selection-done', lambda w: w.destroy())
168
169
            # show the menu
            menu.show_all()
Dicson's avatar
Dicson committed
170
            menu.attach_to_widget(widget, None)
Yann Leboulanger's avatar
Yann Leboulanger committed
171
            menu.popup(None, None, None, None, event.button, event.time)
172
        elif event.button == 1:  # left click
173
174
            self.on_set_avatar_button_clicked(widget)

175
176
177
178
179
180
181
182
183
    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')
184
185
186
187
                ErrorDialog(
                    pritext,
                    _('Format of the date must be YYYY-MM-DD'),
                    transient_for=self)
Yann Leboulanger's avatar
Yann Leboulanger committed
188
                GLib.idle_add(lambda: widget.grab_focus())
189
190
            return True

191
192
    def set_value(self, entry_name, value):
        try:
193
194
195
196
197
            widget = self.xml.get_object(entry_name)
            val = widget.get_text()
            if val:
                value = val + ' / ' + value
            widget.set_text(value)
198
199
200
201
202
        except AttributeError:
            pass

    def set_values(self, vcard_):
        button = self.xml.get_object('PHOTO_button')
Sophie Herold's avatar
Sophie Herold committed
203
        image = self.xml.get_object('PHOTO_image')
204
        text_button = self.xml.get_object('NOPHOTO_button')
205
        if 'PHOTO' not in vcard_:
206
207
208
209
210
211
            # 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
212
213
                photo_encoded = vcard_[i]['BINVAL']
                if photo_encoded == '':
214
                    continue
215
                self.avatar_encoded = photo_encoded
Philipp Hörist's avatar
Philipp Hörist committed
216
                photo_decoded = base64.b64decode(photo_encoded.encode('utf-8'))
217
218
219
                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
220

221
                scale = self.get_scale_factor()
Philipp Hörist's avatar
Philipp Hörist committed
222
223
224
225
226
227
228
229
                surface = app.interface.get_avatar(
                    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)
230
231
232
                button.show()
                text_button.hide()
                continue
233
            if i in ('ADR', 'TEL', 'EMAIL'):
234
235
236
237
238
239
240
241
242
243
244
245
                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
246
                        vcard_[i], len(vcard_[i].encode('utf-8')))
247
248
249
250
                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
251
                self.statusbar.remove(self.context_id, self.message_id)
252
253
254
255
            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
256
            GLib.source_remove(self.update_progressbar_timeout_id)
257
258
259
260
            self.progressbar.hide()
            self.progressbar.set_fraction(0)
            self.update_progressbar_timeout_id = None

261
    def _nec_vcard_received(self, jid, resource, room, vcard_, *args):
Philipp Hörist's avatar
Philipp Hörist committed
262
        self.set_values(vcard_)
263

264
265
266
267
268
269
    def add_to_vcard(self, vcard_, entry, txt):
        """
        Add an information to the vCard dictionary
        """
        entries = entry.split('_')
        loc = vcard_
270
        if len(entries) == 3:  # We need to use lists
271
272
            if entries[0] not in loc:
                loc[entries[0]] = []
273

274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
            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
        """
293
294
295
296
297
298
299
300
        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']
301
302
        vcard_ = {}
        for e in entries:
303
            txt = self.xml.get_object(e + '_entry').get_text()
304
305
306
307
308
309
310
            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()
311
        txt = buff.get_text(start_iter, end_iter, False)
312
        if txt != '':
313
            vcard_['DESC'] = txt
314
315
316
317
318
319

        # 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
320
        return vcard_, self.avatar_sha
321
322
323
324
325

    def on_ok_button_clicked(self, widget):
        if self.update_progressbar_timeout_id:
            # Operation in progress
            return
326
        if app.connections[self.account].connected < 2:
327
328
329
330
331
            ErrorDialog(
                _('You are not connected to the server'),
                _('Without a connection, you can not publish your contact '
                  'information.'),
                transient_for=self)
332
            return
Philipp Hörist's avatar
Philipp Hörist committed
333
        vcard_, sha = self.make_vcard()
334
335
336
        nick = vcard_.get('NICKNAME') or None
        app.connections[self.account].get_module('UserNickname').send(nick)
        if not nick:
337
338
            nick = app.config.get_per('accounts', self.account, 'name')
        app.nicks[self.account] = nick
339
340
        app.connections[self.account].get_module('VCardTemp').send_vcard(
            vcard_, sha)
341
342
        self.message_id = self.statusbar.push(
            self.context_id, _('Sending profile…'))
343
        self.progressbar.show()
344
345
        self.update_progressbar_timeout_id = GLib.timeout_add(
            100, self.update_progressbar)
346

347
348
349
    def _nec_vcard_published(self, obj):
        if obj.conn.name != self.account:
            return
350
        if self.update_progressbar_timeout_id is not None:
Yann Leboulanger's avatar
Yann Leboulanger committed
351
            GLib.source_remove(self.update_progressbar_timeout_id)
352
            self.update_progressbar_timeout_id = None
353
        self.destroy()
354

355
356
357
    def _nec_vcard_not_published(self, obj):
        if obj.conn.name != self.account:
            return
358
        if self.message_id:
Dicson's avatar
Dicson committed
359
            self.statusbar.remove(self.context_id, self.message_id)
360
361
362
363
        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)
364
        if self.update_progressbar_timeout_id is not None:
Yann Leboulanger's avatar
Yann Leboulanger committed
365
            GLib.source_remove(self.update_progressbar_timeout_id)
366
367
            self.progressbar.set_fraction(0)
            self.update_progressbar_timeout_id = None
368
369
        InformationDialog(
            _('vCard publication failed'),
370
            _('There was an error while publishing your personal information, '
371
              'try again later.'), transient_for=self)
372
373

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