2
* sxemanager.cpp - Whiteboard manager
3
* Copyright (C) 2006 Joonas Govenius
5
* This program is free software; you can redistribute it and/or
6
* modify it under the terms of the GNU General Public License
7
* as published by the Free Software Foundation; either version 2
8
* of the License, or (at your option) any later version.
10
* This program is distributed in the hope that it will be useful,
11
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
* GNU General Public License for more details.
15
* You should have received a copy of the GNU General Public License
16
* along with this library; if not, write to the Free Software
17
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
21
#include "sxemanager.h"
23
#include "psioptions.h"
29
//----------------------------------------------------------------------------
31
//----------------------------------------------------------------------------
33
SxeManager::SxeManager(Client* client, PsiAccount* pa) : client_(client) {
34
sxeId_ = QTime::currentTime().toString("z").toInt();
37
connect(client_, SIGNAL(messageReceived(const Message &)), SLOT(messageReceived(const Message &)));
38
connect(client_, SIGNAL(groupChatLeft(const Jid &)), SLOT(groupChatLeft(const Jid &)));
39
// connect(client_, SIGNAL(groupChatJoined(const Jid &, const Jid &)), SLOT(groupChatJoined(const Jid &, const Jid &)));
41
negotiationTimer_.setSingleShot(true);
42
negotiationTimer_.setInterval(120000);
43
connect(&negotiationTimer_, SIGNAL(timeout()), SLOT(negotiationTimeout()));
46
void SxeManager::addInvitationCallback(bool (*callback)(const Jid &peer, const QList<QString> &features)) {
47
invitationCallbacks_ += callback;
50
void SxeManager::messageReceived(const Message &message) {
51
// only process messages that contain a <sxe/> with a nonempty
52
// 'session' attribute and that are addressed to this particular account
53
if(!message.sxe().attribute("session").isEmpty()) {
55
// skip messages from self
56
if(ownJids_.contains(message.from().full())) {
61
// // Don't process delayed messages (chat history) but remember the session id
62
// if(!message.spooled()) {
64
// Check if the <sxe/> contains a <negotiation/>
65
if(message.sxe().elementsByTagName("negotiation").length() > 0) {
66
processNegotiationMessage(message);
67
// processNegotiationMessage() will also pass regular SXE edits in the message
68
// to the session so we're done
72
// otherwise, try finding a matching session for the session if new one not negotiated
73
SxeSession* w = findSession(message.sxe().attribute("session"));
76
// pass the message to the session if already established
77
w->processIncomingSxeElement(message.sxe(), message.from());
79
// otherwise record the session id as a "detected session"
80
recordDetectedSession(message);
84
// recordDetectedSession(message);
90
void SxeManager::recordDetectedSession(const Message &message) {
91
// check if a record of the session exists
92
foreach(DetectedSession d, DetectedSessions_) {
93
if(d.session == message.sxe().attribute("session")
94
&& d.jid.compare(message.from(), message.type() != "groupchat"))
98
// store a record of a detected session
99
DetectedSession detected;
100
detected.session = message.sxe().attribute("session");
101
if(message.type() == "groupchat")
102
detected.jid = message.from().bare();
104
detected.jid = message.from();
105
detected.time = QTime::currentTime();
106
DetectedSessions_.append(detected);
109
void SxeManager::removeSession(SxeSession* session) {
110
sessions_.removeAll(session);
112
// cancel possible negotiations
113
foreach(SxeNegotiation* negotiation, negotiations_.values(session->session())) {
114
if(negotiation->target.compare(session->target(), true))
115
abortNegotiation(negotiation);
120
QDomElement sxe = doc.createElementNS(SXDENS, "sxe");
121
sxe.setAttribute("session", session->session());
122
QDomElement negotiation = doc.createElementNS(SXDENS, "negotiation");
123
negotiation.appendChild(doc.createElementNS(SXDENS, "left-session"));
124
sxe.appendChild(negotiation);
125
sendSxe(sxe, session->target(), session->groupChat());
127
// delete the session
128
session->deleteLater();
131
bool SxeManager::processNegotiationAsParticipant(const QDomNode &negotiationElement, SxeNegotiation* negotiation, QDomNode response) {
132
QDomDocument doc = QDomDocument();
134
if(negotiationElement.nodeName() == "left-session") {
136
if(negotiation->state == SxeNegotiation::Finished)
137
emit negotiation->session->peerLeftSession(negotiation->peer);
139
} else if(negotiationElement.nodeName() == "abort-negotiation") {
141
if(negotiation->state < SxeNegotiation::HistoryOffered
142
&& negotiation->state != SxeNegotiation::DocumentBegan) {
143
// Abort, as in delete session, if still establishing it and not trying to create a new groupchat session
144
if(!(negotiation->groupChat && negotiation->peer.resource().isEmpty())) {
145
removeNegotiation(negotiation);
149
// Just remove the "negotation" but keep the session
150
negotiation->state = SxeNegotiation::Finished;
153
} else if(negotiationElement.nodeName() == "connect-request"
154
&& negotiation->state == SxeNegotiation::Finished) {
156
// accept all <connect-request/>'s automatically
157
// if currently not negotiating with someone else
158
negotiation->state = SxeNegotiation::HistoryOffered;
159
response.appendChild(doc.createElementNS(SXDENS, "history-offer"));
161
} else if((negotiationElement.nodeName() == "accept-history"
162
&& negotiation->state == SxeNegotiation::HistoryOffered)
163
|| (negotiationElement.nodeName() == "accept-invitation"
164
&& negotiation->state == SxeNegotiation::InvitationSent)) {
166
// If this is a new session (negotiation->state == SxeNegotiation::HistoryOffered),
167
// create a new SxeSession
168
if(!negotiation->session) {
169
negotiation->session = createSxeSession(negotiation->target, negotiation->sessionId, negotiation->ownJid, negotiation->groupChat, negotiation->features);
170
negotiation->session->initializeDocument(negotiation->initialDoc);
173
if(!negotiation->session) {
174
// Creating a new session failed for some reason.
175
abortNegotiation(negotiation);
179
// Retrieve all the edits to the session so far and start queueing new edits
180
QList<const SxeEdit*> snapshot = negotiation->session->startQueueing();
182
// append <document-begin/>
183
QDomElement documentBegin = doc.createElementNS(SXDENS, "document-begin");
184
response.appendChild(documentBegin);
185
QString prolog = SxeSession::parseProlog(negotiation->session->document());
186
if(!prolog.isEmpty()) {
187
QUrl::encode(prolog);
188
documentBegin.setAttribute("prolog", QString("data:text/xml,%1").arg(prolog));
190
response.appendChild(documentBegin);
192
// append all the SxeEdit's returned by startQueueing()
193
foreach(const SxeEdit* e, snapshot) {
194
response.appendChild(e->xml(doc));
197
// append <documend-end/>
198
QDomElement documentEnd = doc.createElementNS(SXDENS, "document-end");
199
documentEnd.setAttribute("last-sender", negotiation->session->lastSxe()["sender"]);
200
documentEnd.setAttribute("last-id", negotiation->session->lastSxe()["id"]);
201
response.appendChild(documentEnd);
203
// Need to "flush" the sxe here before stopping queueing
204
if(response.hasChildNodes()) {
205
QDomElement sxe = doc.createElementNS(SXDENS, "sxe");
206
sxe.setAttribute("session", negotiation->sessionId);
207
sxe.appendChild(response);
208
sendSxe(sxe.toElement(), negotiation->peer, negotiation->groupChat);
209
sxe.removeChild(response);
211
while(response.hasChildNodes())
212
response.removeChild(response.firstChild());
214
// we're all set and can stop queueing new edits to the session
215
negotiation->state = SxeNegotiation::Finished;
216
negotiation->session->stopQueueing();
218
// signal that a peer joined
219
emit negotiation->session->peerJoinedSession(negotiation->peer);
221
} else if(negotiationElement.nodeName() == "decline-invitation" && negotiation->state == SxeNegotiation::InvitationSent) {
223
QDomNodeList alternatives = negotiationElement.toElement().elementsByTagName("alternative-session");
224
for(int i = 0; i < alternatives.size(); i++) {
225
emit alternativeSession(negotiation->target, alternatives.at(i).toElement().text());
228
if(!negotiation->groupChat || alternatives.size() > 0) {
229
abortNegotiation(negotiation);
238
bool SxeManager::processNegotiationAsJoiner(const QDomNode &negotiationElement, SxeNegotiation* negotiation, QDomNode response, const Message &message) {
239
QDomDocument doc = QDomDocument();
241
if(negotiationElement.nodeName() == "abort-negotiation") {
243
// Abort, as in delete session, if not trying to join a groupchat session
244
if(!(negotiation->groupChat && negotiation->peer.resource().isEmpty())) {
245
removeNegotiation(negotiation);
249
} else if(negotiationElement.nodeName() == "invitation"
250
&& negotiation->state == SxeNegotiation::NotStarted) {
252
// copy the feature strings to negotiation-features
253
for(uint k = 0; k < negotiationElement.childNodes().length(); k++) {
254
if(negotiationElement.childNodes().at(k).nodeName() == "feature") {
255
negotiation->features += negotiationElement.childNodes().at(k).toElement().text();
259
// check if one of the invitation callbacks accepts the invitation.
260
foreach(bool (*callback)(const Jid &peer, const QList<QString> &features), invitationCallbacks_) {
261
if(callback(negotiation->peer, negotiation->features)) {
262
response.appendChild(doc.createElementNS(SXDENS, "accept-invitation"));
263
negotiation->state = SxeNegotiation::InvitationAccepted;
267
// othewise abort negotiation
268
abortNegotiation(negotiation);
271
} else if(negotiationElement.nodeName() == "history-offer"
272
&& negotiation->state == SxeNegotiation::ConnectionRequested) {
274
// accept the first <history-offer/> that arrives in response to a <connect-request/>
275
negotiation->state = SxeNegotiation::HistoryAccepted;
276
negotiation->peer = message.from();
277
response.appendChild(doc.createElementNS(SXDENS, "accept-history"));
279
} else if(negotiationElement.nodeName() == "document-begin"
280
&& (negotiation->state == SxeNegotiation::HistoryAccepted
281
|| negotiation->state == SxeNegotiation::InvitationAccepted)) {
283
// Create the new SxeSession
284
if(!negotiation->session) {
285
negotiation->session = createSxeSession(negotiation->target, negotiation->sessionId, negotiation->ownJid, negotiation->groupChat, negotiation->features);
288
if(negotiation->session) {
289
// set the session to "importing" state which bypasses some version control
291
if(negotiationElement.toElement().hasAttribute("prolog")) {
292
QString prolog = negotiationElement.toElement().attribute("prolog");
293
if(prolog.startsWith("data:")) {
294
// Assuming non-base64
295
prolog = prolog.mid(prolog.indexOf(",") + 1);
296
QUrl::decode(prolog);
297
doc.setContent(prolog);
301
negotiation->session->setImporting(true, doc);
302
negotiation->state = SxeNegotiation::DocumentBegan;
304
// creating the session failed for some reason
305
abortNegotiation(negotiation);
308
} else if(negotiationElement.nodeName() != "document-end"
309
&& negotiation->state == SxeNegotiation::DocumentBegan) {
311
// pass the edit to the session
312
QDomElement sxe = doc.createElementNS(SXDENS, "sxe");
313
sxe.setAttribute("session", negotiation->sessionId);
314
sxe.appendChild(negotiationElement.cloneNode());
315
negotiation->session->processIncomingSxeElement(sxe, negotiation->peer);
317
} else if(negotiationElement.nodeName() == "document-end"
318
&& negotiation->state == SxeNegotiation::DocumentBegan) {
320
// The initial document has been received and we're done
321
negotiation->state = SxeNegotiation::Finished;
323
negotiation->session->eraseQueueUntil(negotiationElement.toElement().attribute("last-sender"),
324
negotiationElement.toElement().attribute("last-id"));
326
// Exit the "importing" state so that normal version control resumes
327
negotiation->session->setImporting(false);
334
QPointer<SxeSession> SxeManager::processNegotiationMessage(const Message &message) {
336
if(PsiOptions::instance()->getOption("options.messages.ignore-non-roster-contacts").toBool() && message.type() != "groupchat") {
337
// Ignore the message if contact not in roster
338
if(!pa_->find(message.from())) {
339
qDebug("SXE invitation received from contact that is not in roster.");
344
// Find or create a negotiation object
345
SxeNegotiation* negotiation = findNegotiation(message.from(), message.sxe().attribute("session"));
348
// Only accept further negotiation messages from the source we are already negotiationing with or if we've requested connection to a groupchat session
349
if(!negotiation->peer.compare(message.from()) && !(negotiation->groupChat && negotiation->peer.resource().isEmpty())) {
350
abortNegotiation(negotiation->sessionId, message.from(), true);
355
negotiation = createNegotiation(message);
357
// Prepare the response <sxe/>
359
QDomElement sxe = doc.createElementNS(SXDENS, "sxe");
360
sxe.setAttribute("session", negotiation->sessionId);
361
QDomElement response = doc.createElementNS(SXDENS, "negotiation");
363
// Process each child of the <sxe/>
365
for(int i = 0; i < message.sxe().childNodes().count(); i++) {
366
n = message.sxe().childNodes().at(i);
372
if(n.nodeName() == "negotiation") {
374
// Process each child element of <negotiation/>
375
for(int j = 0; j < n.childNodes().count(); j++) {
377
if(negotiation->role == SxeNegotiation::Participant) {
378
if(!processNegotiationAsParticipant(n.childNodes().at(j), negotiation, response))
380
} else if(negotiation->role == SxeNegotiation::Joiner) {
381
if(!processNegotiationAsJoiner(n.childNodes().at(j), negotiation, response, message))
386
// Send any responses that were generated
387
if(response.hasChildNodes()) {
388
sxe.appendChild(response);
389
sendSxe(sxe.cloneNode().toElement(), negotiation->peer, negotiation->groupChat);
390
sxe.removeChild(response);
393
while(response.hasChildNodes())
394
response.removeChild(response.firstChild());
397
} else if(negotiation->state == SxeNegotiation::Finished
398
&& negotiation->session) {
400
// If in finished state,
401
// pass the edits to the session normally
403
negotiation->session->processIncomingSxeElement(sxe, negotiation->peer);
411
// Delete negotation objects that are no longer needed
414
if(negotiation->state == SxeNegotiation::NotStarted) {
416
removeNegotiation(negotiation);
418
} else if(negotiation->state == SxeNegotiation::Finished) {
420
// Save session for successful negotiations but delete the negotiation object
421
SxeSession* sxesession = negotiation->session;
423
if(!sessions_.contains(sxesession)) {
425
// store and emit a signal about the session only if it's new
426
sessions_.append(sxesession);
427
emit sessionNegotiated(sxesession);
432
removeNegotiation(negotiation);
434
// return a handle to the session
442
SxeManager::SxeNegotiation* SxeManager::findNegotiation(const Jid &jid, const QString &session) {
443
QList<SxeNegotiation*> negotiations = negotiations_.values(session);
444
foreach(SxeNegotiation* negotiation, negotiations) {
445
if(negotiation->state != SxeNegotiation::Aborted
446
&& negotiation->peer.compare(jid, negotiation->state != SxeNegotiation::ConnectionRequested))
453
void SxeManager::removeNegotiation(SxeNegotiation* negotiation) {
454
negotiations_.remove(negotiation->sessionId, negotiation);
458
SxeManager::SxeNegotiation* SxeManager::createNegotiation(SxeNegotiation::Role role, SxeNegotiation::State state, const QString &sessionId, const Jid &target, const Jid &ownJid, bool groupChat) {
459
SxeNegotiation* negotiation = new SxeNegotiation;
460
negotiation->role = role;
461
negotiation->state = state;
462
negotiation->sessionId = sessionId;
463
negotiation->target = target;
464
negotiation->peer = target;
465
negotiation->ownJid = ownJid;
466
negotiation->groupChat = groupChat;
467
negotiation->session = 0;
469
negotiations_.insert(sessionId, negotiation);
474
SxeManager::SxeNegotiation* SxeManager::createNegotiation(const Message &message) {
476
// Create a new negotiation object
478
SxeNegotiation* negotiation = new SxeNegotiation;
480
negotiation->sessionId = message.sxe().attribute("session");
481
negotiation->session = findSession(negotiation->sessionId);
483
negotiation->peer = message.from();
485
if(negotiation->session) {
487
// If negotiation exists, we're going to be the "server" for the negotiation
488
negotiation->role = SxeNegotiation::Participant;
489
negotiation->state = SxeNegotiation::Finished;
490
negotiation->target = negotiation->session->target();
491
negotiation->groupChat = negotiation->session->groupChat();
492
negotiation->ownJid = negotiation->session->ownJid();
493
negotiation->features = negotiation->session->features();
497
// Otherwise we're joining a session
498
negotiation->role = SxeNegotiation::Joiner;
499
negotiation->state = SxeNegotiation::NotStarted;
501
if(message.type() == "groupchat") {
503
// If we're being invited from a groupchat,
504
// ownJid is determined based on the bare part of ownJids_
506
negotiation->groupChat = true;
507
foreach(QString j, ownJids_) {
508
if(message.from().bare() == j.left(j.indexOf("/"))) {
509
negotiation->ownJid = j;
513
// Also, the target is just the bare JID in a groupchat
514
negotiation->target = message.from().bare();
518
negotiation->groupChat = false;
519
negotiation->target = negotiation->peer;
523
if(negotiation->ownJid.isEmpty())
524
negotiation->ownJid = message.to();
529
negotiationTimer_.start();
532
negotiations_.insert(negotiation->sessionId, negotiation);
537
void SxeManager::joinSession(const Jid &target, const Jid &ownJid, bool groupChat, const QString &session) {
538
// Prepare the <connect-request/>
540
QDomElement sxe = doc.createElementNS(SXDENS, "sxe");
541
sxe.setAttribute("session", session);
542
QDomElement negotiationElement = doc.createElementNS(SXDENS, "negotiation");
543
QDomElement request = doc.createElementNS(SXDENS, "connect-request");
544
negotiationElement.appendChild(request);
545
sxe.appendChild(negotiationElement);
547
// Create the negotiation object
548
createNegotiation(SxeNegotiation::Joiner, SxeNegotiation::ConnectionRequested, session, target, ownJid, groupChat);
550
sendSxe(sxe, target, groupChat);
552
// Reset the timeout for negotiations
553
negotiationTimer_.start();
558
void SxeManager::startNewSession(const Jid &target, const Jid &ownJid, bool groupChat, const QDomDocument &initialDoc, QList<QString> features) {
560
// generate a session identifier
563
session = SxeSession::generateUUID();
564
} while (findSession(session));
566
if(features.size() == 0) {
567
// some features must be specified
571
// Prepare the <invitation/>
573
QDomElement sxe = doc.createElementNS(SXDENS, "sxe");
574
sxe.setAttribute("session", session);
575
QDomElement negotiationElement = doc.createElementNS(SXDENS, "negotiation");
576
QDomElement request = doc.createElementNS(SXDENS, "invitation");
577
QDomElement feature = doc.createElementNS(SXDENS, "feature");
578
foreach(QString f, features) {
579
feature = feature.cloneNode(false).toElement();
580
feature.appendChild(doc.createTextNode(f));
581
request.appendChild(feature);
583
negotiationElement.appendChild(request);
584
sxe.appendChild(negotiationElement);
586
// Create the negotiation object
587
SxeNegotiation* negotiation = createNegotiation(SxeNegotiation::Participant, SxeNegotiation::InvitationSent, session, target, ownJid, groupChat);
588
negotiation->initialDoc = initialDoc;
589
negotiation->features = features;
591
sendSxe(sxe, target, groupChat);
593
// Reset the timeout for negotiations
594
negotiationTimer_.start();
599
void SxeManager::negotiationTimeout() {
600
foreach(SxeNegotiation* negotiation, negotiations_.values()){
601
if(negotiation->role == SxeNegotiation::Participant && negotiation->state < SxeNegotiation::HistoryOffered && negotiation->state != SxeNegotiation::DocumentBegan) {
602
if(negotiation->session)
603
negotiation->session->endSession();
604
abortNegotiation(negotiation->sessionId, negotiation->peer, negotiation->groupChat);
609
negotiations_.clear();
612
// #include <QTextStream>
613
void SxeManager::sendSxe(QDomElement sxe, const Jid & receiver, bool groupChat) {
615
// Add a unique id to each sent sxe element
617
sxe.setAttribute("id", sxeId_);
619
SxeSession* session = qobject_cast<SxeSession*>(sender());
621
session->setLastSxe(session->ownJid().full(), QString("%1").arg(sxeId_));
625
if(groupChat && receiver.resource().isEmpty())
626
m.setType("groupchat");
628
if(client_->isActive()) {
629
// send queued messages first
630
while(!queuedMessages_.isEmpty())
631
client_->sendMessage(queuedMessages_.takeFirst());
633
client_->sendMessage(m);
635
queuedMessages_.append(m);
639
QList< QPointer<SxeSession> > SxeManager::findSession(const Jid &jid) {
640
// find if a session for the jid already exists
641
QList< QPointer<SxeSession> > matching;
642
foreach(QPointer<SxeSession> w, sessions_) {
643
// does the jid match?
644
if(w->target().compare(jid)) {
651
QPointer<SxeSession> SxeManager::findSession(const QString &session) {
652
// find if a session for the session already exists
653
foreach(SxeSession* w, sessions_) {
654
// does the session match?
655
if(w->session() == session)
661
QPointer<SxeSession> SxeManager::createSxeSession(const Jid &target, QString session, const Jid &ownJid, bool groupChat, const QList<QString> &features) {
662
if(session.isEmpty() || !target.isValid())
664
if(!ownJids_.contains(ownJid.full()))
665
ownJids_.append(ownJid.full());
666
// FIXME: detect serverside support
667
bool serverSupport = false;
668
// create the SxeSession
669
QPointer<SxeSession> w = new SxeSession(target, session, ownJid, groupChat, serverSupport, features);
670
// connect the signals
671
connect(w, SIGNAL(newSxeElement(QDomElement, Jid, bool)), SLOT(sendSxe(const QDomElement &, const Jid &, bool)));
672
connect(w, SIGNAL(sessionEnded(SxeSession*)), SLOT(removeSession(SxeSession*)));
673
removeDetectedSession(w);
675
// Note: the session should be added to sessions_ once negotiation is finished
678
void SxeManager::abortNegotiation(QString session, const Jid &peer, bool groupChat) {
679
QDomDocument doc = QDomDocument();
680
QDomElement sxe = doc.createElementNS(SXDENS, "sxe");
681
sxe.setAttribute("session", session);
682
QDomElement negotiationElement = doc.createElementNS(SXDENS, "negotiation");
683
negotiationElement.appendChild(doc.createElementNS(SXDENS, "abort-negotiation"));
684
sxe.appendChild(negotiationElement);
685
sendSxe(sxe, peer, groupChat);
688
void SxeManager::abortNegotiation(SxeNegotiation* negotiation) {
689
abortNegotiation(negotiation->sessionId, negotiation->peer, negotiation->groupChat);
691
removeNegotiation(negotiation);
694
void SxeManager::removeDetectedSession(SxeSession* session) {
695
for(int i = 0; i < DetectedSessions_.size(); i++) {
696
DetectedSession detected = DetectedSessions_.at(i);
697
// Remove the specified session from the list
698
if(detected.session == session->session() && detected.jid.compare(session->target(), true))
699
DetectedSessions_.removeAt(i);
700
else if(detected.time.secsTo(QTime::currentTime()) > 1800)
701
// Remove detected session that are old
702
DetectedSessions_.removeAt(i);
706
void SxeManager::groupChatLeft(const Jid &jid) {
707
for(int i = 0; i < ownJids_.size(); i++) {
708
if(jid.bare() == ownJids_.at(i).left(ownJids_.at(i).indexOf("/")))
709
ownJids_.removeAt(i);
711
QList< QPointer<SxeSession> > matching = findSession(jid);
712
foreach(QPointer<SxeSession> w, matching)
716
void SxeManager::groupChatJoined(const Jid &, const Jid &ownJid) {
717
if(!ownJids_.contains(ownJid.full()))
718
ownJids_.append(ownJid.full());