This leads to multiple problems 1. We cant assume only items with id='current' are stored in the node which would lead to retracting 'current' but another item would become the last published and sent to users 2. Even if we have a SingletonNode retracting the only item means the Node would be empty and offline clients would not receive the last published item on coming online, because there is no item anymore Instead we always publish an empty item from now on
374 lines
15 KiB
Python
374 lines
15 KiB
Python
# 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/>.
|
|
|
|
import base64
|
|
import time
|
|
import logging
|
|
import hashlib
|
|
|
|
from gi.repository import Gtk
|
|
from gi.repository import Gdk
|
|
from gi.repository import GLib
|
|
|
|
from gajim import gtkgui_helpers
|
|
from gajim.gtk import ErrorDialog
|
|
from gajim.gtk import InformationDialog
|
|
from gajim.gtk.util import get_builder
|
|
from gajim.gtk.filechoosers import AvatarChooserDialog
|
|
from gajim.common.const import AvatarSize
|
|
from gajim.common import app
|
|
from gajim.common import ged
|
|
|
|
log = logging.getLogger('gajim.profile')
|
|
|
|
|
|
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)
|
|
|
|
self.xml = get_builder('profile_window.ui')
|
|
self.add(self.xml.get_object('profile_box'))
|
|
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
|
|
self.jid = app.get_jid_from_account(account)
|
|
account_label = app.config.get_per(
|
|
'accounts', account, 'account_label')
|
|
self.set_value('account_label', account_label)
|
|
|
|
self.dialog = None
|
|
self.avatar_mime_type = None
|
|
self.avatar_encoded = None
|
|
self.avatar_sha = None
|
|
self.message_id = self.statusbar.push(self.context_id,
|
|
_('Retrieving profile…'))
|
|
self.update_progressbar_timeout_id = GLib.timeout_add(
|
|
100, self.update_progressbar)
|
|
self.remove_statusbar_timeout_id = None
|
|
|
|
self.xml.connect_signals(self)
|
|
app.ged.register_event_handler('vcard-published', ged.GUI1,
|
|
self._nec_vcard_published)
|
|
app.ged.register_event_handler('vcard-not-published', ged.GUI1,
|
|
self._nec_vcard_not_published)
|
|
|
|
self.show_all()
|
|
self.xml.get_object('ok_button').grab_focus()
|
|
app.connections[account].get_module('VCardTemp').request_vcard(
|
|
self._nec_vcard_received, self.jid)
|
|
|
|
def on_information_notebook_switch_page(self, widget, page, page_num):
|
|
GLib.idle_add(self.xml.get_object('ok_button').grab_focus)
|
|
|
|
def update_progressbar(self):
|
|
self.progressbar.pulse()
|
|
return True
|
|
|
|
def remove_statusbar(self, message_id):
|
|
self.statusbar.remove(self.context_id, message_id)
|
|
self.remove_statusbar_timeout_id = None
|
|
|
|
def on_profile_window_destroy(self, widget):
|
|
if self.update_progressbar_timeout_id is not None:
|
|
GLib.source_remove(self.update_progressbar_timeout_id)
|
|
if self.remove_statusbar_timeout_id is not None:
|
|
GLib.source_remove(self.remove_statusbar_timeout_id)
|
|
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)
|
|
|
|
if self.dialog: # Image chooser dialog
|
|
self.dialog.destroy()
|
|
|
|
def on_profile_window_key_press_event(self, widget, event):
|
|
if event.keyval == Gdk.KEY_Escape:
|
|
self.destroy()
|
|
|
|
def _clear_photo(self, widget):
|
|
# empty the image
|
|
button = self.xml.get_object('PHOTO_button')
|
|
image = self.xml.get_object('PHOTO_image')
|
|
image.set_from_pixbuf(None)
|
|
button.hide()
|
|
text_button = self.xml.get_object('NOPHOTO_button')
|
|
text_button.show()
|
|
self.avatar_encoded = None
|
|
self.avatar_sha = None
|
|
self.avatar_mime_type = None
|
|
|
|
def on_set_avatar_button_clicked(self, widget):
|
|
def on_ok(path_to_file):
|
|
sha = app.interface.save_avatar(path_to_file, publish=True)
|
|
if sha is None:
|
|
ErrorDialog(
|
|
_('Could not load image'), transient_for=self)
|
|
return
|
|
|
|
scale = self.get_scale_factor()
|
|
surface = app.interface.get_avatar(sha, AvatarSize.VCARD, scale)
|
|
|
|
button = self.xml.get_object('PHOTO_button')
|
|
image = self.xml.get_object('PHOTO_image')
|
|
image.set_from_surface(surface)
|
|
button.show()
|
|
text_button = self.xml.get_object('NOPHOTO_button')
|
|
text_button.hide()
|
|
|
|
self.avatar_sha = sha
|
|
publish = app.interface.get_avatar(sha, publish=True)
|
|
self.avatar_encoded = base64.b64encode(publish).decode('utf-8')
|
|
self.avatar_mime_type = 'image/png'
|
|
|
|
AvatarChooserDialog(on_ok, transient_for=self)
|
|
|
|
def on_PHOTO_button_press_event(self, widget, event):
|
|
"""
|
|
If right-clicked, show popup
|
|
"""
|
|
|
|
if event.button == 3:
|
|
# right click
|
|
menu = Gtk.Menu()
|
|
|
|
nick = app.config.get_per('accounts', self.account, 'name')
|
|
if self.avatar_sha is None:
|
|
return
|
|
menuitem = Gtk.MenuItem.new_with_mnemonic(_('Save _As'))
|
|
menuitem.connect(
|
|
'activate',
|
|
gtkgui_helpers.on_avatar_save_as_menuitem_activate,
|
|
self.avatar_sha, nick)
|
|
menu.append(menuitem)
|
|
menu.connect('selection-done', lambda w: w.destroy())
|
|
# show the menu
|
|
menu.show_all()
|
|
menu.attach_to_widget(widget, None)
|
|
menu.popup(None, None, None, None, event.button, event.time)
|
|
elif event.button == 1: # left click
|
|
self.on_set_avatar_button_clicked(widget)
|
|
|
|
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')
|
|
ErrorDialog(
|
|
pritext,
|
|
_('Format of the date must be YYYY-MM-DD'),
|
|
transient_for=self)
|
|
GLib.idle_add(lambda: widget.grab_focus())
|
|
return True
|
|
|
|
def set_value(self, entry_name, value):
|
|
try:
|
|
widget = self.xml.get_object(entry_name)
|
|
val = widget.get_text()
|
|
if val:
|
|
value = val + ' / ' + value
|
|
widget.set_text(value)
|
|
except AttributeError:
|
|
pass
|
|
|
|
def set_values(self, vcard_):
|
|
button = self.xml.get_object('PHOTO_button')
|
|
image = self.xml.get_object('PHOTO_image')
|
|
text_button = self.xml.get_object('NOPHOTO_button')
|
|
if 'PHOTO' not in vcard_:
|
|
# set default image
|
|
image.set_from_pixbuf(None)
|
|
button.hide()
|
|
text_button.show()
|
|
for i in vcard_.keys():
|
|
if i == 'PHOTO':
|
|
photo_encoded = vcard_[i]['BINVAL']
|
|
if photo_encoded == '':
|
|
continue
|
|
self.avatar_encoded = photo_encoded
|
|
photo_decoded = base64.b64decode(photo_encoded.encode('utf-8'))
|
|
self.avatar_sha = hashlib.sha1(photo_decoded).hexdigest()
|
|
if 'TYPE' in vcard_[i]:
|
|
self.avatar_mime_type = vcard_[i]['TYPE']
|
|
|
|
scale = self.get_scale_factor()
|
|
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)
|
|
button.show()
|
|
text_button.hide()
|
|
continue
|
|
if i == 'ADR' or i == 'TEL' or i == 'EMAIL':
|
|
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(
|
|
vcard_[i], len(vcard_[i].encode('utf-8')))
|
|
else:
|
|
self.set_value(i + '_entry', vcard_[i])
|
|
if self.update_progressbar_timeout_id is not None:
|
|
if self.message_id:
|
|
self.statusbar.remove(self.context_id, self.message_id)
|
|
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)
|
|
GLib.source_remove(self.update_progressbar_timeout_id)
|
|
self.progressbar.hide()
|
|
self.progressbar.set_fraction(0)
|
|
self.update_progressbar_timeout_id = None
|
|
|
|
def _nec_vcard_received(self, jid, resource, room, vcard_, *args):
|
|
self.set_values(vcard_)
|
|
|
|
def add_to_vcard(self, vcard_, entry, txt):
|
|
"""
|
|
Add an information to the vCard dictionary
|
|
"""
|
|
entries = entry.split('_')
|
|
loc = vcard_
|
|
if len(entries) == 3: # We need to use lists
|
|
if entries[0] not in loc:
|
|
loc[entries[0]] = []
|
|
|
|
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
|
|
"""
|
|
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']
|
|
vcard_ = {}
|
|
for e in entries:
|
|
txt = self.xml.get_object(e + '_entry').get_text()
|
|
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()
|
|
txt = buff.get_text(start_iter, end_iter, False)
|
|
if txt != '':
|
|
vcard_['DESC'] = txt
|
|
|
|
# Avatar
|
|
if self.avatar_encoded:
|
|
vcard_['PHOTO'] = {'BINVAL': self.avatar_encoded}
|
|
if self.avatar_mime_type:
|
|
vcard_['PHOTO']['TYPE'] = self.avatar_mime_type
|
|
return vcard_, self.avatar_sha
|
|
|
|
def on_ok_button_clicked(self, widget):
|
|
if self.update_progressbar_timeout_id:
|
|
# Operation in progress
|
|
return
|
|
if app.connections[self.account].connected < 2:
|
|
ErrorDialog(
|
|
_('You are not connected to the server'),
|
|
_('Without a connection, you can not publish your contact '
|
|
'information.'),
|
|
transient_for=self)
|
|
return
|
|
vcard_, sha = self.make_vcard()
|
|
nick = vcard_.get('NICKNAME') or None
|
|
app.connections[self.account].get_module('UserNickname').send(nick)
|
|
if not nick:
|
|
nick = app.config.get_per('accounts', self.account, 'name')
|
|
app.nicks[self.account] = nick
|
|
app.connections[self.account].get_module('VCardTemp').send_vcard(
|
|
vcard_, sha)
|
|
self.message_id = self.statusbar.push(
|
|
self.context_id, _('Sending profile…'))
|
|
self.progressbar.show()
|
|
self.update_progressbar_timeout_id = GLib.timeout_add(
|
|
100, self.update_progressbar)
|
|
|
|
def _nec_vcard_published(self, obj):
|
|
if obj.conn.name != self.account:
|
|
return
|
|
if self.update_progressbar_timeout_id is not None:
|
|
GLib.source_remove(self.update_progressbar_timeout_id)
|
|
self.update_progressbar_timeout_id = None
|
|
self.destroy()
|
|
|
|
def _nec_vcard_not_published(self, obj):
|
|
if obj.conn.name != self.account:
|
|
return
|
|
if self.message_id:
|
|
self.statusbar.remove(self.context_id, self.message_id)
|
|
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)
|
|
if self.update_progressbar_timeout_id is not None:
|
|
GLib.source_remove(self.update_progressbar_timeout_id)
|
|
self.progressbar.set_fraction(0)
|
|
self.update_progressbar_timeout_id = None
|
|
InformationDialog(
|
|
_('vCard publication failed'),
|
|
_('There was an error while publishing your personal information, '
|
|
'try again later.'), transient_for=self)
|
|
|
|
def on_cancel_button_clicked(self, widget):
|
|
self.destroy()
|