Refactor MUC module

- nbxmpp provides now most of the MUC code
This commit is contained in:
Philipp Hörist 2018-12-25 20:30:36 +01:00
parent c63e32634a
commit 8094cadbea
14 changed files with 278 additions and 527 deletions

View file

@ -1482,12 +1482,12 @@ class ChatControl(ChatControlBase):
self._add_info_bar_message(markup, [b], file_props, Gtk.MessageType.ERROR) self._add_info_bar_message(markup, [b], file_props, Gtk.MessageType.ERROR)
def _on_accept_gc_invitation(self, widget, event): def _on_accept_gc_invitation(self, widget, event):
if event.is_continued: if event.continued:
app.interface.join_gc_room(self.account, event.room_jid, app.interface.join_gc_room(self.account, str(event.muc),
app.nicks[self.account], event.password, app.nicks[self.account], event.password,
is_continued=True) is_continued=True)
else: else:
app.interface.join_gc_minimal(self.account, event.room_jid) app.interface.join_gc_minimal(self.account, str(event.muc))
app.events.remove_events(self.account, self.contact.jid, event=event) app.events.remove_events(self.account, self.contact.jid, event=event)
@ -1495,14 +1495,14 @@ class ChatControl(ChatControlBase):
app.events.remove_events(self.account, self.contact.jid, event=event) app.events.remove_events(self.account, self.contact.jid, event=event)
def _get_gc_invitation(self, event): def _get_gc_invitation(self, event):
markup = '<b>%s:</b> %s' % (_('Groupchat Invitation'), event.room_jid) markup = '<b>%s:</b> %s' % (_('Groupchat Invitation'), event.muc)
if event.reason: if event.reason:
markup += ' (%s)' % event.reason markup += ' (%s)' % event.reason
b1 = Gtk.Button.new_with_mnemonic(_('_Join')) b1 = Gtk.Button.new_with_mnemonic(_('_Join'))
b1.connect('clicked', self._on_accept_gc_invitation, event) b1.connect('clicked', self._on_accept_gc_invitation, event)
b2 = Gtk.Button(stock=Gtk.STOCK_CANCEL) b2 = Gtk.Button(stock=Gtk.STOCK_CANCEL)
b2.connect('clicked', self._on_cancel_gc_invitation, event) b2.connect('clicked', self._on_cancel_gc_invitation, event)
self._add_info_bar_message(markup, [b1, b2], (event.room_jid, self._add_info_bar_message(markup, [b1, b2], (event.muc,
event.reason), Gtk.MessageType.QUESTION) event.reason), Gtk.MessageType.QUESTION)
def on_event_added(self, event): def on_event_added(self, event):
@ -1546,7 +1546,7 @@ class ChatControl(ChatControlBase):
removed = False removed = False
for ib_msg in self.info_bar_queue: for ib_msg in self.info_bar_queue:
if ev.type_ == 'gc-invitation': if ev.type_ == 'gc-invitation':
if ev.room_jid == ib_msg[2][0]: if ev.muc == ib_msg[2][0]:
self.info_bar_queue.remove(ib_msg) self.info_bar_queue.remove(ib_msg)
removed = True removed = True
else: # file-* else: # file-*

View file

@ -480,6 +480,7 @@ def zeroconf_is_connected():
config.get_per('accounts', ZEROCONF_ACC_NAME, 'is_zeroconf') config.get_per('accounts', ZEROCONF_ACC_NAME, 'is_zeroconf')
def in_groupchat(account, room_jid): def in_groupchat(account, room_jid):
room_jid = str(room_jid)
if room_jid not in gc_connected[account]: if room_jid not in gc_connected[account]:
return False return False
return gc_connected[account][room_jid] return gc_connected[account][room_jid]

View file

