/**
* This file is part of TelepathyQt
*
* @copyright Copyright (C) 2009 Collabora Ltd.
* @copyright Copyright (C) 2009 Nokia Corporation
* @license LGPL 2.1
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/
#include
#include "TelepathyQt/_gen/text-channel.moc.hpp"
#include "TelepathyQt/debug-internal.h"
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
namespace Tp
{
struct TP_QT_NO_EXPORT TextChannel::Private
{
Private(TextChannel *parent);
~Private();
static void introspectMessageQueue(Private *self);
static void introspectMessageCapabilities(Private *self);
static void introspectMessageSentSignal(Private *self);
static void enableChatStateNotifications(Private *self);
void updateInitialMessages();
void updateCapabilities();
void processMessageQueue();
void processChatStateQueue();
void contactLost(uint handle);
void contactFound(ContactPtr contact);
// Public object
TextChannel *parent;
Client::ChannelTypeTextInterface *textInterface;
Client::DBus::PropertiesInterface *properties;
ReadinessHelper *readinessHelper;
// FeatureMessageCapabilities and FeatureMessageQueue
QVariantMap props;
bool getAllInFlight;
bool gotProperties;
// requires FeatureMessageCapabilities
QList supportedMessageTypes;
QStringList supportedContentTypes;
MessagePartSupportFlags messagePartSupport;
DeliveryReportingSupportFlags deliveryReportingSupport;
// FeatureMessageQueue
bool initialMessagesReceived;
struct MessageEvent
{
MessageEvent(const ReceivedMessage &message)
: isMessage(true), message(message),
removed(0)
{ }
MessageEvent(uint removed)
: isMessage(false), message(), removed(removed)
{ }
bool isMessage;
ReceivedMessage message;
uint removed;
};
QList messages;
QList incompleteMessages;
QHash acknowledgeBatches;
// FeatureChatState
struct ChatStateEvent
{
ChatStateEvent(uint contactHandle, uint state)
: contactHandle(contactHandle), state(state)
{ }
ContactPtr contact;
uint contactHandle;
uint state;
};
QList chatStateQueue;
QHash chatStates;
QSet awaitingContacts;
};
TextChannel::Private::Private(TextChannel *parent)
: parent(parent),
textInterface(parent->interface()),
properties(parent->interface()),
readinessHelper(parent->readinessHelper()),
getAllInFlight(false),
gotProperties(false),
messagePartSupport(nullptr),
deliveryReportingSupport(nullptr),
initialMessagesReceived(false)
{
ReadinessHelper::Introspectables introspectables;
ReadinessHelper::Introspectable introspectableMessageQueue(
QSet() << 0, // makesSenseForStatuses
Features() << Channel::FeatureCore, // dependsOnFeatures (core)
QStringList(), // dependsOnInterfaces
(ReadinessHelper::IntrospectFunc) &Private::introspectMessageQueue,
this);
introspectables[FeatureMessageQueue] = introspectableMessageQueue;
ReadinessHelper::Introspectable introspectableMessageCapabilities(
QSet() << 0, // makesSenseForStatuses
Features() << Channel::FeatureCore, // dependsOnFeatures (core)
QStringList(), // dependsOnInterfaces
(ReadinessHelper::IntrospectFunc) &Private::introspectMessageCapabilities,
this);
introspectables[FeatureMessageCapabilities] = introspectableMessageCapabilities;
ReadinessHelper::Introspectable introspectableMessageSentSignal(
QSet() << 0, // makesSenseForStatuses
Features() << Channel::FeatureCore, // dependsOnFeatures (core)
QStringList(), // dependsOnInterfaces
(ReadinessHelper::IntrospectFunc) &Private::introspectMessageSentSignal,
this);
introspectables[FeatureMessageSentSignal] = introspectableMessageSentSignal;
ReadinessHelper::Introspectable introspectableChatState(
QSet() << 0, // makesSenseForStatuses
Features() << Channel::FeatureCore, // dependsOnFeatures (core)
QStringList() << TP_QT_IFACE_CHANNEL_INTERFACE_CHAT_STATE, // dependsOnInterfaces
(ReadinessHelper::IntrospectFunc) &Private::enableChatStateNotifications,
this);
introspectables[FeatureChatState] = introspectableChatState;
readinessHelper->addIntrospectables(introspectables);
}
TextChannel::Private::~Private()
{
foreach (MessageEvent *e, incompleteMessages) {
delete e;
}
foreach (ChatStateEvent *e, chatStateQueue) {
delete e;
}
}
void TextChannel::Private::introspectMessageQueue(
TextChannel::Private *self)
{
TextChannel *parent = self->parent;
if (parent->hasMessagesInterface()) {
Client::ChannelInterfaceMessagesInterface *messagesInterface =
parent->interface();
// FeatureMessageQueue needs signal connections + Get (but we
// might as well do GetAll and reduce the number of code paths)
parent->connect(messagesInterface,
SIGNAL(MessageReceived(Tp::MessagePartList)),
SLOT(onMessageReceived(Tp::MessagePartList)));
parent->connect(messagesInterface,
SIGNAL(PendingMessagesRemoved(Tp::UIntList)),
SLOT(onPendingMessagesRemoved(Tp::UIntList)));
if (!self->gotProperties && !self->getAllInFlight) {
self->getAllInFlight = true;
QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(
self->properties->GetAll(
TP_QT_IFACE_CHANNEL_INTERFACE_MESSAGES),
parent);
parent->connect(watcher,
SIGNAL(finished(QDBusPendingCallWatcher*)),
SLOT(gotProperties(QDBusPendingCallWatcher*)));
} else if (self->gotProperties) {
self->updateInitialMessages();
}
} else {
// FeatureMessageQueue needs signal connections + ListPendingMessages
parent->connect(self->textInterface,
SIGNAL(Received(uint,uint,uint,uint,uint,QString)),
SLOT(onTextReceived(uint,uint,uint,uint,uint,const QString)));
// we present SendError signals as if they were incoming
// messages, to be consistent with Messages
parent->connect(self->textInterface,
SIGNAL(SendError(uint,uint,uint,QString)),
SLOT(onTextSendError(uint,uint,uint,QString)));
QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(
self->textInterface->ListPendingMessages(false), parent);
parent->connect(watcher,
SIGNAL(finished(QDBusPendingCallWatcher*)),
SLOT(gotPendingMessages(QDBusPendingCallWatcher*)));
}
}
void TextChannel::Private::introspectMessageCapabilities(
TextChannel::Private *self)
{
TextChannel *parent = self->parent;
if (parent->hasMessagesInterface()) {
if (!self->gotProperties && !self->getAllInFlight) {
self->getAllInFlight = true;
QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(
self->properties->GetAll(
TP_QT_IFACE_CHANNEL_INTERFACE_MESSAGES),
parent);
parent->connect(watcher,
SIGNAL(finished(QDBusPendingCallWatcher*)),
SLOT(gotProperties(QDBusPendingCallWatcher*)));
} else if (self->gotProperties) {
self->updateCapabilities();
}
} else {
self->supportedContentTypes =
(QStringList(QLatin1String("text/plain")));
parent->readinessHelper()->setIntrospectCompleted(
FeatureMessageCapabilities, true);
}
}
void TextChannel::Private::introspectMessageSentSignal(
TextChannel::Private *self)
{
TextChannel *parent = self->parent;
if (parent->hasMessagesInterface()) {
Client::ChannelInterfaceMessagesInterface *messagesInterface =
parent->interface();
parent->connect(messagesInterface,
SIGNAL(MessageSent(Tp::MessagePartList,uint,QString)),
SLOT(onMessageSent(Tp::MessagePartList,uint,QString)));
} else {
parent->connect(self->textInterface,
SIGNAL(Sent(uint,uint,QString)),
SLOT(onTextSent(uint,uint,QString)));
}
self->readinessHelper->setIntrospectCompleted(FeatureMessageSentSignal, true);
}
void TextChannel::Private::enableChatStateNotifications(
TextChannel::Private *self)
{
TextChannel *parent = self->parent;
Client::ChannelInterfaceChatStateInterface *chatStateInterface =
parent->interface();
parent->connect(chatStateInterface,
SIGNAL(ChatStateChanged(uint,uint)),
SLOT(onChatStateChanged(uint,uint)));
// FIXME fd.o#24882 - Download contacts' initial chat states
self->readinessHelper->setIntrospectCompleted(FeatureChatState, true);
}
void TextChannel::Private::updateInitialMessages()
{
if (!readinessHelper->requestedFeatures().contains(FeatureMessageQueue) ||
readinessHelper->isReady(Features() << FeatureMessageQueue)) {
return;
}
Q_ASSERT(!initialMessagesReceived);
initialMessagesReceived = true;
MessagePartListList messages = qdbus_cast(
props[QLatin1String("PendingMessages")]);
if (messages.isEmpty()) {
debug() << "Message queue empty: FeatureMessageQueue is now ready";
readinessHelper->setIntrospectCompleted(FeatureMessageQueue, true);
} else {
foreach (const MessagePartList &message, messages) {
parent->onMessageReceived(message);
}
}
}
void TextChannel::Private::updateCapabilities()
{
if (!readinessHelper->requestedFeatures().contains(FeatureMessageCapabilities) ||
readinessHelper->isReady(Features() << FeatureMessageCapabilities)) {
return;
}
UIntList messageTypesAsUIntList = qdbus_cast(props[QLatin1String("MessageTypes")]);
// Populate the list with the correct variable type
supportedMessageTypes.clear();
foreach (uint messageType, messageTypesAsUIntList) {
supportedMessageTypes.append(static_cast(messageType));
}
supportedContentTypes = qdbus_cast(
props[QLatin1String("SupportedContentTypes")]);
if (supportedContentTypes.isEmpty()) {
supportedContentTypes << QLatin1String("text/plain");
}
messagePartSupport = MessagePartSupportFlags(qdbus_cast(
props[QLatin1String("MessagePartSupportFlags")]));
deliveryReportingSupport = DeliveryReportingSupportFlags(
qdbus_cast(props[QLatin1String("DeliveryReportingSupport")]));
readinessHelper->setIntrospectCompleted(FeatureMessageCapabilities, true);
}
void TextChannel::Private::processMessageQueue()
{
// Proceed as far as we can with the processing of incoming messages
// and message-removal events; message IDs aren't necessarily globally
// unique, so we need to process them in the correct order relative
// to incoming messages
while (!incompleteMessages.isEmpty()) {
const MessageEvent *e = incompleteMessages.first();
debug() << "MessageEvent:" << reinterpret_cast(e);
if (e->isMessage) {
if (e->message.senderHandle() != 0 &&
!e->message.sender()) {
// the message doesn't have a sender Contact, but needs one.
// We'll have to stop processing here, and come back to it
// when we have more Contact objects
break;
}
// if we reach here, the message is ready
debug() << "Message is usable, copying to main queue";
messages << e->message;
emit parent->messageReceived(e->message);
} else {
// forget about the message(s) with ID e->removed (there should be
// at most one under normal circumstances)
int i = 0;
while (i < messages.size()) {
if (messages.at(i).pendingId() == e->removed) {
ReceivedMessage removedMessage = messages.at(i);
messages.removeAt(i);
emit parent->pendingMessageRemoved(removedMessage);
} else {
i++;
}
}
}
debug() << "Dropping first event";
delete incompleteMessages.takeFirst();
}
if (incompleteMessages.isEmpty()) {
if (readinessHelper->requestedFeatures().contains(FeatureMessageQueue) &&
!readinessHelper->isReady(Features() << FeatureMessageQueue)) {
debug() << "incompleteMessages empty for the first time: "
"FeatureMessageQueue is now ready";
readinessHelper->setIntrospectCompleted(FeatureMessageQueue, true);
}
return;
}
// What Contact objects do we need in order to proceed, ignoring those
// for which we've already sent a request?
HandleIdentifierMap contactsRequired;
foreach (const MessageEvent *e, incompleteMessages) {
if (e->isMessage) {
uint handle = e->message.senderHandle();
if (handle != 0 && !e->message.sender()
&& !awaitingContacts.contains(handle)) {
contactsRequired.insert(handle, e->message.senderId());
}
}
}
if (contactsRequired.isEmpty()) {
return;
}
ConnectionPtr conn = parent->connection();
conn->lowlevel()->injectContactIds(contactsRequired);
parent->connect(conn->contactManager()->contactsForHandles(
contactsRequired.keys()),
SIGNAL(finished(Tp::PendingOperation*)),
SLOT(onContactsFinished(Tp::PendingOperation*)));
awaitingContacts |= contactsRequired.keys().toSet();
}
void TextChannel::Private::processChatStateQueue()
{
while (!chatStateQueue.isEmpty()) {
const ChatStateEvent *e = chatStateQueue.first();
debug() << "ChatStateEvent:" << reinterpret_cast(e);
if (e->contact.isNull()) {
// the chat state Contact object wasn't retrieved yet, but needs
// one. We'll have to stop processing here, and come back to it
// when we have more Contact objects
break;
}
chatStates.insert(e->contact, (ChannelChatState) e->state);
// if we reach here, the Contact object is ready
emit parent->chatStateChanged(e->contact, (ChannelChatState) e->state);
debug() << "Dropping first event";
delete chatStateQueue.takeFirst();
}
// What Contact objects do we need in order to proceed, ignoring those
// for which we've already sent a request?
QSet contactsRequired;
foreach (const ChatStateEvent *e, chatStateQueue) {
if (!e->contact &&
!awaitingContacts.contains(e->contactHandle)) {
contactsRequired << e->contactHandle;
}
}
if (contactsRequired.isEmpty()) {
return;
}
// TODO: pass id hints to ContactManager if we ever gain support to retrieve contact ids
// from ChatState.
parent->connect(parent->connection()->contactManager()->contactsForHandles(
contactsRequired.toList()),
SIGNAL(finished(Tp::PendingOperation*)),
SLOT(onContactsFinished(Tp::PendingOperation*)));
awaitingContacts |= contactsRequired;
}
void TextChannel::Private::contactLost(uint handle)
{
// we're not going to get a Contact object for this handle, so mark the
// messages from that handle as "unknown sender"
foreach (MessageEvent *e, incompleteMessages) {
if (e->isMessage && e->message.senderHandle() == handle
&& !e->message.sender()) {
e->message.clearSenderHandle();
}
}
// there is no point in sending chat state notifications for unknown
// contacts, removing chat state events from queue that refer to this handle
foreach (ChatStateEvent *e, chatStateQueue) {
if (e->contactHandle == handle) {
chatStateQueue.removeOne(e);
delete e;
}
}
}
void TextChannel::Private::contactFound(ContactPtr contact)
{
uint handle = contact->handle().at(0);
foreach (MessageEvent *e, incompleteMessages) {
if (e->isMessage && e->message.senderHandle() == handle
&& !e->message.sender()) {
e->message.setSender(contact);
}
}
foreach (ChatStateEvent *e, chatStateQueue) {
if (e->contactHandle == handle) {
e->contact = contact;
}
}
}
/**
* \class TextChannel
* \ingroup clientchannel
* \headerfile TelepathyQt/text-channel.h
*
* \brief The TextChannel class represents a Telepathy channel of type Text.
*
* For more details, please refer to \telepathy_spec.
*
* See \ref async_model, \ref shared_ptr
*/
/**
* Feature representing the core that needs to become ready to make the
* TextChannel object usable.
*
* This is currently the same as Channel::FeatureCore, but may change to include more.
*
* When calling isReady(), becomeReady(), this feature is implicitly added
* to the requested features.
*/
const Feature TextChannel::FeatureCore = Feature(QLatin1String(Channel::staticMetaObject.className()), 0, true);
/**
* Feature used in order to access the message queue info.
*
* See message queue methods' documentation for more details.
*
* \sa messageQueue(), messageReceived(), pendingMessageRemoved(), forget(), acknowledge()
*/
const Feature TextChannel::FeatureMessageQueue = Feature(QLatin1String(TextChannel::staticMetaObject.className()), 0);
/**
* Feature used in order to access message capabilities info.
*
* See message capabilities methods' documentation for more details.
*
* \sa supportedContentTypes(), messagePartSupport(), deliveryReportingSupport()
*/
const Feature TextChannel::FeatureMessageCapabilities = Feature(QLatin1String(TextChannel::staticMetaObject.className()), 1);
/**
* Feature used in order to receive notification when a message is sent.
*
* \sa messageSent()
*/
const Feature TextChannel::FeatureMessageSentSignal = Feature(QLatin1String(TextChannel::staticMetaObject.className()), 2);
/**
* Feature used in order to keep track of chat state changes.
*
* See chat state methods' documentation for more details.
*
* \sa chatState(), chatStateChanged()
*/
const Feature TextChannel::FeatureChatState = Feature(QLatin1String(TextChannel::staticMetaObject.className()), 3);
/**
* \fn void TextChannel::messageSent(const Tp::Message &message,
* Tp::MessageSendingFlags flags,
* const QString &sentMessageToken)
*
* Emitted when a message is sent, if the TextChannel::FeatureMessageSentSignal
* has been enabled.
*
* This signal is emitted regardless of whether the message is sent by this
* client, or another client using the same channel via D-Bus.
*
* \param message A message. This may differ slightly from what the client
* requested to send, for instance if it has been altered due
* to limitations of the instant messaging protocol used.
* \param flags #MessageSendingFlags that were in effect when the message was
* sent. Clients can use these in conjunction with
* deliveryReportingSupport() to determine whether delivery
* reporting can be expected.
* \param sentMessageToken Either an empty QString, or an opaque token used
* to match the message to any delivery reports.
*/
/**
* \fn void TextChannel::messageReceived(const Tp::ReceivedMessage &message)
*
* Emitted when a message is added to messageQueue(), if the
* TextChannel::FeatureMessageQueue Feature has been enabled.
*
* This occurs slightly later than the message being received over D-Bus;
* see messageQueue() for details.
*
* \param message The message received.
* \sa messageQueue(), acknowledge(), forget()
*/
/**
* \fn void TextChannel::pendingMessageRemoved(
* const Tp::ReceivedMessage &message)
*
* Emitted when a message is removed from messageQueue(), if the
* TextChannel::FeatureMessageQueue Feature has been enabled. See messageQueue() for the
* circumstances in which this happens.
*
* \param message The message removed.
* \sa messageQueue(), acknowledge(), forget()
*/
/**
* \fn void TextChannel::chatStateChanged(const Tp::ContactPtr &contact,
* ChannelChatState state)
*
* Emitted when the state of a member of the channel has changed, if the
* TextChannel::FeatureChatState feature has been enabled.
*
* Local state changes are also emitted here.
*
* \param contact The contact whose chat state changed.
* \param state The new chat state for \a contact.
* \sa chatState()
*/
/**
* Create a new TextChannel object.
*
* \param connection Connection owning this channel, and specifying the
* service.
* \param objectPath The channel object path.
* \param immutableProperties The channel immutable properties.
* \return A TextChannelPtr object pointing to the newly created
* TextChannel object.
*/
TextChannelPtr TextChannel::create(const ConnectionPtr &connection,
const QString &objectPath, const QVariantMap &immutableProperties)
{
return TextChannelPtr(new TextChannel(connection, objectPath,
immutableProperties, TextChannel::FeatureCore));
}
/**
* Construct a new TextChannel object.
*
* \param connection Connection owning this channel, and specifying the
* service.
* \param objectPath The channel object path.
* \param immutableProperties The channel immutable properties.
* \param coreFeature The core feature of the channel type, if any. The corresponding introspectable should
* depend on TextChannel::FeatureCore.
*/
TextChannel::TextChannel(const ConnectionPtr &connection,
const QString &objectPath,
const QVariantMap &immutableProperties,
const Feature &coreFeature)
: Channel(connection, objectPath, immutableProperties, coreFeature),
mPriv(new Private(this))
{
}
/**
* Class destructor.
*/
TextChannel::~TextChannel()
{
delete mPriv;
}
/**
* Return whether this channel supports the Messages interface.
*
* If the interface is not supported, some advanced functionality will be unavailable.
*
* This method requires TextChannel::FeatureCore to be ready.
*
* \return \c true if the Messages interface is supported, \c false otherwise.
*/
bool TextChannel::hasMessagesInterface() const
{
return interfaces().contains(TP_QT_IFACE_CHANNEL_INTERFACE_MESSAGES);
}
/**
* Return whether this channel supports the ChatState interface.
*
* If the interface is not supported, requestChatState() will fail and all contacts' chat states
* will appear to be #ChannelChatStateInactive.
*
* This method requires TextChannel::FeatureCore to be ready.
*
* \return \c true if the ChatState interface is supported, \c false otherwise.
* \sa requestChatState(), chatStateChanged()
*/
bool TextChannel::hasChatStateInterface() const
{
return interfaces().contains(TP_QT_IFACE_CHANNEL_INTERFACE_CHAT_STATE);
}
/**
* Return whether contacts can be invited into this channel using
* inviteContacts() (which is equivalent to Channel::groupAddContacts()).
*
* Whether this is the case depends on the underlying protocol, the type of channel,
* and the user's privileges (in some chatrooms, only a privileged user
* can invite other contacts).
*
* This is an alias for Channel::groupCanAddContacts(), to indicate its meaning more
* clearly for Text channels.
*
* This method requires Channel::FeatureCore to be ready.
*
* \return \c true if contacts can be invited, \c false otherwise.
* \sa inviteContacts(), Channel::groupCanAddContacts(), Channel::groupAddContacts()
*/
bool TextChannel::canInviteContacts() const
{
return groupCanAddContacts();
}
/* in the block below is used to escape the star-slash sequence */
/**
* Return a list of supported MIME content types for messages on this channel.
*
* For a simple text channel this will be a list containing one item,
* "text/plain".
*
* This list may contain the special value "*/*", which
* indicates that any content type is supported.
*
* This method requires TextChannel::FeatureMessageCapabilities to be ready.
*
* \return The list of MIME content types.
*/
QStringList TextChannel::supportedContentTypes() const
{
return mPriv->supportedContentTypes;
}
/**
* Return the message types supported by this channel.
*
* This method requires TextChannel::FeatureMessageCapabilities to be ready.
*
* \return The list of supported message types
*/
QList TextChannel::supportedMessageTypes() const
{
if (!isReady(FeatureMessageCapabilities)) {
warning() << "TextChannel::supportedMessageTypes() used with "
"FeatureMessageCapabilities not ready";
}
return mPriv->supportedMessageTypes;
}
/**
* Return whether the provided message type is supported.
*
* This method requires TextChannel::FeatureMessageCapabilities to be ready.
*
* \param messageType The message type to check.
* \return \c true if supported, \c false otherwise
*/
bool TextChannel::supportsMessageType(ChannelTextMessageType messageType) const
{
if (!isReady(FeatureMessageCapabilities)) {
warning() << "TextChannel::supportsMessageType() used with "
"FeatureMessageCapabilities not ready";
}
return mPriv->supportedMessageTypes.contains(messageType);
}
/**
* Return a set of flags indicating support for multi-part messages on this
* channel.
*
* This is zero on simple text channels, or greater than zero if
* there is partial or full support for multi-part messages.
*
* This method requires TextChannel::FeatureMessageCapabilities to be ready.
*
* \return The flags as #MessagePartSupportFlags.
*/
MessagePartSupportFlags TextChannel::messagePartSupport() const
{
return mPriv->messagePartSupport;
}
/**
* Return a set of flags indicating support for delivery reporting on this
* channel.
*
* This is zero if there are no particular guarantees, or greater
* than zero if delivery reports can be expected under certain circumstances.
*
* This method requires TextChannel::FeatureMessageCapabilities to be ready.
*
* \return The flags as #DeliveryReportingSupportFlags.
*/
DeliveryReportingSupportFlags TextChannel::deliveryReportingSupport() const
{
return mPriv->deliveryReportingSupport;
}
/**
* Return a list of messages received in this channel.
*
* Messages are added to this list when they are received from the instant
* messaging service; the messageReceived() signal is emitted.
*
* There is a small delay between the message being received over D-Bus and
* becoming available to users of this C++ API, since a small amount of
* additional information needs to be fetched. However, the relative ordering
* of all the messages in a channel is preserved.
*
* Messages are removed from this list when they are acknowledged with the
* acknowledge() or forget() methods. On channels where hasMessagesInterface()
* returns \c true, they will also be removed when acknowledged by a different
* client. In either case, the pendingMessageRemoved() signal is emitted.
*
* This method requires TextChannel::FeatureMessageQueue to be ready.
*
* \return A list of ReceivedMessage objects.
* \sa messageReceived()
*/
QList TextChannel::messageQueue() const
{
return mPriv->messages;
}
/**
* Return the current chat state for \a contact.
*
* If hasChatStateInterface() returns \c false, this method will always return
* #ChannelChatStateInactive.
*
* This method requires TextChannel::FeatureChatState to be ready.
*
* \return The contact chat state as #ChannelChatState.
*/
ChannelChatState TextChannel::chatState(const ContactPtr &contact) const
{
if (!isReady(FeatureChatState)) {
warning() << "TextChannel::chatState() used with "
"FeatureChatState not ready";
return ChannelChatStateInactive;
}
if (mPriv->chatStates.contains(contact)) {
return mPriv->chatStates.value(contact);
}
return ChannelChatStateInactive;
}
void TextChannel::onAcknowledgePendingMessagesReply(
QDBusPendingCallWatcher *watcher)
{
UIntList ids = mPriv->acknowledgeBatches.value(watcher);
QDBusPendingReply<> reply = *watcher;
if (reply.isError()) {
// One of the IDs was bad, and we can't know which one. Recover by
// doing as much as possible, and hope for the best...
debug() << "Recovering from AcknowledgePendingMessages failure for: "
<< ids;
foreach (uint id, ids) {
mPriv->textInterface->AcknowledgePendingMessages(UIntList() << id);
}
}
mPriv->acknowledgeBatches.remove(watcher);
watcher->deleteLater();
}
/**
* Acknowledge that received messages have been displayed to the user.
*
* Note that this method should only be called by the main handler of a channel, usually
* meaning the user interface process that displays the channel to the user
* (when a channel dispatcher is used, the handler must acknowledge messages,
* and other approvers or observers must not acknowledge messages).
*
* Processes other than the main handler of a channel can free memory used
* by the library by calling forget() instead.
*
* This method requires TextChannel::FeatureMessageQueue to be ready.
*
* \param messages A list of received messages that have now been displayed.
* \sa forget(), messageQueue(), messageReceived(), pendingMessageRemoved()
*/
void TextChannel::acknowledge(const QList &messages)
{
UIntList ids;
foreach (const ReceivedMessage &m, messages) {
if (m.isFromChannel(TextChannelPtr(this))) {
ids << m.pendingId();
} else {
warning() << "message did not come from this channel, ignoring";
}
}
if (ids.isEmpty()) {
return;
}
// we're going to acknowledge these messages (or as many as possible, if
// we lose a race with another acknowledging process), so let's remove
// them from the list immediately
forget(messages);
QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(
mPriv->textInterface->AcknowledgePendingMessages(ids),
this);
connect(watcher,
SIGNAL(finished(QDBusPendingCallWatcher*)),
SLOT(onAcknowledgePendingMessagesReply(QDBusPendingCallWatcher*)));
mPriv->acknowledgeBatches[watcher] = ids;
}
/**
* Remove messages from the message queue without acknowledging them.
*
* Note that this method frees memory used by the library, but
* does not free the corresponding memory in the CM process.
* It should be used by clients that are not the main handler for a channel;
* the main handler for a channel should use acknowledge() instead.
*
* This method requires TextChannel::FeatureMessageQueue to be ready.
*
* \param messages A list of received messages that have now been processed.
* \sa acknowledge(), messageQueue(), messageReceived(), pendingMessageRemoved()
*/
void TextChannel::forget(const QList &messages)
{
foreach (const ReceivedMessage &m, messages) {
if (!m.isFromChannel(TextChannelPtr(this))) {
warning() << "message did not come from this channel, ignoring";
} else if (mPriv->messages.removeOne(m)) {
emit pendingMessageRemoved(m);
}
}
}
/**
* Request that a message be sent on this channel.
*
* When the message has been submitted for delivery,
* this method will return and the messageSent() signal will be emitted.
*
* If the message cannot be submitted for delivery, the returned pending operation will fail and no
* signal is emitted.
*
* This method requires TextChannel::FeatureCore to be ready.
*
* \param text The message body.
* \param type The message type.
* \param flags Flags affecting how the message is sent.
* Note that the channel may ignore some or all flags, depending on
* deliveryReportingSupport(); the flags that were handled by the CM are provided in
* messageSent().
* \return A PendingOperation which will emit PendingOperation::finished
* when the message has been submitted for delivery.
* \sa messageSent()
*/
PendingSendMessage *TextChannel::send(const QString &text,
ChannelTextMessageType type, MessageSendingFlags flags)
{
Message m(type, text);
PendingSendMessage *op = new PendingSendMessage(TextChannelPtr(this), m);
if (hasMessagesInterface()) {
Client::ChannelInterfaceMessagesInterface *messagesInterface =
interface();
connect(new QDBusPendingCallWatcher(
messagesInterface->SendMessage(m.parts(),
(uint) flags)),
SIGNAL(finished(QDBusPendingCallWatcher*)),
op,
SLOT(onMessageSent(QDBusPendingCallWatcher*)));
} else {
connect(new QDBusPendingCallWatcher(mPriv->textInterface->Send(type, text)),
SIGNAL(finished(QDBusPendingCallWatcher*)),
op,
SLOT(onTextSent(QDBusPendingCallWatcher*)));
}
return op;
}
/**
* Request that a message be sent on this channel.
*
* When the message has been submitted for delivery,
* this method will return and the messageSent() signal will be emitted.
*
* If the message cannot be submitted for delivery, the returned pending operation will fail and no
* signal is emitted.
*
* This method requires TextChannel::FeatureCore to be ready.
*
* \param parts The message parts.
* \param flags Flags affecting how the message is sent.
* Note that the channel may ignore some or all flags, depending on
* deliveryReportingSupport(); the flags that were handled by the CM are provided in
* messageSent().
* \return A PendingOperation which will emit PendingOperation::finished
* when the message has been submitted for delivery.
* \sa messageSent()
*/
PendingSendMessage *TextChannel::send(const MessagePartList &parts,
MessageSendingFlags flags)
{
Message m(parts);
PendingSendMessage *op = new PendingSendMessage(TextChannelPtr(this), m);
if (hasMessagesInterface()) {
Client::ChannelInterfaceMessagesInterface *messagesInterface =
interface();
connect(new QDBusPendingCallWatcher(
messagesInterface->SendMessage(m.parts(),
(uint) flags)),
SIGNAL(finished(QDBusPendingCallWatcher*)),
op,
SLOT(onMessageSent(QDBusPendingCallWatcher*)));
} else {
connect(new QDBusPendingCallWatcher(mPriv->textInterface->Send(
m.messageType(), m.text())),
SIGNAL(finished(QDBusPendingCallWatcher*)),
op,
SLOT(onTextSent(QDBusPendingCallWatcher*)));
}
return op;
}
/**
* Set the local chat state and notify other members of the channel that it has
* changed.
*
* Note that only the primary handler of the channel should set its chat
* state.
*
* This method requires TextChannel::FeatureCore to be ready.
*
* \param state The new state.
* \sa chatStateChanged()
*/
PendingOperation *TextChannel::requestChatState(ChannelChatState state)
{
if (!interfaces().contains(TP_QT_IFACE_CHANNEL_INTERFACE_CHAT_STATE)) {
warning() << "TextChannel::requestChatState() used with no chat "
"state interface";
return new PendingFailure(TP_QT_ERROR_NOT_IMPLEMENTED,
QLatin1String("TextChannel does not support chat state interface"),
TextChannelPtr(this));
}
Client::ChannelInterfaceChatStateInterface *chatStateInterface =
interface();
return new PendingVoid(chatStateInterface->SetChatState(
(uint) state), TextChannelPtr(this));
}
void TextChannel::onMessageSent(const MessagePartList &parts,
uint flags,
const QString &sentMessageToken)
{
emit messageSent(Message(parts), MessageSendingFlag(flags),
sentMessageToken);
}
void TextChannel::onContactsFinished(PendingOperation *op)
{
PendingContacts *pc = qobject_cast(op);
UIntList failed;
Q_ASSERT(pc->isForHandles());
foreach (uint handle, pc->handles()) {
mPriv->awaitingContacts -= handle;
}
if (pc->isError()) {
warning().nospace() << "Gathering contacts failed: "
<< pc->errorName() << ": " << pc->errorMessage();
foreach (uint handle, pc->handles()) {
mPriv->contactLost(handle);
}
} else {
foreach (const ContactPtr &contact, pc->contacts()) {
mPriv->contactFound(contact);
}
foreach (uint handle, pc->invalidHandles()) {
mPriv->contactLost(handle);
}
}
// all contacts for messages and chat state events we were asking about
// should now be ready
mPriv->processMessageQueue();
mPriv->processChatStateQueue();
}
void TextChannel::onMessageReceived(const MessagePartList &parts)
{
if (!mPriv->initialMessagesReceived) {
return;
}
mPriv->incompleteMessages << new Private::MessageEvent(
ReceivedMessage(parts, TextChannelPtr(this)));
mPriv->processMessageQueue();
}
void TextChannel::onPendingMessagesRemoved(const UIntList &ids)
{
if (!mPriv->initialMessagesReceived) {
return;
}
foreach (uint id, ids) {
mPriv->incompleteMessages << new Private::MessageEvent(id);
}
mPriv->processMessageQueue();
}
void TextChannel::onTextSent(uint timestamp, uint type, const QString &text)
{
emit messageSent(Message(timestamp, type, text), nullptr,
QLatin1String(""));
}
void TextChannel::onTextReceived(uint id, uint timestamp, uint sender,
uint type, uint flags, const QString &text)
{
if (!mPriv->initialMessagesReceived) {
return;
}
MessagePart header;
if (timestamp == 0) {
#if QT_VERSION < QT_VERSION_CHECK(5, 8, 0)
timestamp = QDateTime::currentDateTime().toTime_t();
#else
timestamp = QDateTime::currentDateTime().toSecsSinceEpoch();
#endif
}
header.insert(QLatin1String("message-received"),
QDBusVariant(static_cast(timestamp)));
header.insert(QLatin1String("pending-message-id"), QDBusVariant(id));
header.insert(QLatin1String("message-sender"), QDBusVariant(sender));
header.insert(QLatin1String("message-type"), QDBusVariant(type));
if (flags & ChannelTextMessageFlagScrollback) {
header.insert(QLatin1String("scrollback"), QDBusVariant(true));
}
if (flags & ChannelTextMessageFlagRescued) {
header.insert(QLatin1String("rescued"), QDBusVariant(true));
}
MessagePart body;
body.insert(QLatin1String("content-type"),
QDBusVariant(QLatin1String("text/plain")));
body.insert(QLatin1String("content"), QDBusVariant(text));
if (flags & ChannelTextMessageFlagTruncated) {
header.insert(QLatin1String("truncated"), QDBusVariant(true));
}
MessagePartList parts;
parts << header;
parts << body;
ReceivedMessage m(parts, TextChannelPtr(this));
if (flags & ChannelTextMessageFlagNonTextContent) {
// set the "you are not expected to understand this" flag
m.setForceNonText();
}
mPriv->incompleteMessages << new Private::MessageEvent(m);
mPriv->processMessageQueue();
}
void TextChannel::onTextSendError(uint error, uint timestamp, uint type,
const QString &text)
{
if (!mPriv->initialMessagesReceived) {
return;
}
MessagePart header;
header.insert(QLatin1String("message-received"),
QDBusVariant(static_cast(
#if QT_VERSION_CHECK(5, 8, 0)
QDateTime::currentDateTime().toTime_t())));
#else
QDateTime::currentDateTime().toSecsSinceEpoch())));
#endif
header.insert(QLatin1String("message-type"),
QDBusVariant(static_cast(
ChannelTextMessageTypeDeliveryReport)));
// we can't tell whether it's a temporary or permanent failure here,
// so guess based on the delivery-error
uint deliveryStatus;
switch (error) {
case ChannelTextSendErrorOffline:
case ChannelTextSendErrorPermissionDenied:
deliveryStatus = DeliveryStatusTemporarilyFailed;
break;
case ChannelTextSendErrorInvalidContact:
case ChannelTextSendErrorTooLong:
case ChannelTextSendErrorNotImplemented:
deliveryStatus = DeliveryStatusPermanentlyFailed;
break;
case ChannelTextSendErrorUnknown:
default:
deliveryStatus = DeliveryStatusTemporarilyFailed;
break;
}
header.insert(QLatin1String("delivery-status"),
QDBusVariant(deliveryStatus));
header.insert(QLatin1String("delivery-error"), QDBusVariant(error));
MessagePart echoHeader;
echoHeader.insert(QLatin1String("message-sent"),
QDBusVariant(timestamp));
echoHeader.insert(QLatin1String("message-type"),
QDBusVariant(type));
MessagePart echoBody;
echoBody.insert(QLatin1String("content-type"),
QDBusVariant(QLatin1String("text/plain")));
echoBody.insert(QLatin1String("content"), QDBusVariant(text));
MessagePartList echo;
echo << echoHeader;
echo << echoBody;
header.insert(QLatin1String("delivery-echo"),
QDBusVariant(QVariant::fromValue(echo)));
MessagePartList parts;
parts << header;
}
void TextChannel::gotProperties(QDBusPendingCallWatcher *watcher)
{
Q_ASSERT(mPriv->getAllInFlight);
mPriv->getAllInFlight = false;
mPriv->gotProperties = true;
QDBusPendingReply reply = *watcher;
if (reply.isError()) {
warning().nospace() << "Properties::GetAll(Channel.Interface.Messages)"
" failed with " << reply.error().name() << ": " <<
reply.error().message();
ReadinessHelper *readinessHelper = mPriv->readinessHelper;
if (readinessHelper->requestedFeatures().contains(FeatureMessageQueue) &&
!readinessHelper->isReady(Features() << FeatureMessageQueue)) {
readinessHelper->setIntrospectCompleted(FeatureMessageQueue, false, reply.error());
}
if (readinessHelper->requestedFeatures().contains(FeatureMessageCapabilities) &&
!readinessHelper->isReady(Features() << FeatureMessageCapabilities)) {
readinessHelper->setIntrospectCompleted(FeatureMessageCapabilities, false, reply.error());
}
return;
}
debug() << "Properties::GetAll(Channel.Interface.Messages) returned";
mPriv->props = reply.value();
mPriv->updateInitialMessages();
mPriv->updateCapabilities();
watcher->deleteLater();
}
void TextChannel::gotPendingMessages(QDBusPendingCallWatcher *watcher)
{
Q_ASSERT(!mPriv->initialMessagesReceived);
mPriv->initialMessagesReceived = true;
QDBusPendingReply reply = *watcher;
if (reply.isError()) {
warning().nospace() << "Properties::GetAll(Channel.Interface.Messages)"
" failed with " << reply.error().name() << ": " <<
reply.error().message();
// TODO should we fail here?
mPriv->readinessHelper->setIntrospectCompleted(FeatureMessageQueue, false, reply.error());
return;
}
debug() << "Text::ListPendingMessages returned";
PendingTextMessageList list = reply.value();
if (!list.isEmpty()) {
foreach (const PendingTextMessage &message, list) {
onTextReceived(message.identifier, message.unixTimestamp,
message.sender, message.messageType, message.flags,
message.text);
}
// processMessageQueue sets FeatureMessageQueue ready when the queue is empty for the first
// time
} else {
mPriv->readinessHelper->setIntrospectCompleted(FeatureMessageQueue, true);
}
watcher->deleteLater();
}
void TextChannel::onChatStateChanged(uint contactHandle, uint state)
{
mPriv->chatStateQueue.append(new Private::ChatStateEvent(
contactHandle, state));
mPriv->processChatStateQueue();
}
} // Tp