Add support for Room Avatars

- Groupchats promote a vcard hash with presence

Refactoring:

- Dont delete groupchat contacts if they are maximized from the roster
- Roster and GroupchatControl use the same contact object
This commit is contained in:
Philipp Hörist 2018-04-19 22:11:41 +02:00
parent 70a7000d44
commit 290e761f88
6 changed files with 144 additions and 67 deletions

View File

@ -291,6 +291,8 @@ class ConnectionVcard:
self._vcard_presence_received) self._vcard_presence_received)
app.ged.register_event_handler('gc-presence-received', ged.GUI2, app.ged.register_event_handler('gc-presence-received', ged.GUI2,
self._vcard_gc_presence_received) self._vcard_gc_presence_received)
app.ged.register_event_handler('room-avatar-received', ged.GUI2,
self._vcard_presence_received)
def _vcard_presence_received(self, obj): def _vcard_presence_received(self, obj):
if obj.conn.name != self.name: if obj.conn.name != self.name:
@ -300,6 +302,10 @@ class ConnectionVcard:
# No Avatar is advertised # No Avatar is advertised
return return
room_avatar = False
if isinstance(obj, RoomAvatarReceivedEvent):
room_avatar = True
if self.get_own_jid().bareMatch(obj.jid): if self.get_own_jid().bareMatch(obj.jid):
app.log('avatar').info('Update (vCard): %s %s', app.log('avatar').info('Update (vCard): %s %s',
obj.jid, obj.avatar_sha) obj.jid, obj.avatar_sha)
@ -324,16 +330,33 @@ class ConnectionVcard:
app.log('avatar').debug('Remove: %s', obj.jid) app.log('avatar').debug('Remove: %s', obj.jid)
app.contacts.set_avatar(self.name, obj.jid, None) app.contacts.set_avatar(self.name, obj.jid, None)
own_jid = self.get_own_jid().getStripped() own_jid = self.get_own_jid().getStripped()
if not room_avatar:
app.logger.set_avatar_sha(own_jid, obj.jid, None) app.logger.set_avatar_sha(own_jid, obj.jid, None)
app.interface.update_avatar(self.name, obj.jid) app.interface.update_avatar(
self.name, obj.jid, room_avatar=room_avatar)
else: else:
app.log('avatar').info( app.log('avatar').info(
'Update (vCard): %s %s', obj.jid, obj.avatar_sha) 'Update (vCard): %s %s', obj.jid, obj.avatar_sha)
current_sha = app.contacts.get_avatar_sha(self.name, obj.jid) current_sha = app.contacts.get_avatar_sha(self.name, obj.jid)
if obj.avatar_sha != current_sha: if obj.avatar_sha != current_sha:
if room_avatar:
# We dont save the room avatar hash in our DB, so check
# if we previously downloaded it
if app.interface.avatar_exists(obj.avatar_sha):
app.contacts.set_avatar(self.name, obj.jid, obj.avatar_sha)
app.interface.update_avatar(
self.name, obj.jid, room_avatar=room_avatar)
else:
app.log('avatar').info(
'Request (vCard): %s', obj.jid)
self.request_vcard(self._on_room_avatar_received, obj.jid)
else:
app.log('avatar').info( app.log('avatar').info(
'Request (vCard): %s', obj.jid) 'Request (vCard): %s', obj.jid)
self.request_vcard(self._on_avatar_received, obj.jid) self.request_vcard(self._on_avatar_received, obj.jid)
else: else:
app.log('avatar').info( app.log('avatar').info(
'Avatar already known (vCard): %s %s', 'Avatar already known (vCard): %s %s',
@ -568,6 +591,14 @@ class ConnectionVcard:
self.send_avatar_presence() self.send_avatar_presence()
self.avatar_presence_sent = True self.avatar_presence_sent = True
def _on_room_avatar_received(self, jid, resource, room, vcard):
avatar_sha, photo_decoded = self._get_vcard_photo(vcard, jid)
app.interface.save_avatar(photo_decoded)
app.log('avatar').info('Received (vCard): %s %s', jid, avatar_sha)
app.contacts.set_avatar(self.name, jid, avatar_sha)
app.interface.update_avatar(self.name, jid, room_avatar=True)
def _on_avatar_received(self, jid, resource, room, vcard): def _on_avatar_received(self, jid, resource, room, vcard):
""" """
Called when we receive a vCard Parse the vCard and trigger Events Called when we receive a vCard Parse the vCard and trigger Events

View File

@ -797,6 +797,17 @@ PresenceHelperEvent):
time_str = idle_tag.getAttr('since') time_str = idle_tag.getAttr('since')
tim = helpers.datetime_tuple(time_str) tim = helpers.datetime_tuple(time_str)
self.idle_time = timegm(tim) self.idle_time = timegm(tim)
# Check if presence is from the room itself, used when the room
# sends a avatar hash
contact = app.contacts.get_groupchat_contact(self.conn.name, self.fjid)
if contact:
app.nec.push_incoming_event(
RoomAvatarReceivedEvent(
None, conn=self.conn, stanza=self.stanza,
contact=contact, jid=self.jid))
return
xtags = self.stanza.getTags('x') xtags = self.stanza.getTags('x')
for x in xtags: for x in xtags:
namespace = x.getNamespace() namespace = x.getNamespace()
@ -2229,6 +2240,25 @@ class UpdateRosterAvatarEvent(nec.NetworkIncomingEvent):
def generate(self): def generate(self):
return True return True
class UpdateRoomAvatarEvent(nec.NetworkIncomingEvent):
name = 'update-room-avatar'
base_network_events = []
def generate(self):
return True
class RoomAvatarReceivedEvent(nec.NetworkIncomingEvent):
name = 'room-avatar-received'
base_network_events = []
def generate(self):
vcard = self.stanza.getTag('x', namespace=nbxmpp.NS_VCARD_UPDATE)
if vcard is None:
log.warning('Invalid room self presence:\n%s', self.stanza)
return
self.avatar_sha = vcard.getTagData('photo')
return True
class PEPConfigReceivedEvent(nec.NetworkIncomingEvent): class PEPConfigReceivedEvent(nec.NetworkIncomingEvent):
name = 'pep-config-received' name = 'pep-config-received'
base_network_events = [] base_network_events = []

View File

@ -29,6 +29,7 @@
## ##
try: try:
from gajim.common import app
from gajim.common import caps_cache from gajim.common import caps_cache
from gajim.common.account import Account from gajim.common.account import Account
from gajim import common from gajim import common
@ -92,7 +93,7 @@ class Contact(CommonContact):
""" """
def __init__(self, jid, account, name='', groups=None, show='', status='', def __init__(self, jid, account, name='', groups=None, show='', status='',
sub='', ask='', resource='', priority=0, keyID='', client_caps=None, sub='', ask='', resource='', priority=0, keyID='', client_caps=None,
our_chatstate=None, chatstate=None, idle_time=None, avatar_sha=None): our_chatstate=None, chatstate=None, idle_time=None, avatar_sha=None, groupchat=False):
if not isinstance(jid, str): if not isinstance(jid, str):
print('no str') print('no str')
if groups is None: if groups is None:
@ -104,6 +105,7 @@ class Contact(CommonContact):
self.contact_name = '' # nick choosen by contact self.contact_name = '' # nick choosen by contact
self.groups = [i if i else _('General') for i in set(groups)] # filter duplicate values self.groups = [i if i else _('General') for i in set(groups)] # filter duplicate values
self.avatar_sha = avatar_sha self.avatar_sha = avatar_sha
self._is_groupchat = groupchat
self.sub = sub self.sub = sub
self.ask = ask self.ask = ask
@ -164,10 +166,7 @@ class Contact(CommonContact):
return is_observer return is_observer
def is_groupchat(self): def is_groupchat(self):
for account in common.app.gc_connected: return self._is_groupchat
if self.jid in common.app.gc_connected[account]:
return True
return False
def is_transport(self): def is_transport(self):
# if not '@' or '@' starts the jid then contact is transport # if not '@' or '@' starts the jid then contact is transport
@ -249,7 +248,7 @@ class LegacyContactsAPI:
def create_contact(self, jid, account, name='', groups=None, show='', def create_contact(self, jid, account, name='', groups=None, show='',
status='', sub='', ask='', resource='', priority=0, keyID='', status='', sub='', ask='', resource='', priority=0, keyID='',
client_caps=None, our_chatstate=None, chatstate=None, idle_time=None, client_caps=None, our_chatstate=None, chatstate=None, idle_time=None,
avatar_sha=None): avatar_sha=None, groupchat=False):
if groups is None: if groups is None:
groups = [] groups = []
# Use Account object if available # Use Account object if available
@ -258,7 +257,7 @@ class LegacyContactsAPI:
show=show, status=status, sub=sub, ask=ask, resource=resource, show=show, status=status, sub=sub, ask=ask, resource=resource,
priority=priority, keyID=keyID, client_caps=client_caps, priority=priority, keyID=keyID, client_caps=client_caps,
our_chatstate=our_chatstate, chatstate=chatstate, our_chatstate=our_chatstate, chatstate=chatstate,
idle_time=idle_time, avatar_sha=avatar_sha) idle_time=idle_time, avatar_sha=avatar_sha, groupchat=groupchat)
def create_self_contact(self, jid, account, resource, show, status, priority, def create_self_contact(self, jid, account, resource, show, status, priority,
name='', keyID=''): name='', keyID=''):
@ -304,6 +303,9 @@ class LegacyContactsAPI:
if remove_meta: if remove_meta:
self._metacontact_manager.remove_metacontact(account, jid) self._metacontact_manager.remove_metacontact(account, jid)
def get_groupchat_contact(self, account, jid):
return self._accounts[account].contacts.get_groupchat_contact(jid)
def get_contacts(self, account, jid): def get_contacts(self, account, jid):
return self._accounts[account].contacts.get_contacts(jid) return self._accounts[account].contacts.get_contacts(jid)
@ -518,6 +520,15 @@ class Contacts():
if c.resource == resource: if c.resource == resource:
return c return c
def get_groupchat_contact(self, jid):
if jid in self._contacts:
contacts = self._contacts[jid]
if len(contacts) > 1:
app.log('contacts').warning(
'Groupchat Contact found more than once')
if contacts[0].is_groupchat():
return contacts[0]
def get_avatar(self, jid, size=None, scale=None): def get_avatar(self, jid, size=None, scale=None):
if jid not in self._contacts: if jid not in self._contacts:
return None return None

