blob: 7c6b8a5df6d3bdb65b70b2c4d9e25e3c52a86645 [file] [log] [blame]
Yingdi Yu2c9e7712014-10-20 11:55:05 -07001/* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil -*- */
2/*
Junxiao Shi9887fd12016-07-23 02:09:00 +00003 * Copyright (c) 2013-2016, Regents of the University of California
4 * Yingdi Yu
Yingdi Yu2c9e7712014-10-20 11:55:05 -07005 *
6 * BSD license, See the LICENSE file for more information
7 *
8 * Author: Yingdi Yu <yingdi@cs.ucla.edu>
Yingdi Yuf3401182015-02-02 20:21:07 -08009 * Qiuhan Ding <qiuhanding@cs.ucla.edu>
Yingdi Yu2c9e7712014-10-20 11:55:05 -070010 */
11
12#include "controller-backend.hpp"
13
14#ifndef Q_MOC_RUN
Yingdi Yu7ff31f02015-02-05 11:21:07 -080015#include <ndn-cxx/util/segment-fetcher.hpp>
Varun Patil3d850902020-11-23 12:19:14 +053016#include <ndn-cxx/security/signing-helpers.hpp>
17#include <ndn-cxx/security/certificate-fetcher-offline.hpp>
Yingdi Yu2c9e7712014-10-20 11:55:05 -070018#include "invitation.hpp"
Varun Patil3d850902020-11-23 12:19:14 +053019#include <iostream>
Yingdi Yu2c9e7712014-10-20 11:55:05 -070020#endif
21
Yingdi Yueb692ac2015-02-10 18:46:18 -080022namespace chronochat {
Yingdi Yu2c9e7712014-10-20 11:55:05 -070023
24using std::string;
25
26using ndn::Face;
Yingdi Yu2c9e7712014-10-20 11:55:05 -070027
Qiuhan Dingba3e57a2015-01-08 19:07:39 -080028static const ndn::Name::Component ROUTING_HINT_SEPARATOR =
29 ndn::name::Component::fromEscapedString("%F0%2E");
30static const int MAXIMUM_REQUEST = 3;
Yingdi Yuf3401182015-02-02 20:21:07 -080031static const int CONNECTION_RETRY_TIMER = 3;
Yingdi Yu2c9e7712014-10-20 11:55:05 -070032
33ControllerBackend::ControllerBackend(QObject* parent)
34 : QThread(parent)
Yingdi Yuf3401182015-02-02 20:21:07 -080035 , m_shouldResume(false)
Yingdi Yu2c9e7712014-10-20 11:55:05 -070036 , m_contactManager(m_face)
Yingdi Yu2c9e7712014-10-20 11:55:05 -070037{
38 // connection to contact manager
39 connect(this, SIGNAL(identityUpdated(const QString&)),
40 &m_contactManager, SLOT(onIdentityUpdated(const QString&)));
41
42 connect(&m_contactManager, SIGNAL(contactIdListReady(const QStringList&)),
43 this, SLOT(onContactIdListReady(const QStringList&)));
44
Varun Patil3d850902020-11-23 12:19:14 +053045 m_validator = make_shared<ndn::security::ValidatorNull>();
Yingdi Yu2c9e7712014-10-20 11:55:05 -070046}
47
48ControllerBackend::~ControllerBackend()
49{
50}
51
52void
53ControllerBackend::run()
54{
Yingdi Yuf3401182015-02-02 20:21:07 -080055 bool shouldResume = false;
56 do {
57 try {
58 setInvitationListener();
59 m_face.processEvents();
60 }
61 catch (std::runtime_error& e) {
62 {
63 std::lock_guard<std::mutex>lock(m_nfdConnectionMutex);
64 m_isNfdConnected = false;
65 }
66 emit nfdError();
67 {
68 std::lock_guard<std::mutex>lock(m_resumeMutex);
69 m_shouldResume = true;
70 }
71#ifdef BOOST_THREAD_USES_CHRONO
72 time::seconds reconnectTimer = time::seconds(CONNECTION_RETRY_TIMER);
73#else
74 boost::posix_time::time_duration reconnectTimer;
75 reconnectTimer = boost::posix_time::seconds(CONNECTION_RETRY_TIMER);
76#endif
77 while (!m_isNfdConnected) {
78#ifdef BOOST_THREAD_USES_CHRONO
79 boost::this_thread::sleep_for(reconnectTimer);
80#else
81 boost::this_thread::sleep(reconnectTimer);
82#endif
83 }
84 }
85 {
86 std::lock_guard<std::mutex>lock(m_resumeMutex);
87 shouldResume = m_shouldResume;
88 m_shouldResume = false;
89 }
90 } while (shouldResume);
Yingdi Yu2c9e7712014-10-20 11:55:05 -070091 std::cerr << "Bye!" << std::endl;
92}
93
94// private methods:
95
96void
97tmpOnInvitationInterest(const ndn::Name& prefix,
98 const ndn::Interest& interest)
99{
100 std::cerr << "tmpOnInvitationInterest" << std::endl;
101}
102
103void
104tmpOnInvitationRegisterFailed(const Name& prefix,
105 const string& failInfo)
106{
107 std::cerr << "tmpOnInvitationRegisterFailed" << std::endl;
108}
109
110void
111tmpUnregisterPrefixSuccessCallback()
112{
113 std::cerr << "tmpUnregisterPrefixSuccessCallback" << std::endl;
114}
115
116void
117tmpUnregisterPrefixFailureCallback(const string& failInfo)
118{
119 std::cerr << "tmpUnregisterPrefixSuccessCallback" << std::endl;
120}
121
122void
123ControllerBackend::setInvitationListener()
124{
125 QMutexLocker locker(&m_mutex);
126
127 Name invitationPrefix;
Qiuhan Dingba3e57a2015-01-08 19:07:39 -0800128 Name requestPrefix;
Yingdi Yu2c9e7712014-10-20 11:55:05 -0700129 Name routingPrefix = getInvitationRoutingPrefix();
130 size_t offset = 0;
131 if (!routingPrefix.isPrefixOf(m_identity)) {
Qiuhan Dingba3e57a2015-01-08 19:07:39 -0800132 invitationPrefix.append(routingPrefix).append(ROUTING_HINT_SEPARATOR);
133 requestPrefix.append(routingPrefix).append(ROUTING_HINT_SEPARATOR);
Yingdi Yu2c9e7712014-10-20 11:55:05 -0700134 offset = routingPrefix.size() + 1;
135 }
136 invitationPrefix.append(m_identity).append("CHRONOCHAT-INVITATION");
Qiuhan Dingba3e57a2015-01-08 19:07:39 -0800137 requestPrefix.append(m_identity).append("CHRONOCHAT-INVITATION-REQUEST");
Yingdi Yu2c9e7712014-10-20 11:55:05 -0700138
Varun Patil3d850902020-11-23 12:19:14 +0530139 auto invitationListenerId =
140 make_shared<ndn::RegisteredPrefixHandle>(m_face.setInterestFilter(invitationPrefix,
Yingdi Yu2c9e7712014-10-20 11:55:05 -0700141 bind(&ControllerBackend::onInvitationInterest,
142 this, _1, _2, offset),
143 bind(&ControllerBackend::onInvitationRegisterFailed,
Varun Patil3d850902020-11-23 12:19:14 +0530144 this, _1, _2)));
Yingdi Yu2c9e7712014-10-20 11:55:05 -0700145
146 if (m_invitationListenerId != 0) {
Varun Patil3d850902020-11-23 12:19:14 +0530147 invitationListenerId->unregister(
148 bind(&ControllerBackend::onInvitationPrefixReset, this),
149 bind(&ControllerBackend::onInvitationPrefixResetFailed, this, _1));
Yingdi Yu2c9e7712014-10-20 11:55:05 -0700150 }
151
152 m_invitationListenerId = invitationListenerId;
153
Varun Patil3d850902020-11-23 12:19:14 +0530154 auto requestListenerId =
155 make_shared<ndn::RegisteredPrefixHandle>(
156 m_face.setInterestFilter(requestPrefix,
157 bind(&ControllerBackend::onInvitationRequestInterest,
158 this, _1, _2, offset),
159 [] (const Name& prefix, const std::string& failInfo) {}));
Qiuhan Dingba3e57a2015-01-08 19:07:39 -0800160
161 if (m_requestListenerId != 0) {
Varun Patil3d850902020-11-23 12:19:14 +0530162 m_requestListenerId->unregister(
163 []{},
164 [] (const std::string& failInfo) {});
Qiuhan Dingba3e57a2015-01-08 19:07:39 -0800165 }
166
167 m_requestListenerId = requestListenerId;
Yingdi Yu2c9e7712014-10-20 11:55:05 -0700168}
169
170ndn::Name
171ControllerBackend::getInvitationRoutingPrefix()
172{
173 return Name("/ndn/broadcast");
174}
175
176void
177ControllerBackend::onInvitationPrefixReset()
178{
179 // _LOG_DEBUG("ControllerBackend::onInvitationPrefixReset");
180}
181
182void
183ControllerBackend::onInvitationPrefixResetFailed(const std::string& failInfo)
184{
185 // _LOG_DEBUG("ControllerBackend::onInvitationPrefixResetFailed: " << failInfo);
186}
187
188
189void
190ControllerBackend::onInvitationInterest(const ndn::Name& prefix,
191 const ndn::Interest& interest,
192 size_t routingPrefixOffset)
193{
194 // _LOG_DEBUG("onInvitationInterest: " << interest.getName());
195 shared_ptr<Interest> invitationInterest =
Yingdi Yu6a614442014-10-31 17:42:43 -0700196 make_shared<Interest>(interest.getName().getSubName(routingPrefixOffset));
Yingdi Yu2c9e7712014-10-20 11:55:05 -0700197
198 // check if the chatroom already exists;
199 try {
200 Invitation invitation(invitationInterest->getName());
201 if (m_chatDialogList.contains(QString::fromStdString(invitation.getChatroom())))
202 return;
203 }
204 catch (Invitation::Error& e) {
205 // Cannot parse the invitation;
206 return;
207 }
208
Varun Patil3d850902020-11-23 12:19:14 +0530209 m_validator->validate(
210 *invitationInterest,
211 bind(&ControllerBackend::onInvitationValidated, this, _1),
212 bind(&ControllerBackend::onInvitationValidationFailed, this, _1, _2));
Yingdi Yu2c9e7712014-10-20 11:55:05 -0700213}
214
215void
Qiuhan Dingba3e57a2015-01-08 19:07:39 -0800216ControllerBackend::onInvitationRegisterFailed(const Name& prefix, const std::string& failInfo)
Yingdi Yu2c9e7712014-10-20 11:55:05 -0700217{
218 // _LOG_DEBUG("ControllerBackend::onInvitationRegisterFailed: " << failInfo);
219}
220
221void
Qiuhan Dingba3e57a2015-01-08 19:07:39 -0800222ControllerBackend::onInvitationRequestInterest(const ndn::Name& prefix,
223 const ndn::Interest& interest,
224 size_t routingPrefixOffset)
225{
226 shared_ptr<const Data> data = m_ims.find(interest);
227 if (data != nullptr) {
228 m_face.put(*data);
229 return;
230 }
231 Name interestName = interest.getName();
232 size_t i;
233 for (i = 0; i < interestName.size(); i++)
234 if (interestName.at(i) == Name::Component("CHRONOCHAT-INVITATION-REQUEST"))
235 break;
236 if (i < interestName.size()) {
237 string chatroom = interestName.at(i+1).toUri();
238 string alias = interestName.getSubName(i+2).getPrefix(-1).toUri();
239 emit invitationRequestReceived(QString::fromStdString(alias),
240 QString::fromStdString(chatroom),
241 interestName);
242 }
243}
244
245void
Varun Patil3d850902020-11-23 12:19:14 +0530246ControllerBackend::onInvitationValidated(const Interest& interest)
Yingdi Yu2c9e7712014-10-20 11:55:05 -0700247{
Varun Patil3d850902020-11-23 12:19:14 +0530248 Invitation invitation(interest.getName());
Yingdi Yu2c9e7712014-10-20 11:55:05 -0700249 // Should be obtained via a method of ContactManager.
Varun Patil3d850902020-11-23 12:19:14 +0530250 string alias = invitation.getInviterCertificate().getKeyName().getPrefix(-1).toUri();
Yingdi Yu2c9e7712014-10-20 11:55:05 -0700251
Qiuhan Dingba3e57a2015-01-08 19:07:39 -0800252 emit invitationValidated(QString::fromStdString(alias),
253 QString::fromStdString(invitation.getChatroom()),
Varun Patil3d850902020-11-23 12:19:14 +0530254 interest.getName());
Yingdi Yu2c9e7712014-10-20 11:55:05 -0700255}
256
257void
Varun Patil3d850902020-11-23 12:19:14 +0530258ControllerBackend::onInvitationValidationFailed(const Interest& interest,
259 const ndn::security::ValidationError& failureInfo)
Yingdi Yu2c9e7712014-10-20 11:55:05 -0700260{
261 // _LOG_DEBUG("Invitation: " << interest->getName() <<
262 // " cannot not be validated due to: " << failureInfo);
263}
264
265void
Yingdi Yu7ff31f02015-02-05 11:21:07 -0800266ControllerBackend::onLocalPrefix(const ndn::ConstBufferPtr& data)
Yingdi Yu2c9e7712014-10-20 11:55:05 -0700267{
268 Name prefix;
269
Yingdi Yu7ff31f02015-02-05 11:21:07 -0800270 Block contentBlock(tlv::Content, data);
Yingdi Yu2c9e7712014-10-20 11:55:05 -0700271 try {
272 contentBlock.parse();
273
274 for (Block::element_const_iterator it = contentBlock.elements_begin();
275 it != contentBlock.elements_end(); it++) {
276 Name candidate;
277 candidate.wireDecode(*it);
278 if (candidate.isPrefixOf(m_identity)) {
279 prefix = candidate;
280 break;
281 }
282 }
283
284 if (prefix.empty()) {
285 if (contentBlock.elements_begin() != contentBlock.elements_end())
286 prefix.wireDecode(*contentBlock.elements_begin());
287 else
288 prefix = Name("/private/local");
289 }
290 }
291 catch (Block::Error& e) {
292 prefix = Name("/private/local");
293 }
294
295 updateLocalPrefix(prefix);
296}
297
298void
Yingdi Yu7ff31f02015-02-05 11:21:07 -0800299ControllerBackend::onLocalPrefixError(uint32_t code, const std::string& msg)
Yingdi Yu2c9e7712014-10-20 11:55:05 -0700300{
301 Name localPrefix("/private/local");
302 updateLocalPrefix(localPrefix);
303}
304
305void
306ControllerBackend::updateLocalPrefix(const Name& localPrefix)
307{
308 if (m_localPrefix.empty() || m_localPrefix != localPrefix) {
309 m_localPrefix = localPrefix;
310 emit localPrefixUpdated(QString::fromStdString(localPrefix.toUri()));
311 }
312}
313
Qiuhan Dingba3e57a2015-01-08 19:07:39 -0800314void
Varun Patil3d850902020-11-23 12:19:14 +0530315ControllerBackend::onRequestResponse(const Interest& interest, const Data& data)
Qiuhan Dingba3e57a2015-01-08 19:07:39 -0800316{
317 size_t i;
318 Name interestName = interest.getName();
319 for (i = 0; i < interestName.size(); i++) {
320 if (interestName.at(i) == Name::Component("CHRONOCHAT-INVITATION-REQUEST"))
321 break;
322 }
323 Name::Component chatroomName = interestName.at(i+1);
324 Block contentBlock = data.getContent();
325 int res = ndn::readNonNegativeInteger(contentBlock);
Qiuhan Ding59c45432015-04-01 18:15:03 -0700326 if (m_chatDialogList.contains(QString::fromStdString(chatroomName.toUri())))
327 return;
328
Qiuhan Dingba3e57a2015-01-08 19:07:39 -0800329 // if data is true,
330 if (res == 1)
331 emit startChatroom(QString::fromStdString(chatroomName.toUri()), false);
332 else
333 emit invitationRequestResult("You are rejected to enter chatroom: " + chatroomName.toUri());
334}
335
336void
337ControllerBackend::onRequestTimeout(const Interest& interest, int& resendTimes)
338{
339 if (resendTimes < MAXIMUM_REQUEST)
340 m_face.expressInterest(interest,
341 bind(&ControllerBackend::onRequestResponse, this, _1, _2),
Varun Patil3d850902020-11-23 12:19:14 +0530342 bind(&ControllerBackend::onRequestTimeout, this, _1, resendTimes + 1),
Qiuhan Dingba3e57a2015-01-08 19:07:39 -0800343 bind(&ControllerBackend::onRequestTimeout, this, _1, resendTimes + 1));
344 else
345 emit invitationRequestResult("Invitation request times out.");
346}
347
Yingdi Yu2c9e7712014-10-20 11:55:05 -0700348// public slots:
349void
350ControllerBackend::shutdown()
351{
Yingdi Yuf3401182015-02-02 20:21:07 -0800352 {
353 std::lock_guard<std::mutex>lock(m_resumeMutex);
354 m_shouldResume = false;
355 }
356 {
357 // In this case, we just stop checking the nfd connection and exit
358 std::lock_guard<std::mutex>lock(m_nfdConnectionMutex);
359 m_isNfdConnected = true;
360 }
Yingdi Yu2c9e7712014-10-20 11:55:05 -0700361 m_face.getIoService().stop();
362}
363
364void
365ControllerBackend::addChatroom(QString chatroom)
366{
367 m_chatDialogList.append(chatroom);
368}
369
370void
371ControllerBackend::removeChatroom(QString chatroom)
372{
373 m_chatDialogList.removeAll(chatroom);
374}
375
376void
377ControllerBackend::onUpdateLocalPrefixAction()
378{
Yingdi Yu7ff31f02015-02-05 11:21:07 -0800379 Interest interest("/localhop/nfd/rib/routable-prefixes");
Yingdi Yu2c9e7712014-10-20 11:55:05 -0700380 interest.setInterestLifetime(time::milliseconds(1000));
381 interest.setMustBeFresh(true);
382
Varun Patil3d850902020-11-23 12:19:14 +0530383 auto fetcher = ndn::util::SegmentFetcher::start(m_face,
384 interest,
385 m_nullValidator);
386 fetcher->onComplete.connect(bind(&ControllerBackend::onLocalPrefix, this, _1));
387 fetcher->onError.connect(bind(&ControllerBackend::onLocalPrefixError, this, _1, _2));
Yingdi Yu2c9e7712014-10-20 11:55:05 -0700388}
389
390void
391ControllerBackend::onIdentityChanged(const QString& identity)
392{
393 m_chatDialogList.clear();
394
395 m_identity = Name(identity.toStdString());
396
397 std::cerr << "ControllerBackend::onIdentityChanged: " << m_identity << std::endl;
398
399 m_keyChain.createIdentity(m_identity);
400
401 setInvitationListener();
402
403 emit identityUpdated(identity);
404}
405
406void
407ControllerBackend::onInvitationResponded(const ndn::Name& invitationName, bool accepted)
408{
409 shared_ptr<Data> response = make_shared<Data>();
Varun Patil3d850902020-11-23 12:19:14 +0530410 shared_ptr<ndn::security::Certificate> chatroomCert;
Yingdi Yu2c9e7712014-10-20 11:55:05 -0700411
412 // generate reply;
413 if (accepted) {
414 Name responseName = invitationName;
415 responseName.append(m_localPrefix.wireEncode());
416
417 response->setName(responseName);
418
419 // We should create a particular certificate for this chatroom,
420 //but let's use default one for now.
Varun Patil3d850902020-11-23 12:19:14 +0530421 chatroomCert = make_shared<ndn::security::Certificate>(
422 m_keyChain.createIdentity(m_identity).getDefaultKey().getDefaultCertificate());
Yingdi Yu2c9e7712014-10-20 11:55:05 -0700423
424 response->setContent(chatroomCert->wireEncode());
425 response->setFreshnessPeriod(time::milliseconds(1000));
426 }
427 else {
428 response->setName(invitationName);
429 response->setFreshnessPeriod(time::milliseconds(1000));
430 }
431
Varun Patil3d850902020-11-23 12:19:14 +0530432 m_keyChain.sign(*response, ndn::security::signingByIdentity(m_identity));
Yingdi Yu2c9e7712014-10-20 11:55:05 -0700433
434 // Check if we need a wrapper
435 Name invitationRoutingPrefix = getInvitationRoutingPrefix();
436 if (invitationRoutingPrefix.isPrefixOf(m_identity))
437 m_face.put(*response);
438 else {
439 Name wrappedName;
440 wrappedName.append(invitationRoutingPrefix)
Qiuhan Dingba3e57a2015-01-08 19:07:39 -0800441 .append(ROUTING_HINT_SEPARATOR)
Yingdi Yu2c9e7712014-10-20 11:55:05 -0700442 .append(response->getName());
443
444 // _LOG_DEBUG("onInvitationResponded: prepare reply " << wrappedName);
445
446 shared_ptr<Data> wrappedData = make_shared<Data>(wrappedName);
447 wrappedData->setContent(response->wireEncode());
448 wrappedData->setFreshnessPeriod(time::milliseconds(1000));
449
Varun Patil3d850902020-11-23 12:19:14 +0530450 m_keyChain.sign(*wrappedData, ndn::security::signingByIdentity(m_identity));
Yingdi Yu2c9e7712014-10-20 11:55:05 -0700451 m_face.put(*wrappedData);
452 }
453
454 Invitation invitation(invitationName);
455 emit startChatroomOnInvitation(invitation, true);
456}
457
458void
Qiuhan Dingba3e57a2015-01-08 19:07:39 -0800459ControllerBackend::onInvitationRequestResponded(const ndn::Name& invitationResponseName,
460 bool accepted)
461{
462 shared_ptr<Data> response = make_shared<Data>(invitationResponseName);
463 if (accepted)
Junxiao Shi9887fd12016-07-23 02:09:00 +0000464 response->setContent(ndn::makeNonNegativeIntegerBlock(tlv::Content, 1));
Qiuhan Dingba3e57a2015-01-08 19:07:39 -0800465 else
Junxiao Shi9887fd12016-07-23 02:09:00 +0000466 response->setContent(ndn::makeNonNegativeIntegerBlock(tlv::Content, 0));
Qiuhan Dingba3e57a2015-01-08 19:07:39 -0800467
Varun Patil3d850902020-11-23 12:19:14 +0530468 response->setFreshnessPeriod(time::milliseconds(1000));
469 m_keyChain.sign(*response, ndn::security::signingByIdentity(m_identity));
Qiuhan Dingba3e57a2015-01-08 19:07:39 -0800470 m_ims.insert(*response);
471 m_face.put(*response);
472}
473
474void
475ControllerBackend::onSendInvitationRequest(const QString& chatroomName, const QString& prefix)
476{
477 if (prefix.length() == 0)
478 return;
479 Name interestName = getInvitationRoutingPrefix();
480 interestName.append(ROUTING_HINT_SEPARATOR).append(prefix.toStdString());
481 interestName.append("CHRONOCHAT-INVITATION-REQUEST");
482 interestName.append(chatroomName.toStdString());
483 interestName.append(m_identity);
484 interestName.appendTimestamp();
485 Interest interest(interestName);
486 interest.setInterestLifetime(time::milliseconds(10000));
487 interest.setMustBeFresh(true);
488 interest.getNonce();
489 m_face.expressInterest(interest,
490 bind(&ControllerBackend::onRequestResponse, this, _1, _2),
Varun Patil3d850902020-11-23 12:19:14 +0530491 bind(&ControllerBackend::onRequestTimeout, this, _1, 0),
Qiuhan Dingba3e57a2015-01-08 19:07:39 -0800492 bind(&ControllerBackend::onRequestTimeout, this, _1, 0));
493}
494
495void
Yingdi Yu2c9e7712014-10-20 11:55:05 -0700496ControllerBackend::onContactIdListReady(const QStringList& list)
497{
498 ContactList contactList;
499
500 m_contactManager.getContactList(contactList);
Varun Patil3d850902020-11-23 12:19:14 +0530501 // m_validator.cleanTrustAnchor();
Yingdi Yu2c9e7712014-10-20 11:55:05 -0700502
Varun Patil3d850902020-11-23 12:19:14 +0530503 // for (ContactList::const_iterator it = contactList.begin(); it != contactList.end(); it++)
504 // m_validator.addTrustAnchor((*it)->getPublicKeyName(), (*it)->getPublicKey());
Yingdi Yu2c9e7712014-10-20 11:55:05 -0700505
506}
507
Yingdi Yuf3401182015-02-02 20:21:07 -0800508void
509ControllerBackend::onNfdReconnect()
510{
511 std::lock_guard<std::mutex>lock(m_nfdConnectionMutex);
512 m_isNfdConnected = true;
513}
514
Yingdi Yu2c9e7712014-10-20 11:55:05 -0700515
Yingdi Yueb692ac2015-02-10 18:46:18 -0800516} // namespace chronochat
Yingdi Yu2c9e7712014-10-20 11:55:05 -0700517
518#if WAF
519#include "controller-backend.moc"
520// #include "controller-backend.cpp.moc"
521#endif