Commit 2f7ca557 authored by Philipp Hörist's avatar Philipp Hörist

Refactor sending messages

- Move stanza building and message logging code into message module
- Don't use event flow to create and send messages
- Remove resource lock
- Properly support sending a message to multiple recipients
parent 904c6209
......@@ -587,8 +587,8 @@ class ChatControl(ChatControlBase):
if not event.message:
return
self.last_sent_msg = event.stanza_id
message_id = event.msg_iq.getID()
self.last_sent_msg = event.message_id
message_id = event.message_id
if event.label:
displaymarking = event.label.getTag('displaymarking')
......@@ -919,8 +919,12 @@ class ChatControl(ChatControlBase):
fixed.set_no_show_all(True)
self.close_jingle_content(jingle_type)
def send_message(self, message, xhtml=None,
process_commands=True, attention=False):
def send_message(self,
message,
xhtml=None,
process_commands=True,
attention=False,
is_muc_pm=False):
"""
Send a message to contact
"""
......@@ -941,7 +945,8 @@ class ChatControl(ChatControlBase):
type_='chat',
xhtml=xhtml,
process_commands=process_commands,
attention=attention)
attention=attention,
is_muc_pm=is_muc_pm)
def get_our_nick(self):
return app.nicks[self.account]
......
......@@ -43,8 +43,8 @@ from gajim.common.i18n import _
from gajim.common.nec import EventHelper
from gajim.common.helpers import AdditionalDataDict
from gajim.common.contacts import GC_Contact
from gajim.common.connection_handlers_events import MessageOutgoingEvent
from gajim.common.const import Chatstate
from gajim.common.structs import OutgoingMessage
from gajim import gtkgui_helpers
......@@ -273,7 +273,6 @@ class ChatControlBase(ChatCommandProcessor, CommandTools, EventHelper):
('ping-error', ged.GUI1, self._nec_ping),
('sec-catalog-received', ged.GUI1, self._sec_labels_received),
('style-changed', ged.GUI1, self._style_changed),
('message-outgoing', ged.OUT_GUI1, self._nec_message_outgoing),
])
# pylint: enable=line-too-long
......@@ -451,38 +450,6 @@ class ChatControlBase(ChatCommandProcessor, CommandTools, EventHelper):
def _nec_ping(self, obj):
raise NotImplementedError
def _nec_message_outgoing(self, obj):
# Send the given message to the active tab.
# Doesn't return None if error
if obj.control != self:
return
obj.message = helpers.remove_invalid_xml_chars(obj.message)
conn = app.connections[self.account]
if not self.session:
if (not obj.resource and
obj.jid != app.get_jid_from_account(self.account)):
if self.resource:
obj.resource = self.resource
else:
obj.resource = self.contact.resource
sess = conn.find_controlless_session(obj.jid, resource=obj.resource)
if self.resource:
obj.jid += '/' + self.resource
if not sess:
if self._type.is_privatechat:
sess = conn.make_new_session(obj.jid, type_='pm')
else:
sess = conn.make_new_session(obj.jid)
self.set_session(sess)
obj.session = self.session
def setup_seclabel(self):
self.xml.label_selector.hide()
self.xml.label_selector.set_no_show_all(True)
......@@ -1014,7 +981,8 @@ class ChatControlBase(ChatCommandProcessor, CommandTools, EventHelper):
resource=None,
xhtml=None,
process_commands=True,
attention=False):
attention=False,
is_muc_pm=False):
"""
Send the given message to the active tab. Doesn't return None if error
"""
......@@ -1035,22 +1003,23 @@ class ChatControlBase(ChatCommandProcessor, CommandTools, EventHelper):
chatstate = con.get_module('Chatstate').get_active_chatstate(
self.contact)
event = MessageOutgoingEvent(None,
account=self.account,
jid=self.contact.jid,
message=message,
type_=type_,
chatstate=chatstate,
resource=resource,
user_nick=self.user_nick,
label=label,
control=self,
attention=attention,
correct_id=correct_id,
automatic_message=False,
encryption=self.encryption)
event.additional_data.set_value('gajim', 'xhtml', xhtml)
app.nec.push_outgoing_event(event)
message_ = OutgoingMessage(account=self.account,
jid=self.contact.jid,
message=message,
type_=type_,
is_muc_pm=is_muc_pm,
chatstate=chatstate,
resource=resource,
user_nick=self.user_nick,
label=label,
control=self,
attention=attention,
correct_id=correct_id,
automatic_message=False,
encryption=self.encryption,
xhtml=xhtml)
con.send_message(message_)
# Record the history of sent messages
self.save_message(message, 'sent')
......
This diff is collapsed.
......@@ -27,8 +27,7 @@ from gajim.common import helpers
from gajim.common import app
from gajim.common import i18n
from gajim.common.i18n import _
from gajim.common.helpers import AdditionalDataDict
from gajim.common.const import KindConstant, SSLError
from gajim.common.const import SSLError
from gajim.common.jingle_transport import JingleTransportSocks5
from gajim.common.file_props import FilesProp
......@@ -464,73 +463,6 @@ class NotificationEvent(nec.NetworkIncomingEvent):
self.popup_text = pres_obj.status
self.popup_event_type = _('Contact Signed Out')
class MessageOutgoingEvent(nec.NetworkOutgoingEvent):
name = 'message-outgoing'
def init(self):
self.additional_data = AdditionalDataDict()
self.message = None
self.type_ = 'chat'
self.kind = None
self.timestamp = None
self.subject = ''
self.chatstate = None
self.stanza_id = None
self.resource = None
self.user_nick = None
self.label = None
self.session = None
self.delayed = None
self.callback = None
self.callback_args = []
self.now = False
self.is_loggable = True
self.control = None
self.attention = False
self.correct_id = None
self.automatic_message = True
def get_full_jid(self):
if self.resource:
return self.jid + '/' + self.resource
if self.session:
return self.session.get_to()
return self.jid
def generate(self):
if self.type_ == 'chat':
self.kind = KindConstant.CHAT_MSG_SENT
else:
self.kind = KindConstant.SINGLE_MSG_SENT
return True
class StanzaMessageOutgoingEvent(nec.NetworkOutgoingEvent):
name = 'stanza-message-outgoing'
class GcStanzaMessageOutgoingEvent(nec.NetworkOutgoingEvent):
name = 'gc-stanza-message-outgoing'
class GcMessageOutgoingEvent(nec.NetworkOutgoingEvent):
name = 'gc-message-outgoing'
def init(self):
self.additional_data = AdditionalDataDict()
self.message = ''
self.chatstate = None
self.stanza_id = None
self.label = None
self.callback = None
self.callback_args = []
self.is_loggable = True
self.control = None
self.correct_id = None
self.automatic_message = True
def generate(self):
return True
class InformationEvent(nec.NetworkIncomingEvent):
name = 'information'
......
......@@ -148,3 +148,6 @@ class StanzaMalformed(Exception):
self._msg = '{}\n{}'.format(message, stanza)
def __str__(self):
return self._msg
class SendMessageError(Exception):
pass
......@@ -43,6 +43,7 @@ import time
import logging
import json
import shutil
import copy
import collections
from collections import defaultdict
import random
......@@ -1411,6 +1412,9 @@ class AdditionalDataDict(collections.UserDict):
except KeyError:
return
def copy(self):
return copy.deepcopy(self)
def save_roster_position(window):
if not app.config.get('save-roster-position'):
......
......@@ -30,9 +30,8 @@ from gi.repository import GLib
from gajim.common import app
from gajim.common.nec import NetworkEvent
from gajim.common.const import Chatstate as State
from gajim.common.structs import OutgoingMessage
from gajim.common.modules.base import BaseModule
from gajim.common.connection_handlers_events import MessageOutgoingEvent
from gajim.common.connection_handlers_events import GcMessageOutgoingEvent
from gajim.common.types import ContactT
from gajim.common.types import ConnectionT
......@@ -272,17 +271,7 @@ class Chatstate(BaseModule):
self._log.info('Send last state: %-10s - %s',
State.ACTIVE, contact.jid)
event_attrs = {'account': self._account,
'jid': contact.jid,
'chatstate': str(State.ACTIVE)}
if contact.is_groupchat:
if contact.is_connected:
app.nec.push_outgoing_event(
GcMessageOutgoingEvent(None, **event_attrs))
else:
app.nec.push_outgoing_event(
MessageOutgoingEvent(None, **event_attrs))
self._send_chatstate(contact, str(State.ACTIVE))
self._chatstates.pop(contact.jid, None)
self._last_mouse_activity.pop(contact.jid, None)
......@@ -317,20 +306,20 @@ class Chatstate(BaseModule):
self._log.info('Send: %-10s - %s', state, contact.jid)
event_attrs = {'account': self._account,
'jid': contact.jid,
'chatstate': str(state)}
if contact.is_groupchat:
if contact.is_connected:
app.nec.push_outgoing_event(
GcMessageOutgoingEvent(None, **event_attrs))
else:
app.nec.push_outgoing_event(
MessageOutgoingEvent(None, **event_attrs))
self._send_chatstate(contact, str(state))
self._chatstates[contact.jid] = state
def _send_chatstate(self, contact, chatstate):
type_ = 'groupchat' if contact.is_groupchat else 'chat'
message = OutgoingMessage(account=self._account,
jid=contact.jid,
message=None,
type_=type_,
chatstate=chatstate)
self._con.send_message(message)
@ensure_enabled
def set_mouse_activity(self, contact: ContactT, was_paused: bool) -> None:
if self._get_chatstate_setting(contact) == 'disabled':
......
......@@ -20,22 +20,19 @@ import io
from urllib.parse import urlparse
import mimetypes
import nbxmpp
from nbxmpp import NS_HTTPUPLOAD_0
from nbxmpp.util import is_error_result
from gi.repository import GLib
from gi.repository import Soup
from gajim.common import app
from gajim.common import ged
from gajim.common.i18n import _
from gajim.common.helpers import get_tls_error_phrase
from gajim.common.const import FTState
from gajim.common.filetransfer import FileTransfer
from gajim.common.modules.base import BaseModule
from gajim.common.structs import OutgoingMessage
from gajim.common.connection_handlers_events import InformationEvent
from gajim.common.connection_handlers_events import MessageOutgoingEvent
from gajim.common.connection_handlers_events import GcMessageOutgoingEvent
class HTTPUpload(BaseModule):
......@@ -50,19 +47,11 @@ class HTTPUpload(BaseModule):
self.httpupload_namespace = None
self.max_file_size = None # maximum file size in bytes
self._text = []
self._queued_messages = {}
self._session = Soup.Session()
self._session.props.ssl_strict = False
self._session.props.user_agent = 'Gajim %s' % app.version
# pylint: disable=line-too-long
self.register_events([
('stanza-message-outgoing', ged.OUT_PREGUI, self._handle_outgoing_stanza),
('gc-stanza-message-outgoing', ged.OUT_PREGUI, self._handle_outgoing_stanza),
])
# pylint: enable=line-too-long
def pass_disco(self, info):
if not info.has_httpupload:
return
......@@ -84,18 +73,6 @@ class HTTPUpload(BaseModule):
for ctrl in app.interface.msg_win_mgr.get_controls(acct=self._account):
ctrl.update_actions()
def _handle_outgoing_stanza(self, event):
if event.conn.name != self._account:
return
body = event.msg_iq.getTagData('body')
if body and body in self._text:
self._text.remove(body)
# Add oob information before sending message to recipient,
# to distinguish HTTP File Upload Link from pasted URL
oob = event.msg_iq.addChild('x', namespace=nbxmpp.NS_X_OOB)
oob.addChild('url').setData(body)
event.additional_data.set_value('gajim', 'oob_url', body)
def check_file_before_transfer(self, path, encryption, contact,
groupchat=False):
if not path or not os.path.exists(path):
......@@ -258,23 +235,18 @@ class HTTPUpload(BaseModule):
if message.props.status_code in (Soup.Status.OK, Soup.Status.CREATED):
self._log.info('Upload completed successfully')
uri = transfer.get_transformed_uri()
self._text.append(uri)
type_ = 'chat'
if transfer.is_groupchat:
app.nec.push_outgoing_event(
GcMessageOutgoingEvent(None,
account=self._account,
jid=transfer.contact.jid,
message=uri,
automatic_message=False))
else:
app.nec.push_outgoing_event(
MessageOutgoingEvent(None,
account=self._account,
jid=transfer.contact.jid,
message=uri,
type_='chat',
automatic_message=False))
type_ = 'groupchat'
message = OutgoingMessage(account=self._account,
jid=transfer.contact.jid,
message=uri,
type_=type_,
oob_url=uri)
self._con.send_message(message)
else:
phrase = Soup.Status.get_phrase(message.props.status_code)
......
......@@ -18,10 +18,12 @@ import time
import nbxmpp
from nbxmpp.structs import StanzaHandler
from nbxmpp.util import generate_id
from gajim.common import app
from gajim.common.nec import NetworkEvent
from gajim.common.helpers import AdditionalDataDict
from gajim.common.helpers import validate_jid
from gajim.common.const import KindConstant
from gajim.common.modules.base import BaseModule
from gajim.common.modules.util import get_eme_message
......@@ -29,6 +31,8 @@ from gajim.common.modules.security_labels import parse_securitylabel
from gajim.common.modules.misc import parse_correction
from gajim.common.modules.misc import parse_oob
from gajim.common.modules.misc import parse_xhtml
from gajim.common.connection_handlers_events import InformationEvent
from gajim.common.connection_handlers_events import MessageSentEvent
class Message(BaseModule):
......@@ -280,6 +284,88 @@ class Message(BaseModule):
# stanza-id not added by the archive, ignore it.
return None, None
def build_message_stanza(self, message):
own_jid = self._con.get_own_jid()
stanza = nbxmpp.Message(to=message.jid,
body=message.message,
typ=message.type_,
subject=message.subject,
xhtml=message.xhtml)
if message.correct_id:
stanza.setTag('replace', attrs={'id': message.correct_id},
namespace=nbxmpp.NS_CORRECT)
# XEP-0359
message.message_id = generate_id()
stanza.setID(message.message_id)
stanza.setOriginID(message.message_id)
if message.label:
stanza.addChild(node=message.label)
# XEP-0172: user_nickname
if message.user_nick:
stanza.setTag('nick', namespace=nbxmpp.NS_NICK).setData(
message.user_nick)
# XEP-0203
# TODO: Seems delayed is not set anywhere
if message.delayed:
timestamp = time.strftime('%Y-%m-%dT%H:%M:%SZ',
time.gmtime(message.delayed))
stanza.addChild('delay',
namespace=nbxmpp.NS_DELAY2,
attrs={'from': str(own_jid), 'stamp': timestamp})
# XEP-0224
if message.attention:
stanza.setTag('attention', namespace=nbxmpp.NS_ATTENTION)
# XEP-0066
if message.oob_url is not None:
oob = stanza.addChild('x', namespace=nbxmpp.NS_X_OOB)
oob.addChild('url').setData(message.oob_url)
# XEP-0184
if not own_jid.bareMatch(message.jid):
if message.message and not message.is_groupchat:
stanza.setReceiptRequest()
# Mark Message as MUC PM
if message.is_muc_pm:
stanza.setTag('x', namespace=nbxmpp.NS_MUC_USER)
# XEP-0085
if message.chatstate is not None:
stanza.setTag(message.chatstate, namespace=nbxmpp.NS_CHATSTATES)
if not message.message:
stanza.setTag('no-store',
namespace=nbxmpp.NS_MSG_HINTS)
return stanza
def log_message(self, message):
if not message.is_loggable:
return
if not app.config.should_log(self._account, message.jid):
return
if message.message is None:
return
app.logger.insert_into_logs(self._account,
message.jid,
message.timestamp,
message.kind,
message=message.message,
subject=message.subject,
additional_data=message.additional_data,
message_id=message.message_id,
stanza_id=message.message_id)
def get_instance(*args, **kwargs):
return Message(*args, **kwargs), 'Message'
......@@ -12,11 +12,13 @@
# You should have received a copy of the GNU General Public License
# along with Gajim. If not, see <http://www.gnu.org/licenses/>.
import time
from collections import namedtuple
from nbxmpp.protocol import JID
from gajim.common.const import MUCJoinedState
from gajim.common.const import KindConstant
URI = namedtuple('URI', 'type action data')
URI.__new__.__defaults__ = (None, None) # type: ignore
......@@ -48,3 +50,122 @@ class MUCData:
@property
def config(self):
return self._config
class OutgoingMessage:
def __init__(self,
account,
jid,
message,
type_,
subject=None,
is_muc_pm=False,
chatstate=None,
resource=None,
user_nick=None,
label=None,
control=None,
attention=None,
correct_id=None,
automatic_message=False,
encryption=None,
oob_url=None,
xhtml=None):
if type_ not in ('chat', 'groupchat', 'normal', 'headline'):
raise ValueError('Unknown message type: %s' % type_)
if not message and chatstate is None:
raise ValueError('Trying to send message without content')
self.account = account
self.jid = jid
self.message = message
self.type_ = type_
if type_ == 'chat':
self.kind = KindConstant.CHAT_MSG_SENT
elif type_ == 'groupchat':
self.kind = KindConstant.GC_MSG
elif type_ == 'normal':
self.kind = KindConstant.SINGLE_MSG_SENT
else:
raise ValueError('Unknown message type')
from gajim.common.helpers import AdditionalDataDict
self.additional_data = AdditionalDataDict()
self.subject = subject
self.is_muc_pm = is_muc_pm
self.chatstate = chatstate
self.resource = resource
self.user_nick = user_nick
self.label = label
self.control = control
self.attention = attention
self.correct_id = correct_id
self.automatic_message = automatic_message
self.encryption = encryption
self.oob_url = oob_url
if oob_url is not None:
self.additional_data.set_value('gajim', 'oob_url', oob_url)
self.xhtml = xhtml
if xhtml is not None:
self.additional_data.set_value('gajim', 'xhtml', xhtml)
self.timestamp = None
self.message_id = None
self.stanza = None
self.session = None
self.delayed = None # TODO never set
self.is_loggable = True
def copy(self):
message = OutgoingMessage(self.account,
self.jid,
self.message,
self.type_)
for name, value in vars(self).items():
setattr(message, name, value)
message.additional_data = self.additional_data.copy()
return message
@property
def is_groupchat(self):
return self.type_ == 'groupchat'
@property
def is_chat(self):
return self.type_ == 'chat'
@property
def is_normal(self):
return self.type_ == 'normal'