View File

@ -301,7 +301,7 @@ class GroupchatControl(ChatControlBase):
# will be processed with this command host. # will be processed with this command host.
COMMAND_HOST = GroupChatCommands COMMAND_HOST = GroupChatCommands
def __init__(self, parent_win, contact, acct, is_continued=False): def __init__(self, parent_win, contact, nick, acct, is_continued=False):
ChatControlBase.__init__(self, self.TYPE_ID, parent_win, ChatControlBase.__init__(self, self.TYPE_ID, parent_win,
'groupchat_control', contact, acct) 'groupchat_control', contact, acct)
@ -362,7 +362,7 @@ class GroupchatControl(ChatControlBase):
self.handlers[id_] = widget self.handlers[id_] = widget
self.room_jid = self.contact.jid self.room_jid = self.contact.jid
self.nick = contact.name self.nick = nick
self.new_nick = '' self.new_nick = ''
self.name = '' self.name = ''
for bm in app.connections[self.account].bookmarks: for bm in app.connections[self.account].bookmarks:
@ -371,6 +371,7 @@ class GroupchatControl(ChatControlBase):
break break
if not self.name: if not self.name:
self.name = self.room_jid.split('@')[0] self.name = self.room_jid.split('@')[0]
self.contact.name = self.name
self.widget_set_visible(self.xml.get_object('banner_eventbox'), self.widget_set_visible(self.xml.get_object('banner_eventbox'),
app.config.get('hide_groupchat_banner')) app.config.get('hide_groupchat_banner'))
@ -519,6 +520,8 @@ class GroupchatControl(ChatControlBase):
self._nec_vcard_published) self._nec_vcard_published)
app.ged.register_event_handler('update-gc-avatar', ged.GUI1, app.ged.register_event_handler('update-gc-avatar', ged.GUI1,
self._nec_update_avatar) self._nec_update_avatar)
app.ged.register_event_handler('update-room-avatar', ged.GUI1,
self._nec_update_room_avatar)
app.ged.register_event_handler('gc-subject-received', ged.GUI1, app.ged.register_event_handler('gc-subject-received', ged.GUI1,
self._nec_gc_subject_received) self._nec_gc_subject_received)
app.ged.register_event_handler('gc-config-changed-received', ged.GUI1, app.ged.register_event_handler('gc-config-changed-received', ged.GUI1,
@ -1091,6 +1094,12 @@ class GroupchatControl(ChatControlBase):
banner_status_img = self.xml.get_object('gc_banner_status_image') banner_status_img = self.xml.get_object('gc_banner_status_image')
if self.room_jid in app.gc_connected[self.account] and \ if self.room_jid in app.gc_connected[self.account] and \
app.gc_connected[self.account][self.room_jid]: app.gc_connected[self.account][self.room_jid]:
if self.contact.avatar_sha:
surface = app.interface.get_avatar(self.contact.avatar_sha,
AvatarSize.ROSTER,
self.scale_factor)
banner_status_img.set_from_surface(surface)
return
icon = gtkgui_helpers.get_iconset_name_for('muc-active') icon = gtkgui_helpers.get_iconset_name_for('muc-active')
else: else:
icon = gtkgui_helpers.get_iconset_name_for('muc-inactive') icon = gtkgui_helpers.get_iconset_name_for('muc-inactive')
@ -1147,6 +1156,11 @@ class GroupchatControl(ChatControlBase):
obj.contact.name, obj.contact.avatar_sha) obj.contact.name, obj.contact.avatar_sha)
self.draw_avatar(obj.contact) self.draw_avatar(obj.contact)
def _nec_update_room_avatar(self, obj):
if obj.jid != self.room_jid:
return
self._update_banner_state_image()
def _nec_mam_decrypted_message_received(self, obj): def _nec_mam_decrypted_message_received(self, obj):
if not obj.groupchat: if not obj.groupchat:
return return
@ -2161,8 +2175,8 @@ class GroupchatControl(ChatControlBase):
ctrl.parent_win = None ctrl.parent_win = None
self.send_chatstate('inactive', self.contact) self.send_chatstate('inactive', self.contact)
app.interface.roster.add_groupchat(self.contact.jid, self.account, app.interface.roster.minimize_groupchat(
status = self.subject) self.account, self.contact.jid, status=self.subject)
del win._controls[self.account][self.contact.jid] del win._controls[self.account][self.contact.jid]
@ -2233,6 +2247,8 @@ class GroupchatControl(ChatControlBase):
self._nec_vcard_published) self._nec_vcard_published)
app.ged.remove_event_handler('update-gc-avatar', ged.GUI1, app.ged.remove_event_handler('update-gc-avatar', ged.GUI1,
self._nec_update_avatar) self._nec_update_avatar)
app.ged.remove_event_handler('update-room-avatar', ged.GUI1,
self._nec_update_room_avatar)
app.ged.remove_event_handler('gc-subject-received', ged.GUI1, app.ged.remove_event_handler('gc-subject-received', ged.GUI1,
self._nec_gc_subject_received) self._nec_gc_subject_received)
app.ged.remove_event_handler('gc-config-changed-received', ged.GUI1, app.ged.remove_event_handler('gc-config-changed-received', ged.GUI1,

View File

@ -92,7 +92,8 @@ from gajim.common import passwords
from gajim.common import logging_helpers from gajim.common import logging_helpers
from gajim.common.connection_handlers_events import ( from gajim.common.connection_handlers_events import (
OurShowEvent, FileRequestErrorEvent, FileTransferCompletedEvent, OurShowEvent, FileRequestErrorEvent, FileTransferCompletedEvent,
UpdateRosterAvatarEvent, UpdateGCAvatarEvent, HTTPUploadProgressEvent) UpdateRosterAvatarEvent, UpdateGCAvatarEvent, UpdateRoomAvatarEvent,
HTTPUploadProgressEvent)
from gajim.common.connection import Connection from gajim.common.connection import Connection
from gajim.common.file_props import FilesProp from gajim.common.file_props import FilesProp
from gajim.common import pep from gajim.common import pep
@ -2071,8 +2072,10 @@ class Interface:
if minimize: if minimize:
# GCMIN # GCMIN
contact = app.contacts.create_contact(jid=room_jid, contact = app.contacts.create_contact(jid=room_jid,
account=account, name=nick) account=account, groups=[_('Groupchats')], sub='none',
gc_control = GroupchatControl(None, contact, account) groupchat=True)
app.contacts.add_contact(account, contact)
gc_control = GroupchatControl(None, contact, nick, account)
app.interface.minimized_controls[account][room_jid] = \ app.interface.minimized_controls[account][room_jid] = \
gc_control gc_control
self.roster.add_groupchat(room_jid, account) self.roster.add_groupchat(room_jid, account)
@ -2094,12 +2097,13 @@ class Interface:
# Get target window, create a control, and associate it with the window # Get target window, create a control, and associate it with the window
# GCMIN # GCMIN
contact = app.contacts.create_contact(jid=room_jid, account=account, contact = app.contacts.create_contact(jid=room_jid, account=account,
name=nick) groups=[_('Groupchats')], sub='none', groupchat=True)
app.contacts.add_contact(account, contact)
mw = self.msg_win_mgr.get_window(contact.jid, account) mw = self.msg_win_mgr.get_window(contact.jid, account)
if not mw: if not mw:
mw = self.msg_win_mgr.create_window(contact, account, mw = self.msg_win_mgr.create_window(contact, account,
GroupchatControl.TYPE_ID) GroupchatControl.TYPE_ID)
gc_control = GroupchatControl(mw, contact, account, gc_control = GroupchatControl(mw, contact, nick, account,
is_continued=is_continued) is_continued=is_continued)
mw.new_tab(gc_control) mw.new_tab(gc_control)
mw.set_active_tab(gc_control) mw.set_active_tab(gc_control)
@ -2421,8 +2425,11 @@ class Interface:
sys.exit() sys.exit()
@staticmethod @staticmethod
def update_avatar(account=None, jid=None, contact=None): def update_avatar(account=None, jid=None, contact=None, room_avatar=False):
if contact is None: if room_avatar:
app.nec.push_incoming_event(
UpdateRoomAvatarEvent(None, account=account, jid=jid))
elif contact is None:
app.nec.push_incoming_event( app.nec.push_incoming_event(
UpdateRosterAvatarEvent(None, account=account, jid=jid)) UpdateRosterAvatarEvent(None, account=account, jid=jid))
else: else:
@ -2524,6 +2531,13 @@ class Interface:
return pixbuf return pixbuf
return Gdk.cairo_surface_create_from_pixbuf(pixbuf, scale) return Gdk.cairo_surface_create_from_pixbuf(pixbuf, scale)
@staticmethod
def avatar_exists(filename):
path = os.path.join(app.AVATAR_PATH, filename)
if not os.path.isfile(path):
return False
return True
def auto_join_bookmarks(self, account): def auto_join_bookmarks(self, account):
""" """
Autojoin bookmarked GCs that have 'auto join' on for this account Autojoin bookmarked GCs that have 'auto join' on for this account
@ -2538,12 +2552,6 @@ class Interface:
minimize = bm['minimize'] in ('1', 'true') minimize = bm['minimize'] in ('1', 'true')
self.join_gc_room(account, jid, bm['nick'], self.join_gc_room(account, jid, bm['nick'],
bm['password'], minimize = minimize) bm['password'], minimize = minimize)
elif jid in self.minimized_controls[account]:
# more or less a hack:
# On disconnect the minimized gc contact instances
# were set to offline. Reconnect them to show up in the
# roster.
self.roster.add_groupchat(jid, account)
def add_gc_bookmark(self, account, name, jid, autojoin, minimize, password, def add_gc_bookmark(self, account, name, jid, autojoin, minimize, password,
nick): nick):

View File

@ -743,7 +743,7 @@ class RosterWindow:
return contacts[0][0] # it's contact/big brother with highest priority return contacts[0][0] # it's contact/big brother with highest priority
def remove_contact(self, jid, account, force=False, backend=False): def remove_contact(self, jid, account, force=False, backend=False, maximize=False):
""" """
Remove contact from roster Remove contact from roster
@ -785,6 +785,8 @@ class RosterWindow:
# If a window is still opened: don't remove contact instance # If a window is still opened: don't remove contact instance
# Remove contact before redrawing, otherwise the old # Remove contact before redrawing, otherwise the old
# numbers will still be show # numbers will still be show
if not maximize:
# Dont remove contact when we maximize a room
app.contacts.remove_jid(account, jid, remove_meta=True) app.contacts.remove_jid(account, jid, remove_meta=True)
if iters: if iters:
rest_of_family = [data for data in family rest_of_family = [data for data in family
@ -829,49 +831,26 @@ class RosterWindow:
self.model[self_iter][Column.JID] = new_jid self.model[self_iter][Column.JID] = new_jid
self.draw_contact(new_jid, account) self.draw_contact(new_jid, account)
def add_groupchat(self, jid, account, status=''): def minimize_groupchat(self, account, jid, status=''):
gc_control = app.interface.msg_win_mgr.get_gc_control(jid, account)
app.interface.minimized_controls[account][jid] = gc_control
self.add_groupchat(jid, account)
def add_groupchat(self, jid, account):
""" """
Add groupchat to roster and draw it. Return the added contact instance Add groupchat to roster and draw it. Return the added contact instance
""" """
contact = app.contacts.get_contact_with_highest_priority(account, jid) contact = app.contacts.get_groupchat_contact(account, jid)
# Do not show gc if we are disconnected and minimize it show = 'offline'
if app.account_is_connected(account): if app.account_is_connected(account):
show = 'online' show = 'online'
else:
show = 'offline'
status = ''
if contact is None:
gc_control = app.interface.msg_win_mgr.get_gc_control(jid,
account)
if gc_control:
# there is a window that we can minimize
app.interface.minimized_controls[account][jid] = gc_control
name = gc_control.name
elif jid in app.interface.minimized_controls[account]:
name = app.interface.minimized_controls[account][jid].name
else:
name = jid.split('@')[0]
# New groupchat
contact = app.contacts.create_contact(jid=jid, account=account,
name=name, groups=[_('Groupchats')], show=show, status=status,
sub='none')
app.contacts.add_contact(account, contact)
self.add_contact(jid, account)
else:
if jid not in app.interface.minimized_controls[account]:
# there is a window that we can minimize
gc_control = app.interface.msg_win_mgr.get_gc_control(jid,
account)
app.interface.minimized_controls[account][jid] = gc_control
contact.show = show contact.show = show
contact.status = status self.add_contact(jid, account)
self.adjust_and_draw_contact_context(jid, account)
return contact return contact
def remove_groupchat(self, jid, account, maximize=False):
def remove_groupchat(self, jid, account):
""" """
Remove groupchat from roster and redraw account and group Remove groupchat from roster and redraw account and group
""" """
@ -879,7 +858,7 @@ class RosterWindow:
if contact.is_groupchat(): if contact.is_groupchat():
if jid in app.interface.minimized_controls[account]: if jid in app.interface.minimized_controls[account]:
del app.interface.minimized_controls[account][jid] del app.interface.minimized_controls[account][jid]
self.remove_contact(jid, account, force=True, backend=True) self.remove_contact(jid, account, force=True, backend=True, maximize=maximize)
return True return True
else: else:
return False return False
@ -3137,7 +3116,7 @@ class RosterWindow:
ctrl.on_groupchat_maximize() ctrl.on_groupchat_maximize()
mw.new_tab(ctrl) mw.new_tab(ctrl)
mw.set_active_tab(ctrl) mw.set_active_tab(ctrl)
self.remove_groupchat(jid, account) self.remove_groupchat(jid, account, maximize=True)
def on_edit_account(self, widget, account): def on_edit_account(self, widget, account):
if 'accounts' in app.interface.instances: if 'accounts' in app.interface.instances:
@ -5912,6 +5891,8 @@ class RosterWindow:
self._nec_pep_received) self._nec_pep_received)
app.ged.register_event_handler('update-roster-avatar', ged.GUI1, app.ged.register_event_handler('update-roster-avatar', ged.GUI1,
self._nec_update_avatar) self._nec_update_avatar)
app.ged.register_event_handler('update-room-avatar', ged.GUI1,
self._nec_update_avatar)
app.ged.register_event_handler('gc-subject-received', ged.GUI1, app.ged.register_event_handler('gc-subject-received', ged.GUI1,
self._nec_gc_subject_received) self._nec_gc_subject_received)
app.ged.register_event_handler('metacontacts-received', ged.GUI2, app.ged.register_event_handler('metacontacts-received', ged.GUI2,