@ -307,12 +307,12 @@ class LegacyContactsAPI:
return self_contact return self_contact
def create_not_in_roster_contact(self, jid, account, resource='', name='', def create_not_in_roster_contact(self, jid, account, resource='', name='',
keyID=''): keyID='', groupchat=False):
# Use Account object if available # Use Account object if available
account = self._accounts.get(account, account) account = self._accounts.get(account, account)
return self.create_contact(jid=jid, account=account, resource=resource, return self.create_contact(jid=jid, account=account, resource=resource,
name=name, groups=[_('Not in Roster')], show='not in roster', name=name, groups=[_('Not in Roster')], show='not in roster',
status='', sub='none', keyID=keyID) status='', sub='none', keyID=keyID, groupchat=groupchat)
def copy_contact(self, contact): def copy_contact(self, contact):
return self.create_contact(contact.jid, contact.account, return self.create_contact(contact.jid, contact.account,

View file

@ -130,15 +130,10 @@ class UnsubscribedEvent(Event):
class GcInvitationtEvent(Event): class GcInvitationtEvent(Event):
type_ = 'gc-invitation' type_ = 'gc-invitation'
def __init__(self, room_jid, reason, password, is_continued, jid_from, def __init__(self, event):
time_=None, show_in_roster=False, show_in_systray=True): Event.__init__(self, None, show_in_roster=False, show_in_systray=True)
Event.__init__(self, time_, show_in_roster=show_in_roster, for key, value in vars(event).items():
show_in_systray=show_in_systray) setattr(self, key, value)
self.room_jid = room_jid
self.reason = reason
self.password = password
self.is_continued = is_continued
self.jid_from = jid_from
class FileRequestEvent(Event): class FileRequestEvent(Event):
type_ = 'file-request' type_ = 'file-request'

View file

@ -1462,6 +1462,15 @@ def load_json(path, key=None, default=None):
return json_dict return json_dict
return json_dict.get(key, default) return json_dict.get(key, default)
def ignore_contact(account, jid):
jid = str(jid)
known_contact = app.contacts.get_contacts(account, jid)
ignore = app.config.get_per('accounts', account, 'ignore_unknown_contacts')
if ignore and not known_contact:
log.info('Ignore unknown contact %s', jid)
return True
return False
class AdditionalDataDict(collections.UserDict): class AdditionalDataDict(collections.UserDict):
def __init__(self, initialdata=None): def __init__(self, initialdata=None):
collections.UserDict.__init__(self, initialdata) collections.UserDict.__init__(self, initialdata)

View file

@ -170,5 +170,29 @@ def parse_bob_data(stanza):
return filepath return filepath
def store_bob_data(bob_data):
if bob_data is None:
return
algo_hash = '%s+%s' % (bob_data.algo, bob_data.hash_)
filepath = Path(configpaths.get('BOB')) / algo_hash
if algo_hash in app.bob_cache or filepath.exists():
log.info('BoB data already cached')
return
if bob_data.max_age == 0:
app.bob_cache[algo_hash] = bob_data.data
else:
try:
with open(str(filepath), 'w+b') as file:
file.write(bob_data.data)
except Exception:
log.exception('Unable to save data')
return
log.info('BoB data stored: %s', algo_hash)
return filepath
def get_instance(*args, **kwargs): def get_instance(*args, **kwargs):
return BitsOfBinary(*args, **kwargs), 'BitsOfBinary' return BitsOfBinary(*args, **kwargs), 'BitsOfBinary'

View file

@ -212,31 +212,14 @@ class Message:
subject = event.stanza.getSubject() subject = event.stanza.getSubject()
groupchat = event.mtype == 'groupchat' groupchat = event.mtype == 'groupchat'
# XEP-0045: only a message that contains a <subject/> but no <body/>
# element shall be considered a subject change for MUC purposes.
muc_subject = subject and groupchat and not event.msgtxt
# Determine timestamps # Determine timestamps
if groupchat: delay_jid = self._con.get_own_jid().getDomain()
delay_entity_jid = event.jid timestamp = parse_delay(event.stanza, from_=delay_jid)
else: if timestamp is None:
delay_entity_jid = self._con.get_own_jid().getDomain()
if muc_subject:
# MUC Subjects can have a delay timestamp
# to indicate when the user has set the subject,
# the 'from' attr on these delays is the MUC server
# but we treat it as user timestamp
timestamp = time.time() timestamp = time.time()
user_timestamp = parse_delay(event.stanza, from_=delay_entity_jid)
else: user_timestamp = parse_delay(event.stanza,
timestamp = parse_delay(event.stanza, from_=delay_entity_jid) not_from=[delay_jid])
if timestamp is None:
timestamp = time.time()
user_timestamp = parse_delay(event.stanza,
not_from=[delay_entity_jid])
if user_timestamp is not None: if user_timestamp is not None:
event.additional_data.set_value( event.additional_data.set_value(
@ -271,11 +254,6 @@ class Message:
event.session, event.fjid, timestamp) event.session, event.fjid, timestamp)
return return
if muc_subject:
app.nec.push_incoming_event(NetworkEvent('gc-subject-received',
**vars(event)))
return
if groupchat: if groupchat:
if not event.msgtxt: if not event.msgtxt:
return return

View file

@ -17,18 +17,17 @@
import time import time
import logging import logging
import weakref
import nbxmpp import nbxmpp
from nbxmpp.const import InviteType
from nbxmpp.structs import StanzaHandler
from gajim.common import i18n from gajim.common import i18n
from gajim.common import app from gajim.common import app
from gajim.common import helpers from gajim.common import helpers
from gajim.common.caps_cache import muc_caps_cache from gajim.common.caps_cache import muc_caps_cache
from gajim.common.nec import NetworkEvent from gajim.common.nec import NetworkEvent
from gajim.common.nec import NetworkIncomingEvent from gajim.common.modules.bits_of_binary import store_bob_data
from gajim.common.modules import dataforms
from gajim.common.modules.bits_of_binary import parse_bob_data
log = logging.getLogger('gajim.c.m.muc') log = logging.getLogger('gajim.c.m.muc')
@ -39,13 +38,55 @@ class MUC:
self._account = con.name self._account = con.name
self.handlers = [ self.handlers = [
('message', self._on_config_change, '', nbxmpp.NS_MUC_USER), StanzaHandler(name='message',
('message', self._mediated_invite, 'normal', nbxmpp.NS_MUC_USER), callback=self._on_subject_change,
('message', self._direct_invite, '', nbxmpp.NS_CONFERENCE), typ='groupchat',
('message', self._on_captcha_challenge, '', nbxmpp.NS_CAPTCHA), priority=49),
('message', self._on_voice_request, '', nbxmpp.NS_DATA, 45), StanzaHandler(name='message',
callback=self._on_config_change,
ns=nbxmpp.NS_MUC_USER,
priority=49),
StanzaHandler(name='message',
callback=self._on_invite_or_decline,
typ='normal',
ns=nbxmpp.NS_MUC_USER,
priority=49),
StanzaHandler(name='message',
callback=self._on_invite_or_decline,
ns=nbxmpp.NS_CONFERENCE,
priority=49),
StanzaHandler(name='message',
callback=self._on_captcha_challenge,
ns=nbxmpp.NS_CAPTCHA,
priority=49),
StanzaHandler(name='message',
callback=self._on_voice_request,
ns=nbxmpp.NS_DATA,
priority=49)
] ]
self._nbmxpp_methods = [
'get_affiliation',
'set_role',
'set_affiliation',
'set_config',
'set_subject',
'cancel_config',
'send_captcha',
'decline',
'request_voice'
'destroy',
]
def __getattr__(self, key):
if key in self._nbmxpp_methods:
if not app.account_is_connected(self._account):
log.warning('Account %s not connected, cant use %s',
self._account, key)
return
module = self._con.connection.get_module('MUC')
return getattr(module, key)
def pass_disco(self, from_, identities, features, _data, _node): def pass_disco(self, from_, identities, features, _data, _node):
for identity in identities: for identity in identities:
if identity.get('category') != 'conference': if identity.get('category') != 'conference':
@ -108,438 +149,136 @@ class MUC:
if tags: if tags:
muc_x.setTag('history', tags) muc_x.setTag('history', tags)
def set_subject(self, room_jid, subject): def _on_subject_change(self, _con, _stanza, properties):
if not app.account_is_connected(self._account): if not properties.is_muc_subject:
return
message = nbxmpp.Message(room_jid, typ='groupchat', subject=subject)
log.info('Set subject for %s', room_jid)
self._con.connection.send(message)
def _on_voice_request(self, _con, stanza):
data_form = stanza.getTag('x', namespace=nbxmpp.NS_DATA)
if data_form is None:
return return
if stanza.getBody(): jid = properties.jid.getBare()
return contact = app.contacts.get_groupchat_contact(self._account, jid)
room_jid = str(stanza.getFrom())
contact = app.contacts.get_groupchat_contact(self._account, room_jid)
if contact is None: if contact is None:
return return
data_form = dataforms.extend_form(data_form) contact.status = properties.subject
try:
if data_form['FORM_TYPE'].value != nbxmpp.NS_MUC_REQUEST: app.nec.push_incoming_event(
return NetworkEvent('gc-subject-received',
except KeyError: account=self._account,
jid=jid,
subject=properties.subject,
nickname=properties.muc_nickname,
user_timestamp=properties.user_timestamp))
raise nbxmpp.NodeProcessed
def _on_voice_request(self, _con, _stanza, properties):
if not properties.is_voice_request:
return
jid = str(properties.jid)
contact = app.contacts.get_groupchat_contact(self._account, jid)
if contact is None:
return return
app.nec.push_incoming_event( app.nec.push_incoming_event(
NetworkEvent('voice-approval', NetworkEvent('voice-approval',
account=self._account, account=self._account,
room_jid=room_jid, jid=jid,
form=data_form)) form=properties.voice_request.form))
raise nbxmpp.NodeProcessed raise nbxmpp.NodeProcessed
def _on_captcha_challenge(self, _con, stanza): def _on_captcha_challenge(self, _con, _stanza, properties):
captcha = stanza.getTag('captcha', namespace=nbxmpp.NS_CAPTCHA) if not properties.is_captcha_challenge:
if captcha is None:
return return
parse_bob_data(stanza) contact = app.contacts.get_groupchat_contact(self._account,
room_jid = str(stanza.getFrom()) str(properties.jid))
contact = app.contacts.get_groupchat_contact(self._account, room_jid)
if contact is None: if contact is None:
return return
log.info('Captcha challenge received from %s', room_jid) log.info('Captcha challenge received from %s', properties.jid)
data_form = captcha.getTag('x', namespace=nbxmpp.NS_DATA) store_bob_data(properties.captcha.bob_data)
data_form = dataforms.extend_form(node=data_form)
app.nec.push_incoming_event( app.nec.push_incoming_event(
NetworkEvent('captcha-challenge', NetworkEvent('captcha-challenge',
account=self._account, account=self._account,
room_jid=room_jid, jid=properties.jid,
form=data_form)) form=properties.captcha.form))
raise nbxmpp.NodeProcessed raise nbxmpp.NodeProcessed
def send_captcha(self, room_jid, form_node): def _on_config_change(self, _con, _stanza, properties):
if not app.account_is_connected(self._account): if not properties.is_muc_config_change:
return
iq = nbxmpp.Iq(typ='set', to=room_jid)
captcha = iq.addChild(name='captcha', namespace=nbxmpp.NS_CAPTCHA)
captcha.addChild(node=form_node)
self._con.connection.send(iq)
def request_config(self, room_jid):
if not app.account_is_connected(self._account):
return
iq = nbxmpp.Iq(typ='get',
queryNS=nbxmpp.NS_MUC_OWNER,
to=room_jid)
iq.setAttr('xml:lang', i18n.LANG)
log.info('Request config for %s', room_jid)
self._con.connection.SendAndCallForResponse(
iq, self._config_received)
def _config_received(self, stanza):
if not nbxmpp.isResultNode(stanza):
log.info('Error: %s', stanza.getError())
return return
room_jid = stanza.getFrom().getStripped() log.info('Received config change: %s %s',
payload = stanza.getQueryPayload() properties.jid, properties.muc_status_codes)
for form in payload:
if form.getNamespace() == nbxmpp.NS_DATA:
dataform = dataforms.extend_form(node=form)
log.info('Config form received for %s', room_jid)
app.nec.push_incoming_event(MucOwnerReceivedEvent(
None,
conn=self._con,
form_node=form,
dataform=dataform,
jid=room_jid))
break
def cancel_config(self, room_jid):
if not app.account_is_connected(self._account):
return
cancel = nbxmpp.Node(tag='x', attrs={'xmlns': nbxmpp.NS_DATA,
'type': 'cancel'})
iq = nbxmpp.Iq(typ='set',
queryNS=nbxmpp.NS_MUC_OWNER,
payload=cancel,
to=room_jid)
log.info('Cancel config for %s', room_jid)
self._con.connection.SendAndCallForResponse(
iq, self._default_response, {})
def _on_config_change(self, _con, stanza):
muc_user = stanza.getTag('x', namespace=nbxmpp.NS_MUC_USER)
if muc_user is None:
return
if stanza.getBody():
return
room_list = app.contacts.get_gc_list(self._account)
room_jid = str(stanza.getFrom())
if room_jid not in room_list:
# Message not from a group chat
return
# https://xmpp.org/extensions/xep-0045.html#registrar-statuscodes
change_codes = ['100', '102', '103', '104',
'170', '171', '172', '173', '174']
codes = set()
for status in muc_user.getTags('status'):
code = status.getAttr('code')
if code in change_codes:
codes.add(code)
if not codes:
return
log.info('Received config change: %s', codes)
app.nec.push_incoming_event( app.nec.push_incoming_event(
NetworkEvent('gc-config-changed-received', NetworkEvent('gc-config-changed-received',
account=self._account, account=self._account,
room_jid=room_jid, jid=properties.jid,
status_codes=codes)) status_codes=properties.muc_status_codes))
raise nbxmpp.NodeProcessed raise nbxmpp.NodeProcessed
def destroy(self, room_jid, reason='', jid=''): def _on_invite_or_decline(self, _con, _stanza, properties):
if not app.account_is_connected(self._account): if properties.muc_decline is not None:
return data = properties.muc_decline
iq = nbxmpp.Iq(typ='set', if helpers.ignore_contact(self._account, data.from_):
queryNS=nbxmpp.NS_MUC_OWNER, raise nbxmpp.NodeProcessed
to=room_jid)
destroy = iq.setQuery().setTag('destroy')
if reason:
destroy.setTagData('reason', reason)
if jid:
destroy.setAttr('jid', jid)
log.info('Destroy room: %s, reason: %s, alternate: %s',
room_jid, reason, jid)
self._con.connection.SendAndCallForResponse(
iq, self._default_response, {})
def set_config(self, room_jid, form): log.info('Invite declined from: %s, reason: %s',
if not app.account_is_connected(self._account): data.from_, data.reason)
return
iq = nbxmpp.Iq(typ='set', to=room_jid, queryNS=nbxmpp.NS_MUC_OWNER)
query = iq.setQuery()
form.setAttr('type', 'submit')
query.addChild(node=form)
log.info('Set config for %s', room_jid)
self._con.connection.SendAndCallForResponse(
iq, self._default_response, {})
def set_affiliation(self, room_jid, users_dict):
if not app.account_is_connected(self._account):
return
iq = nbxmpp.Iq(typ='set', to=room_jid, queryNS=nbxmpp.NS_MUC_ADMIN)
item = iq.setQuery()
for jid in users_dict:
affiliation = users_dict[jid].get('affiliation')
reason = users_dict[jid].get('reason')
nick = users_dict[jid].get('nick')
item_tag = item.addChild('item', {'jid': jid,
'affiliation': affiliation})
if reason is not None:
item_tag.setTagData('reason', reason)
if nick is not None:
item_tag.setAttr('nick', nick)
log.info('Set affiliation for %s: %s', room_jid, users_dict)
self._con.connection.SendAndCallForResponse(
iq, self._default_response, {})
def get_affiliation(self, room_jid, affiliation, success_cb, error_cb):
if not app.account_is_connected(self._account):
return
iq = nbxmpp.Iq(typ='get', to=room_jid, queryNS=nbxmpp.NS_MUC_ADMIN)
item = iq.setQuery().setTag('item')
item.setAttr('affiliation', affiliation)
log.info('Get affiliation %s for %s', affiliation, room_jid)
weak_success_cb = weakref.WeakMethod(success_cb)
weak_error_cb = weakref.WeakMethod(error_cb)
self._con.connection.SendAndCallForResponse(
iq, self._affiliation_received, {'affiliation': affiliation,
'success_cb': weak_success_cb,
'error_cb': weak_error_cb})
def _affiliation_received(self, _con, stanza, affiliation,
success_cb, error_cb):
if not nbxmpp.isResultNode(stanza):
if error_cb() is not None:
error_cb()(affiliation, stanza.getError())
return
room_jid = stanza.getFrom().getStripped()
query = stanza.getTag('query', namespace=nbxmpp.NS_MUC_ADMIN)
items = query.getTags('item')
users_dict = {}
for item in items:
try:
jid = helpers.parse_jid(item.getAttr('jid'))
except helpers.InvalidFormat:
log.warning('Invalid JID: %s, ignoring it',
item.getAttr('jid'))
continue
users_dict[jid] = {}
if item.has_attr('nick'):
users_dict[jid]['nick'] = item.getAttr('nick')
if item.has_attr('role'):
users_dict[jid]['role'] = item.getAttr('role')
reason = item.getTagData('reason')
if reason:
users_dict[jid]['reason'] = reason
log.info('%s affiliations received from %s: %s',
affiliation, room_jid, users_dict)
if success_cb() is not None:
success_cb()(self._account, room_jid, affiliation, users_dict)
def set_role(self, room_jid, nick, role, reason=''):
if not app.account_is_connected(self._account):
return
iq = nbxmpp.Iq(typ='set', to=room_jid, queryNS=nbxmpp.NS_MUC_ADMIN)
item = iq.setQuery().setTag('item')
item.setAttr('nick', nick)
item.setAttr('role', role)
if reason:
item.addChild(name='reason', payload=reason)
log.info('Set role for %s: %s %s %s', room_jid, nick, role, reason)
self._con.connection.SendAndCallForResponse(
iq, self._default_response, {})
def _mediated_invite(self, _con, stanza):
muc_user = stanza.getTag('x', namespace=nbxmpp.NS_MUC_USER)
if muc_user is None:
return
if stanza.getType() == 'error':
return
if stanza.getBody():
return
decline = muc_user.getTag('decline')
if decline is not None:
room_jid = stanza.getFrom().getStripped()
from_ = self._get_from(room_jid, decline)
reason = decline.getTagData('reason')
log.info('Invite declined: %s, %s', reason, from_)
app.nec.push_incoming_event( app.nec.push_incoming_event(
GcDeclineReceived(None, NetworkEvent('gc-decline-received',
account=self._account, account=self._account,
from_=from_, **data._asdict()))
room_jid=room_jid,
reason=reason))
raise nbxmpp.NodeProcessed raise nbxmpp.NodeProcessed
invite = muc_user.getTag('invite') if properties.muc_invite is not None:
if invite is not None: data = properties.muc_invite
if helpers.ignore_contact(self._account, data.from_):
raise nbxmpp.NodeProcessed
room_jid = stanza.getFrom().getStripped() log.info('Invite from: %s, to: %s', data.from_, data.muc)
from_ = self._get_from(room_jid, invite)
reason = invite.getTagData('reason') if app.in_groupchat(self._account, data.muc):
password = muc_user.getTagData('password')
is_continued = invite.getTag('continue') is not None
log.info('Mediated invite: continued: %s, reason: %s, from: %s',
is_continued, reason, from_)
if room_jid in app.gc_connected[self._account] and \
app.gc_connected[self._account][room_jid]:
# We are already in groupchat. Ignore invitation # We are already in groupchat. Ignore invitation
log.info('We are already in this room') log.info('We are already in this room')
raise nbxmpp.NodeProcessed raise nbxmpp.NodeProcessed
app.nec.push_incoming_event( app.nec.push_incoming_event(
GcInvitationReceived(None, NetworkEvent('gc-invitation-received',
account=self._account, account=self._account,
from_=from_, **data._asdict()))
room_jid=room_jid,
reason=reason,
password=password,
is_continued=is_continued))
raise nbxmpp.NodeProcessed raise nbxmpp.NodeProcessed
def _get_from(self, room_jid, stanza):
try:
from_ = nbxmpp.JID(helpers.parse_jid(stanza.getAttr('from')))
except helpers.InvalidFormat:
log.warning('Invalid JID on invite: %s, ignoring it',
stanza.getAttr('from'))
raise nbxmpp.NodeProcessed
known_contact = app.contacts.get_contacts(self._account, room_jid)
ignore = app.config.get_per(
'accounts', self._account, 'ignore_unknown_contacts')
if ignore and not known_contact:
log.info('Ignore invite from unknown contact %s', from_)
raise nbxmpp.NodeProcessed
return from_
def _direct_invite(self, _con, stanza):
direct = stanza.getTag('x', namespace=nbxmpp.NS_CONFERENCE)
if direct is None:
return
from_ = stanza.getFrom()
try:
room_jid = helpers.parse_jid(direct.getAttr('jid'))
except helpers.InvalidFormat:
log.warning('Invalid JID on invite: %s, ignoring it',
direct.getAttr('jid'))
raise nbxmpp.NodeProcessed
reason = direct.getAttr('reason')
password = direct.getAttr('password')
is_continued = direct.getAttr('continue') == 'true'
log.info('Direct invite: continued: %s, reason: %s, from: %s',
is_continued, reason, from_)
app.nec.push_incoming_event(
GcInvitationReceived(None,
account=self._account,
from_=from_,
room_jid=room_jid,
reason=reason,
password=password,
is_continued=is_continued))
raise nbxmpp.NodeProcessed
def invite(self, room, to, reason=None, continue_=False): def invite(self, room, to, reason=None, continue_=False):
if not app.account_is_connected(self._account): if not app.account_is_connected(self._account):
return return
type_ = InviteType.MEDIATED
contact = app.contacts.get_contact_from_full_jid(self._account, to) contact = app.contacts.get_contact_from_full_jid(self._account, to)
if contact and contact.supports(nbxmpp.NS_CONFERENCE): if contact and contact.supports(nbxmpp.NS_CONFERENCE):
invite = self._build_direct_invite(room, to, reason, continue_) type_ = InviteType.DIRECT
else:
invite = self._build_mediated_invite(room, to, reason, continue_)
self._con.connection.send(invite)
@staticmethod
def _build_direct_invite(room, to, reason, continue_):
message = nbxmpp.Message(to=to)
attrs = {'jid': room}
if reason:
attrs['reason'] = reason
if continue_:
attrs['continue'] = 'true'
password = app.gc_passwords.get(room, None) password = app.gc_passwords.get(room, None)
if password: self._con.connection.get_module('MUC').invite(
attrs['password'] = password room, to, reason, password, continue_, type_)
message.addChild(name='x', attrs=attrs,
namespace=nbxmpp.NS_CONFERENCE)
return message
@staticmethod def request_config(self, room_jid):
def _build_mediated_invite(room, to, reason, continue_):
message = nbxmpp.Message(to=room)
muc_user = message.addChild('x', namespace=nbxmpp.NS_MUC_USER)
invite = muc_user.addChild('invite', attrs={'to': to})
if continue_:
invite.addChild(name='continue')
if reason:
invite.setTagData('reason', reason)
password = app.gc_passwords.get(room, None)
if password:
muc_user.setTagData('password', password)
return message
def decline(self, room, to, reason=None):
if not app.account_is_connected(self._account): if not app.account_is_connected(self._account):
return return
message = nbxmpp.Message(to=room)
muc_user = message.addChild('x', namespace=nbxmpp.NS_MUC_USER)
decline = muc_user.addChild('decline', attrs={'to': to})
if reason:
decline.setTagData('reason', reason)
self._con.connection.send(message)
def request_voice(self, room): self._con.connection.get_module('MUC').request_config(
if not app.account_is_connected(self._account): room_jid, i18n.LANG, callback=self._config_received)
def _config_received(self, result):
if result.is_error:
return return
message = nbxmpp.Message(to=room)
xdata = nbxmpp.DataForm(typ='submit')
xdata.addChild(node=nbxmpp.DataField(name='FORM_TYPE',
value=nbxmpp.NS_MUC + '#request'))
xdata.addChild(node=nbxmpp.DataField(name='muc#role',
value='participant',
typ='text-single'))
message.addChild(node=xdata)
self._con.connection.send(message)
@staticmethod app.nec.push_incoming_event(NetworkEvent(
def _default_response(_con, stanza, **kwargs): 'muc-owner-received',
if not nbxmpp.isResultNode(stanza): conn=self._con,
log.info('Error: %s', stanza.getError()) dataform=result.form,
jid=result.jid))
class GcInvitationReceived(NetworkIncomingEvent):
name = 'gc-invitation-received'
class GcDeclineReceived(NetworkIncomingEvent):
name = 'gc-decline-received'
class MucOwnerReceivedEvent(NetworkIncomingEvent):
name = 'muc-owner-received'
def get_instance(*args, **kwargs): def get_instance(*args, **kwargs):

View file

@ -1291,51 +1291,60 @@ class RosterItemExchangeWindow:
class InvitationReceivedDialog: class InvitationReceivedDialog:
def __init__(self, account, room_jid, contact_fjid, password=None, def __init__(self, account, event):
comment=None, is_continued=False):
self.room_jid = room_jid
self.account = account self.account = account
self.password = password self.room_jid = str(event.muc)
self.is_continued = is_continued self.from_ = str(event.from_)
self.contact_fjid = contact_fjid self.password = event.password
self.is_continued = event.continued
jid = app.get_jid_without_resource(contact_fjid) if event.from_.bareMatch(event.muc):
contact_text = event.from_.getResource()
else:
contact = app.contacts.get_first_contact_from_jid(
account, event.from_.getBare())
if contact is None:
contact_text = str(event.from_)
else:
contact_text = contact.get_shown_name()
pritext = _('''You are invited to a groupchat''') pritext = _('''You are invited to a groupchat''')
#Don't translate $Contact #Don't translate $Contact
if is_continued: if self.is_continued:
sectext = _('$Contact has invited you to join a discussion') sectext = _('$Contact has invited you to join a discussion')
else: else:
sectext = _('$Contact has invited you to group chat %(room_jid)s')\ sectext = _('$Contact has invited you to group chat %(room_jid)s')\
% {'room_jid': room_jid} % {'room_jid': self.room_jid}
contact = app.contacts.get_first_contact_from_jid(account, jid)
contact_text = contact and contact.name or jid
sectext = i18n.direction_mark + sectext.replace('$Contact',
contact_text)
if comment: # only if not None and not '' sectext = sectext.replace('$Contact', contact_text)
comment = GLib.markup_escape_text(comment)
if event.reason:
comment = GLib.markup_escape_text(event.reason)
comment = _('Comment: %s') % comment comment = _('Comment: %s') % comment
sectext += '\n\n%s' % comment sectext += '\n\n%s' % comment
sectext += '\n\n' + _('Do you want to accept the invitation?') sectext += '\n\n' + _('Do you want to accept the invitation?')
def on_yes(checked, text): def on_yes(_checked, _text):
if self.is_continued: if self.is_continued:
app.interface.join_gc_room(self.account, self.room_jid, app.interface.join_gc_room(self.account,
app.nicks[self.account], self.password, self.room_jid,
is_continued=True) app.nicks[self.account],
self.password,
is_continued=True)
else: else:
app.interface.join_gc_minimal( app.interface.join_gc_minimal(self.account,
self.account, self.room_jid, password=self.password) self.room_jid,
password=self.password)
def on_no(text): def on_no(text):
app.connections[account].get_module('MUC').decline( app.connections[account].get_module('MUC').decline(
self.room_jid, self.contact_fjid, text) self.room_jid, self.from_, text)
dlg = YesNoDialog(pritext, sectext, dlg = YesNoDialog(pritext,
text_label=_('Reason (if you decline):'), on_response_yes=on_yes, sectext,
on_response_no=on_no) text_label=_('Reason (if you decline):'),
on_response_yes=on_yes,
on_response_no=on_no)
dlg.set_title(_('Groupchat Invitation')) dlg.set_title(_('Groupchat Invitation'))
class ProgressDialog: class ProgressDialog:

View file

@ -33,6 +33,7 @@ import logging
from enum import IntEnum, unique from enum import IntEnum, unique
import nbxmpp import nbxmpp
from nbxmpp.const import StatusCode
from gi.repository import Gtk from gi.repository import Gtk
from gi.repository import Gdk from gi.repository import Gdk
@ -1080,7 +1081,7 @@ class GroupchatControl(ChatControlBase):
def _on_voice_approval(self, event): def _on_voice_approval(self, event):
if event.account != self.account: if event.account != self.account:
return return
if event.room_jid != self.room_jid: if event.jid != self.room_jid:
return return
SingleMessageWindow(self.account, SingleMessageWindow(self.account,
self.room_jid, self.room_jid,
@ -1091,7 +1092,7 @@ class GroupchatControl(ChatControlBase):
def _on_captcha_challenge(self, event): def _on_captcha_challenge(self, event):
if event.account != self.account: if event.account != self.account:
return return
if event.room_jid != self.room_jid: if event.jid != self.room_jid:
return return
if self.form_widget: if self.form_widget:
@ -1419,11 +1420,11 @@ class GroupchatControl(ChatControlBase):
return return
self.set_subject(event.subject) self.set_subject(event.subject)
text = _('%(nick)s has set the subject to %(subject)s') % { text = _('%(nick)s has set the subject to %(subject)s') % {
'nick': event.resource, 'subject': event.subject} 'nick': event.nickname, 'subject': event.subject}
if event.delayed: if event.user_timestamp:
date = time.strftime('%d-%m-%Y %H:%M:%S', date = time.strftime('%d-%m-%Y %H:%M:%S',
time.localtime(event.timestamp)) time.localtime(event.user_timestamp))
text = '%s - %s' % (text, date) text = '%s - %s' % (text, date)
just_joined = self.join_time > time.time() - 10 just_joined = self.join_time > time.time() - 10
@ -1440,35 +1441,31 @@ class GroupchatControl(ChatControlBase):
if event.account != self.account: if event.account != self.account:
return return
if event.room_jid != self.room_jid: if event.jid != self.room_jid:
return return
changes = [] changes = []
if '100' in event.status_codes: if StatusCode.SHOWING_UNAVAILABLE in event.status_codes:
# Can be a presence (see chg_contact_status in groupchat_control.py)
changes.append(_('Any occupant is allowed to see your full JID'))
self.is_anonymous = False
if '102' in event.status_codes:
changes.append(_('Room now shows unavailable members')) changes.append(_('Room now shows unavailable members'))
if '103' in event.status_codes: if StatusCode.NOT_SHOWING_UNAVAILABLE in event.status_codes:
changes.append(_('Room now does not show unavailable members')) changes.append(_('Room now does not show unavailable members'))
if '104' in event.status_codes: if StatusCode.CONFIG_NON_PRIVACY_RELATED in event.status_codes:
changes.append(_('A setting not related to privacy has been ' changes.append(_('A setting not related to privacy has been '
'changed')) 'changed'))
app.connections[self.account].get_module('Discovery').disco_muc( app.connections[self.account].get_module('Discovery').disco_muc(
self.room_jid, self.update_actions, update=True) self.room_jid, self.update_actions, update=True)
if '170' in event.status_codes: if StatusCode.CONFIG_ROOM_LOGGING in event.status_codes:
# Can be a presence (see chg_contact_status in groupchat_control.py) # Can be a presence (see chg_contact_status in groupchat_control.py)
changes.append(_('Room logging is now enabled')) changes.append(_('Room logging is now enabled'))
if '171' in event.status_codes: if StatusCode.CONFIG_NO_ROOM_LOGGING in event.status_codes:
changes.append(_('Room logging is now disabled')) changes.append(_('Room logging is now disabled'))
if '172' in event.status_codes: if StatusCode.CONFIG_NON_ANONYMOUS in event.status_codes:
changes.append(_('Room is now non-anonymous')) changes.append(_('Room is now non-anonymous'))
self.is_anonymous = False self.is_anonymous = False
if '173' in event.status_codes: if StatusCode.CONFIG_SEMI_ANONYMOUS in event.status_codes:
changes.append(_('Room is now semi-anonymous')) changes.append(_('Room is now semi-anonymous'))
self.is_anonymous = True self.is_anonymous = True
if '174' in event.status_codes: if StatusCode.CONFIG_FULL_ANONYMOUS in event.status_codes:
changes.append(_('Room is now fully anonymous')) changes.append(_('Room is now fully anonymous'))
self.is_anonymous = True self.is_anonymous = True

View file

@ -62,8 +62,7 @@ class GroupchatConfig(Gtk.ApplicationWindow):
con.get_module('MUC').get_affiliation( con.get_module('MUC').get_affiliation(
self.jid, self.jid,
affiliation, affiliation,
self._on_affiliations_received, callback=self._on_affiliations_received)
self._on_affiliations_error)
if form is not None: if form is not None:
self._ui.stack.set_visible_child_name('config') self._ui.stack.set_visible_child_name('config')
@ -340,29 +339,28 @@ class GroupchatConfig(Gtk.ApplicationWindow):
con = app.connections[self.account] con = app.connections[self.account]
con.get_module('MUC').set_affiliation(self.jid, diff_dict) con.get_module('MUC').set_affiliation(self.jid, diff_dict)
def _on_affiliations_error(self, affiliation, error): def _on_affiliations_received(self, result):
log.info('Error while requesting %s affiliations: %s', if result.is_error:
affiliation, error) log.info('Error while requesting %s affiliations: %s',
result.affiliation, result.error)
return
def _on_affiliations_received(self, _account, _room_jid, if result.affiliation == 'outcast':
affiliation, users):
if affiliation == 'outcast':
self._ui.stack.get_child_by_name('outcast').show() self._ui.stack.get_child_by_name('outcast').show()
for jid, attrs in users.items(): for jid, attrs in result.users.items():
affiliation_edit, jid_edit = self._allowed_to_edit(affiliation) affiliation_edit, jid_edit = self._allowed_to_edit(result.affiliation)
if affiliation == 'outcast': if result.affiliation == 'outcast':
reason = attrs.get('reason') reason = attrs.get('reason')
self._ui.outcast_store.append( self._ui.outcast_store.append(
[jid, [jid,
reason, reason,
None, None,
affiliation, result.affiliation,
None, None,
affiliation_edit, affiliation_edit,
jid_edit]) jid_edit])
self._affiliations[jid] = {'affiliation': affiliation, self._affiliations[jid] = {'affiliation': result.affiliation,
'reason': reason} 'reason': reason}
else: else:
nick = attrs.get('nick') nick = attrs.get('nick')
@ -371,11 +369,11 @@ class GroupchatConfig(Gtk.ApplicationWindow):
[jid, [jid,
nick, nick,
role, role,
affiliation, result.affiliation,
_(affiliation.capitalize()), _(result.affiliation.capitalize()),
affiliation_edit, affiliation_edit,
jid_edit]) jid_edit])
self._affiliations[jid] = {'affiliation': affiliation, self._affiliations[jid] = {'affiliation': result.affiliation,
'nick': nick} 'nick': nick}
if role is not None: if role is not None:
self._ui.role_column.set_visible(True) self._ui.role_column.set_visible(True)

View file

@ -109,7 +109,7 @@ class Notification:
def _on_event_removed(self, event_list): def _on_event_removed(self, event_list):
for event in event_list: for event in event_list:
if event.type_ == 'gc-invitation': if event.type_ == 'gc-invitation':
self._withdraw('gc-invitation', event.account, event.room_jid) self._withdraw('gc-invitation', event.account, event.muc)
if event.type_ in ('normal', 'printed_chat', 'chat', if event.type_ in ('normal', 'printed_chat', 'chat',
'printed_pm', 'pm', 'printed_marked_gc_msg', 'printed_pm', 'pm', 'printed_marked_gc_msg',
'printed_gc_msg'): 'printed_gc_msg'):

View file

@ -609,39 +609,43 @@ class Interface:
else: else:
GroupchatConfig(account, obj.jid, 'owner', obj.dataform) GroupchatConfig(account, obj.jid, 'owner', obj.dataform)
def handle_event_gc_decline(self, obj): def handle_event_gc_decline(self, event):
gc_control = self.msg_win_mgr.get_gc_control(obj.room_jid, obj.account) gc_control = self.msg_win_mgr.get_gc_control(str(event.muc),
event.account)
if gc_control: if gc_control:
if obj.reason: if event.reason:
gc_control.print_conversation( gc_control.print_conversation(
_('%(jid)s declined the invitation: %(reason)s') % { _('%(jid)s declined the invitation: %(reason)s') % {
'jid': obj.from_, 'reason': obj.reason}, 'jid': event.from_, 'reason': event.reason},
graphics=False) graphics=False)
else: else:
gc_control.print_conversation( gc_control.print_conversation(
_('%(jid)s declined the invitation') % { _('%(jid)s declined the invitation') % {
'jid': obj.from_}, graphics=False) 'jid': event.from_}, graphics=False)
def handle_event_gc_invitation(self, obj): def handle_event_gc_invitation(self, event):
if helpers.allow_popup_window(obj.account) or not self.systray_enabled: if helpers.allow_popup_window(event.account) or not self.systray_enabled:
dialogs.InvitationReceivedDialog( dialogs.InvitationReceivedDialog(event.account, event)
obj.account, obj.room_jid,
str(obj.from_), obj.password, obj.reason,
is_continued=obj.is_continued)
return return
event = events.GcInvitationtEvent( from_ = str(event.from_)
obj.room_jid, obj.reason, muc = str(event.muc)
obj.password, obj.is_continued, str(obj.from_))
self.add_event(obj.account, str(obj.from_), event)
if helpers.allow_showing_notification(obj.account): event_ = events.GcInvitationtEvent(event)
self.add_event(event.account, from_, event_)
if helpers.allow_showing_notification(event.account):
event_type = _('Groupchat Invitation') event_type = _('Groupchat Invitation')
text = _('You are invited to {room} by {user}').format( text = _('You are invited to {room} by {user}').format(room=muc,
room=obj.room_jid, user=str(obj.from_)) user=from_)
app.notification.popup( app.notification.popup(event_type,
event_type, str(obj.from_), obj.account, 'gc-invitation', from_,
'gajim-gc_invitation', event_type, text, room_jid=obj.room_jid) event.account,
'gc-invitation',
'gajim-gc_invitation',
event_type,
text,
room_jid=muc)
def forget_gpg_passphrase(self, keyid): def forget_gpg_passphrase(self, keyid):
if keyid in self.gpg_passphrase: if keyid in self.gpg_passphrase:
@ -1523,7 +1527,9 @@ class Interface:
if app.contacts.get_contact_with_highest_priority(account, jid): if app.contacts.get_contact_with_highest_priority(account, jid):
self.roster.draw_contact(jid, account) self.roster.draw_contact(jid, account)
else: else:
self.roster.add_to_not_in_the_roster(account, jid) groupchat = event.type_ == 'gc-invitation'
self.roster.add_to_not_in_the_roster(
account, jid, groupchat=groupchat)
# Select the big brother contact in roster, it's visible because it has # Select the big brother contact in roster, it's visible because it has
# events. # events.
@ -1621,8 +1627,7 @@ class Interface:
event = app.events.get_first_event(account, jid, type_) event = app.events.get_first_event(account, jid, type_)
if event is None: if event is None:
return return
dialogs.InvitationReceivedDialog(account, event.room_jid, jid, dialogs.InvitationReceivedDialog(account, event)
event.password, event.reason, event.is_continued)
app.events.remove_events(account, jid, event) app.events.remove_events(account, jid, event)
self.roster.draw_contact(jid, account) self.roster.draw_contact(jid, account)
elif type_ == 'subscription_request': elif type_ == 'subscription_request':

View file

@ -1032,14 +1032,16 @@ class RosterWindow:
self.draw_group(group, account) self.draw_group(group, account)
# FIXME: integrate into add_contact() # FIXME: integrate into add_contact()
def add_to_not_in_the_roster(self, account, jid, nick='', resource=''): def add_to_not_in_the_roster(self, account, jid, nick='', resource='',
groupchat=False):
keyID = '' keyID = ''
attached_keys = app.config.get_per('accounts', account, attached_keys = app.config.get_per('accounts', account,
'attached_gpg_keys').split() 'attached_gpg_keys').split()
if jid in attached_keys: if jid in attached_keys:
keyID = attached_keys[attached_keys.index(jid) + 1] keyID = attached_keys[attached_keys.index(jid) + 1]
contact = app.contacts.create_not_in_roster_contact(jid=jid, contact = app.contacts.create_not_in_roster_contact(
account=account, resource=resource, name=nick, keyID=keyID) jid=jid, account=account, resource=resource, name=nick,
keyID=keyID, groupchat=groupchat)
app.contacts.add_contact(account, contact) app.contacts.add_contact(account, contact)
self.add_contact(contact.jid, account) self.add_contact(contact.jid, account)
return contact return contact
@ -2002,9 +2004,7 @@ class RosterWindow:
return True return True
if event.type_ == 'gc-invitation': if event.type_ == 'gc-invitation':
dialogs.InvitationReceivedDialog(account, event.room_jid, dialogs.InvitationReceivedDialog(account, event)
event.jid_from, event.password, event.reason,
is_continued=event.is_continued)
app.events.remove_events(account, jid, event) app.events.remove_events(account, jid, event)
return True return True
@ -2692,12 +2692,8 @@ class RosterWindow:
app.log('avatar').debug('Draw roster avatar: %s', obj.jid) app.log('avatar').debug('Draw roster avatar: %s', obj.jid)
self.draw_avatar(obj.jid, obj.account) self.draw_avatar(obj.jid, obj.account)
def _nec_gc_subject_received(self, obj): def _nec_gc_subject_received(self, event):
contact = app.contacts.get_contact_with_highest_priority( self.draw_contact(event.jid, event.account)
obj.account, obj.jid)
if contact:
contact.status = obj.subject
self.draw_contact(obj.jid, obj.account)
def _nec_metacontacts_received(self, obj): def _nec_metacontacts_received(self, obj):
self.redraw_metacontacts(obj.conn.name) self.redraw_metacontacts(obj.conn.name)