yate/modules/server/subscription.cpp

2596 lines
74 KiB
C++

/**
* subscription.cpp
* This file is part of the YATE Project http://YATE.null.ro
*
* Subscription handler and presence notifier
*
* Yet Another Telephony Engine - a fully featured software PBX and IVR
* Copyright (C) 2004-2006 Null Team
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA.
*/
// TODO:
// - Implement commands
// status (user) [instances|contacts]
// drop subscription [to|from] (user) (contact)
// - Handle automatic (un)subscribe response for known users
#include <yatephone.h>
using namespace TelEngine;
namespace { // anonymous
class SubscriptionState; // This class holds subscription states
class Instance; // A known instance of an user/contact
class InstanceList; // A list of instances
class Contact; // An user's contact
class User; // An user along with its contacts
class PresenceUser; // An presence user along with its contacts
class EventUser; // An event user along with its contacts
class ExpireThread; // An worker who expires event subscriptions
class UserList; // A list of users
class GenericUser; // A generic user along with its contacts
class GenericContact; // A generic user's contact
class GenericUserList; // A list of generic users
class SubMessageHandler; // Message handler(s) installed by the module
class SubscriptionModule; // The module
/*
* This class holds subscription states
*/
class SubscriptionState
{
public:
enum Sub {
None = 0x00,
To = 0x01,
From = 0x02,
PendingIn = 0x10,
PendingOut = 0x20,
};
inline SubscriptionState()
: m_value(None)
{}
inline SubscriptionState(int flags)
: m_value(flags)
{}
inline SubscriptionState(const String& flags)
: m_value(0)
{ replace(flags); }
inline bool to() const
{ return test(To); }
inline bool from() const
{ return test(From); }
inline bool pendingOut() const
{ return test(PendingOut); }
inline bool pendingIn() const
{ return test(PendingIn); }
inline void set(int flag)
{ m_value |= flag; }
inline void reset(int flag)
{ m_value &= ~flag; }
inline void replace(int value)
{ m_value = value; }
inline bool test(int mask) const
{ return (m_value & mask) != 0; }
inline operator int() const
{ return m_value; }
// Replace all flags from a list
void replace(const String& flags);
// Build a list from flags
void toString(String& buf) const;
// Build a list parameters from flags
inline void toParam(NamedList& list, const char* param = "subscription") const {
String buf;
toString(buf);
list.addParam(param,buf);
}
static const TokenDict s_names[];
private:
int m_value; // The value
};
/*
* A known instance of an user/contact
*/
class Instance : public String
{
public:
inline Instance(const char* name, int prio)
: String(name), m_priority(prio), m_caps(0)
{}
// Add prefixed parameter(s) from this instance
void addListParam(NamedList& list, unsigned int index);
inline bool isCaps(const String& capsid) const
{ return m_caps && *m_caps == capsid; }
inline void setCaps(const String& capsid, const NamedList& list) {
TelEngine::destruct(m_caps);
m_caps = new NamedList(capsid);
m_caps->copyParams(list,"caps",'.');
}
// Copy parameters to a list
void addCaps(NamedList& list, const String& prefix = String::empty());
int m_priority;
NamedList* m_caps;
};
/*
* A known instance of an user/contact
*/
class InstanceList : public ObjList
{
public:
// Find an instance
inline Instance* findInstance(const String& name) {
ObjList* o = find(name);
return o ? static_cast<Instance*>(o->get()) : 0;
}
// Insert an instance in the list. Returns it
inline Instance* add(const String& name, int prio)
{ return add(new Instance(name,prio)); }
// Insert an instance in the list. Returns it
Instance* add(Instance* inst);
// Insert or set an existing instance. Returns it
Instance* set(const String& name, int prio, bool* newInst = 0);
// Update capabilities for all instances with the given caps id
void updateCaps(const String& capsid, NamedList& list);
// Remove an instance. Returns it found and not deleted
inline Instance* removeInstance(const String& name, bool delObj = true) {
ObjList* o = find(name);
return o ? static_cast<Instance*>(o->remove(delObj)) : 0;
}
// Add prefixed parameter(s) for all instances
// Return the number of instances added
unsigned int addListParam(NamedList& list, String* skip = 0);
// Notify all instances in the list to/from another one
void notifyInstance(bool online, bool out, const String& from, const String& to,
const String& inst, const char* data) const;
// Notify all instances in the list with the same from/to.
// Notifications are made from/to the given instance to/from all other instances
void notifySkip(bool online, bool out, const String& notifier,
const String& inst, const char* data) const;
// Retrieve data and notify each instance in the list to a given one
void notifyUpdate(bool online, const String& from, const String& to,
const String& inst) const;
// Retrieve data and notify each instance in the list to to another list
void notifyUpdate(bool online, const String& from, const String& to,
const InstanceList& dest) const;
};
/*
* An user's contact
*/
class Contact : public String
{
public:
inline Contact(const char* name, int sub)
: String(name), m_subscription(sub)
{}
inline Contact(const char* name, const String& sub = String::empty())
: String(name), m_subscription(sub)
{}
// Build a 'database' message used to update changes
Message* buildUpdateDb(const String& user, bool add = false);
// Set the contact from an array row
void set(Array& a, int row);
// Build a contact from an array row
static Contact* build(Array& a, int row);
InstanceList m_instances;
SubscriptionState m_subscription;
};
/*
* An user's contact
*/
class EventContact : public NamedList
{
public:
inline EventContact(const String& id, const NamedList& params)
: NamedList(params), m_sequence(0) {
assign(id);
m_time = params.getIntValue("expires") * 1000 + Time::msecNow();
}
inline virtual ~EventContact()
{}
inline bool hasExpired(u_int64_t time)
{ return time > m_time; }
virtual const String& toString() const
{ return *this; }
inline unsigned int getSeq()
{ return m_sequence++; }
inline int getTimeLeft()
{ return m_time - Time::secNow(); }
private:
u_int64_t m_time;
unsigned int m_sequence;
};
/*
* An user along with its contacts
*/
class User : public RefObject, public Mutex
{
public:
User(const char* name);
virtual ~User();
inline const String& user() const
{ return m_user; }
virtual const String& toString() const
{ return m_user; }
ObjList m_list; // The list of contacts
protected:
virtual void destroyed();
private:
String m_user; // The user name
};
/*
* An user along with its contacts
*/
class PresenceUser : public User
{
public:
PresenceUser(const char* name);
virtual ~PresenceUser();
inline InstanceList& instances()
{ return m_instances; }
// Notify all user's instances
void notify(const Message& msg);
// Append a new contact
void appendContact(Contact* c);
inline Contact* appendContact(const char* name, int sub) {
Contact* c = new Contact(name,sub);
appendContact(c);
return c;
}
// Find a contact
inline Contact* findContact(const String& name) {
ObjList* o = m_list.find(name);
return o ? static_cast<Contact*>(o->get()) : 0;
}
// Remove a contact. Return it if found and not deleted
Contact* removeContact(const String& name, bool delObj = true);
//
private:
InstanceList m_instances; // The list of instances
};
/*
* An user along with its contacts
*/
class EventUser : public User
{
public:
EventUser(const char* name);
virtual ~EventUser();
// Notify all user's
void notify(const Message& msg, bool haveDialog = true);
void notifyMwi(const Message& msg);
// Append a new contact
void appendContact(EventContact* c);
// Find a contact
inline EventContact* findContact(const String& name) {
ObjList* o = m_list.find(name);
return o ? static_cast<EventContact*>(o->get()) : 0;
}
void expire(u_int64_t time);
// Remove a contact. Return it if found and not deleted
EventContact* removeContact(const String& name, bool delObj = true);
NamedList* getParams(const NamedList& msg,bool init);
//
};
class ExpireThread : public Thread
{
public:
ExpireThread(Priority prio = Thread::Normal);
virtual ~ExpireThread();
virtual void run();
};
/*
* A list of users
*/
class UserList : public GenObject, public Mutex
{
public:
UserList();
inline ObjList& users()
{ return m_users; }
// Find an user. Load it from database if not found and load is true
// Returns referrenced pointer if found
PresenceUser* getUser(const String& user, bool load = true);
// Remove an user from list
void removeUser(const String& user);
protected:
// Load an user from database. Build an PresenceUser object and returns it if found
PresenceUser* askDatabase(const String& name);
private:
ObjList m_users; // Users list
};
/*
* A generic user along with its contacts
*/
class GenericUser : public RefObject, public Mutex
{
public:
GenericUser(const char* regexp);
~GenericUser();
inline bool matches(const char* str) const
{ return m_user.matches(str); }
inline bool compile()
{ return m_user.compile(); }
virtual const String& toString() const
{ return m_user; }
// Find a contact matching the given string
GenericContact* find(const String& contact);
ObjList m_list; // The list of contacts
protected:
virtual void destroyed();
private:
Regexp m_user; // The user regexp
};
/*
* A generic user's contact
*/
class GenericContact : public Regexp
{
public:
inline GenericContact(const char* regexp)
: Regexp(regexp)
{}
};
/*
* A list of generic users
*/
class GenericUserList : public ObjList, public Mutex
{
public:
GenericUserList();
// (Re)Load from database
void load();
// Find an user matching the given string
// Returns referenced pointer
GenericUser* findUser(const String& user);
};
/*
* Message handler(s) installed by the module
*/
class SubMessageHandler : public MessageHandler
{
public:
enum {
ResSubscribe,
ResNotify,
UserRoster,
UserUpdate,
EngineStart,
CallCdr,
Mwi,
};
SubMessageHandler(int handler, int prio = 80);
protected:
virtual bool received(Message& msg);
private:
int m_handler;
};
/*
* The module
*/
class SubscriptionModule : public Module
{
public:
SubscriptionModule();
virtual ~SubscriptionModule();
virtual void initialize();
// Check if a message was sent by us
inline bool isModule(const Message& msg) const {
String* module = msg.getParam("module");
return module && *module == name();
}
// Build a message to be sent by us
inline Message* message(const char* msg) const {
Message* m = new Message(msg);
m->addParam("module",name());
return m;
}
// Enqueue a resource.notify for a given instance
void notify(bool online, const String& from, const String& to,
const String& fromInst = String::empty(), const String& toInst = String::empty(),
const char* data = 0);
// Notify (un)subscribed
void subscribed(bool sub, const String& from, const String& to);
// Enqueue a resource.subscribe
void subscribe(bool sub, const String& from, const String& to,
const String* instance = 0);
// Enqueue a resource.notify with operation=probe
void probe(const char* from, const char* to);
// Dispatch a user.roster message with operation 'update'
// Load contact data from database
// Return the database result if requested
Array* notifyRosterUpdate(const char* username, const char* contact,
bool retData = false, bool sync = true);
// Handle 'resource.subscribe' for messages with event
bool handleResSubscribe(const String& event, const String& subscriber,
const String& notifier, const String& oper, Message& msg);
void handleCallCdr(const Message& msg, const String& notif);
void handleMwi(const Message& msg);
// Handle 'resource.subscribe' messages with (un)subscribe operation
bool handleResSubscribe(bool sub, const String& subscriber, const String& notifier,
Message& msg);
bool askDB(const String& subscriber, const String& notifier, const String& oper);
EventUser* getEventUser(bool create, const String& notifier, const String& oper);
// Handle 'resource.subscribe' messages with query operation
bool handleResSubscribeQuery(const String& subscriber, const String& notifier,
Message& msg);
// Handle online/offline resource.notify from contact
bool handleResNotify(bool online, Message& msg);
// Handle resource.notify with operation (un)subscribed
bool handleResNotifySub(bool sub, const String& from, const String& to,
Message& msg);
// Handle resource.notify with operation probe
bool handleResNotifyProbe(const String& from, const String& to, Message& msg);
// Update capabilities for all instances with the given caps id
void updateCaps(const String& capsid, NamedList& list);
// Handle 'user.roster' messages with operation 'query'
bool handleUserRosterQuery(const String& user, const String* contact, Message& msg);
// Handle 'user.roster' messages with operation 'update'
bool handleUserRosterUpdate(const String& user, const String& contact, Message& msg);
// Handle 'user.roster' messages with operation 'delete'
bool handleUserRosterDelete(const String& user, const String& contact, Message& msg);
// Handle 'user.update' messages with operation 'delete'
void handleUserUpdateDelete(const String& user, Message& msg);
// Handle 'msg.route' messages
bool imRoute(Message& msg);
void expireSubscriptions();
// Build a database message from account and query.
// Replace query params. Return Message pointer on success
Message* buildDb(const String& account, const String& query,
const NamedList& params);
// Dispatch a database message
// Return Message pointer on success. Release msg on failure
Message* queryDb(Message*& msg);
String m_account;
String m_userLoadQuery;
String m_userEventQuery;
String m_userDeleteQuery;
String m_contactLoadQuery;
String m_contactSubSetQuery;
String m_contactSetQuery;
String m_contactDeleteQuery;
String m_genericUserLoadQuery;
UserList m_users;
NamedList m_events;
ExpireThread* m_expire;
GenericUserList m_genericUsers;
protected:
virtual bool received(Message& msg, int id);
// Execute commands
virtual bool commandExecute(String& retVal, const String& line);
// Handle command complete requests
virtual bool commandComplete(Message& msg, const String& partLine,
const String& partWord);
// Notify 'from' instances to 'to'
void notifyInstances(bool online, PresenceUser& from, PresenceUser& to);
private:
ObjList m_handlers; // Message handlers list
};
INIT_PLUGIN(SubscriptionModule); // The module
static bool s_singleOffline = true; // Enqueue a single 'offline' resource.notify
// message when multiple instances are available
bool s_check = true;
// Subscription flag names
const TokenDict SubscriptionState::s_names[] = {
{"none", None},
{"to", To},
{"from", From},
{"pending_in", PendingIn},
{"pending_out", PendingOut},
{0,0},
};
// Message handlers installed by the module
static const TokenDict s_msgHandler[] = {
{"resource.subscribe", SubMessageHandler::ResSubscribe},
{"resource.notify", SubMessageHandler::ResNotify},
{"user.roster", SubMessageHandler::UserRoster},
{"user.update", SubMessageHandler::UserUpdate},
{"engine.start", SubMessageHandler::EngineStart},
{"call.cdr", SubMessageHandler::CallCdr},
{"mwi", SubMessageHandler::Mwi},
{0,0}
};
static const char* s_cmds[] = {
"status", // Subscription status
"unsubscribe", // Unsubscribe user from contact's presence
0
};
// Decode a list of comma separated flags
static int decodeFlags(const String& str, const TokenDict* flags)
{
int st = 0;
ObjList* list = str.split(',',false);
for (ObjList* ob = list->skipNull(); ob; ob = ob->skipNext())
st |= lookup(static_cast<String*>(ob->get())->c_str(),flags);
TelEngine::destruct(list);
return st;
}
// Encode a value to comma separated list of flags
static void encodeFlags(String& buf, int value, const TokenDict* flags)
{
if (!flags)
return;
for (; flags->token; flags++)
if (0 != (value & flags->value))
buf.append(flags->token,",");
}
/*
* SubscriptionState
*/
// Replace all flags from a list
void SubscriptionState::replace(const String& flags)
{
m_value = decodeFlags(flags,s_names);
}
// Build a list from flags
void SubscriptionState::toString(String& buf) const
{
encodeFlags(buf,m_value,s_names);
}
/*
* Instance
*/
// Add prefixed parameter(s) from this instance
void Instance::addListParam(NamedList& list, unsigned int index)
{
String prefix("instance.");
prefix << index;
list.addParam(prefix,c_str());
addCaps(list,prefix + ".");
}
// Copy parameters to a list
void Instance::addCaps(NamedList& list, const String& prefix)
{
if (!m_caps)
return;
if (!prefix) {
list.copyParams(*m_caps);
return;
}
unsigned int n = m_caps->count();
for (unsigned int i = 0; i < n; i++) {
NamedString* ns = m_caps->getParam(i);
if (ns)
list.addParam(prefix + ns->name(),*ns);
}
}
/*
* InstanceList
*/
// Insert an instance in the list
Instance* InstanceList::add(Instance* inst)
{
if (!inst)
return 0;
ObjList* o = skipNull();
for (; o; o = o->skipNext()) {
Instance* tmp = static_cast<Instance*>(o->get());
if (inst->m_priority > tmp->m_priority)
break;
}
if (o)
o->insert(inst);
else
append(inst);
XDebug(&__plugin,DebugAll,"InstanceList set '%s' prio=%u [%p]",
inst->c_str(),inst->m_priority,this);
return inst;
}
// Insert or set an existing instance
Instance* InstanceList::set(const String& name, int prio, bool* newInst)
{
Instance* inst = 0;
ObjList* o = find(name);
if (newInst)
*newInst = (o == 0);
if (o) {
inst = static_cast<Instance*>(o->get());
// Re-insert if priority changed
if (inst->m_priority != prio) {
o->remove(false);
inst->m_priority = prio;
add(inst);
}
}
else
inst = add(name,prio);
return inst;
}
// Update capabilities for all instances with the given caps id
void InstanceList::updateCaps(const String& capsid, NamedList& list)
{
for (ObjList* o = skipNull(); o; o = o->skipNext()) {
Instance* i = static_cast<Instance*>(o->get());
if (i->isCaps(capsid))
i->setCaps(capsid,list);
}
}
// Add prefixed parameter(s) for all instances
// Return the number of instances added
unsigned int InstanceList::addListParam(NamedList& list, String* skip)
{
unsigned int n = 0;
for (ObjList* o = skipNull(); o; o = o->skipNext()) {
Instance* tmp = static_cast<Instance*>(o->get());
if (!skip || *skip != *tmp)
tmp->addListParam(list,++n);
}
return n;
}
// Notify all instances in the list
void InstanceList::notifyInstance(bool online, bool out, const String& from,
const String& to, const String& inst, const char* data) const
{
DDebug(&__plugin,DebugAll,
"InstanceList::notifyInstance(%s,%s,%s,%s,%s,%p) count=%u [%p]",
online ? "online" : "offline",out ? "from" : "to",
from.c_str(),to.c_str(),inst.c_str(),data,count(),this);
for (ObjList* o = skipNull(); o; o = o->skipNext()) {
Instance* tmp = static_cast<Instance*>(o->get());
if (out)
__plugin.notify(online,from,to,*tmp,inst,data);
else
__plugin.notify(online,from,to,inst,*tmp,data);
}
}
// Notify all instances in the list with the same from/to.
// Notifications are made from/to the given instance to/from all other instances
void InstanceList::notifySkip(bool online, bool out, const String& notifier,
const String& inst, const char* data) const
{
DDebug(&__plugin,DebugAll,"InstanceList::notifySkip(%s,%s,%s,%s,%p) [%p]",
online ? "online" : "offline",out ? "from" : "to",
notifier.c_str(),inst.c_str(),data,this);
for (ObjList* o = skipNull(); o; o = o->skipNext()) {
Instance* tmp = static_cast<Instance*>(o->get());
if (*tmp != inst) {
if (out)
__plugin.notify(online,notifier,notifier,*tmp,inst,data);
else
__plugin.notify(online,notifier,notifier,inst,*tmp,data);
}
}
}
// Retrieve data and notify each instance in the list to a given one
void InstanceList::notifyUpdate(bool online, const String& from, const String& to,
const String& inst) const
{
DDebug(&__plugin,DebugAll,"InstanceList::notifyUpdate(%s,%s,%s,%s) [%p]",
online ? "online" : "offline",from.c_str(),to.c_str(),inst.c_str(),this);
for (ObjList* o = skipNull(); o; o = o->skipNext()) {
Instance* tmp = static_cast<Instance*>(o->get());
Message* m = 0;
const char* data = 0;
if (online) {
m = __plugin.message("resource.notify");
m->addParam("operation","query");
m->addParam("contact",from);
m->addParam("instance",*tmp);
if (Engine::dispatch(m))
data = m->getValue("data");
}
__plugin.notify(online,from,to,*tmp,inst,data);
TelEngine::destruct(m);
}
}
// Retrieve data and notify each instance in the list to to another list
void InstanceList::notifyUpdate(bool online, const String& from, const String& to,
const InstanceList& dest) const
{
DDebug(&__plugin,DebugAll,"InstanceList::notifyUpdate(%s,%s,%s) [%p]",
online ? "online" : "offline",from.c_str(),to.c_str(),this);
if (!dest.skipNull())
return;
for (ObjList* o = skipNull(); o; o = o->skipNext()) {
Instance* tmp = static_cast<Instance*>(o->get());
Message* m = 0;
const char* data = 0;
if (online) {
m = __plugin.message("resource.notify");
m->addParam("operation","query");
m->addParam("contact",from);
m->addParam("instance",*tmp);
if (Engine::dispatch(m))
data = m->getValue("data");
}
dest.notifyInstance(online,false,from,to,*tmp,data);
TelEngine::destruct(m);
}
}
/*
* Contact
*/
// Build a 'database' message used to update changes
Message* Contact::buildUpdateDb(const String& user, bool add)
{
NamedList p("");
p.addParam("username",user);
p.addParam("contact",c_str());
m_subscription.toParam(p);
DDebug(&__plugin,DebugAll,"Contact::buildUpdateDb() user=%s %s contact=%s sub=%s",
user.c_str(),add ? "adding" : "updating",c_str(),p.getValue("subscription"));
return __plugin.buildDb(__plugin.m_account,__plugin.m_contactSubSetQuery,p);
}
// Set the contact from an array row
void Contact::set(Array& a, int row)
{
int cols = a.getColumns();
for (int col = 1; col < cols; col++) {
String* s = YOBJECT(String,a.get(col,0));
if (!s)
continue;
if (*s == "subscription") {
String* sub = YOBJECT(String,a.get(col,row));
if (sub)
m_subscription.replace(*sub);
}
}
}
// Build a contact from an array row
Contact* Contact::build(Array& a, int row)
{
Contact* c = 0;
int cols = a.getColumns();
for (int col = 1; col < cols; col++) {
String* s = YOBJECT(String,a.get(col,0));
if (!s)
continue;
if (*s == "contact") {
String* n = YOBJECT(String,a.get(col,row));
if (!TelEngine::null(n))
c = new Contact(*n,String::empty());
break;
}
}
if (c)
c->set(a,row);
return c;
}
User::User(const char* name)
: Mutex(true,__plugin.name() + ":User"), m_user(name)
{
}
User::~User()
{
m_list.clear();
m_user.clear();
}
void User::destroyed()
{
m_list.clear();
RefObject::destroyed();
}
/*
* PresenceUser
*/
PresenceUser::PresenceUser(const char* name)
: User(name)
{
DDebug(&__plugin,DebugAll,"PresenceUser::PresenceUser(%s) [%p]",name,this);
}
PresenceUser::~PresenceUser()
{
DDebug(&__plugin,DebugAll,"PresenceUser::~PresenceUser(%s) [%p]",user().c_str(),this);
m_list.clear();
}
void PresenceUser::notify(const Message& msg)
{
Lock lock(this);
ObjList* o = m_list.skipNull();
for (; o; o = o->skipNext()) {
Contact* c = static_cast<Contact*>(o->get());
if (!c->m_subscription.from())
continue;
if (!c->m_instances.skipNull()) {
DDebug(&__plugin,DebugAll,"PresenceUser(%s) no instances for contact %s [%p]",
user().c_str(),c->c_str(),this);
continue;
}
DDebug(&__plugin,DebugAll,"PresenceUser(%s) notifying contact %s [%p]",
user().c_str(),c->c_str(),this);
String* oper = msg.getParam("operation");
bool online = !oper || *oper != "finalize";
c->m_instances.notifyInstance(online,false,user(),*c,msg.getValue("callid"),0);
}
}
// Add a contact
void PresenceUser::appendContact(Contact* c)
{
if (!c)
return;
Lock lock(this);
m_list.append(c);
#ifdef DEBUG
String sub;
c->m_subscription.toString(sub);
DDebug(&__plugin,DebugAll,"PresenceUser(%s) added contact (%p,%s) subscription=%s [%p]",
user().c_str(),c,c->c_str(),sub.c_str(),this);
#endif
}
// Remove a contact. Return it if found and not deleted
Contact* PresenceUser::removeContact(const String& name, bool delObj)
{
ObjList* o = m_list.find(name);
if (!o)
return 0;
Contact* c = static_cast<Contact*>(o->get());
#ifdef DEBUG
String sub;
c->m_subscription.toString(sub);
DDebug(&__plugin,DebugAll,"PresenceUser(%s) removed contact (%p,%s) subscription=%s [%p]",
user().c_str(),c,c->c_str(),sub.c_str(),this);
#endif
o->remove(delObj);
return delObj ? 0 : c;
}
/*
* EventUser
*/
EventUser::EventUser(const char* name)
: User(name)
{
DDebug(&__plugin,DebugAll,"EventUser::EventUser(%s) [%p]",name,this);
}
EventUser::~EventUser()
{
DDebug(&__plugin,DebugAll,"PresenceUser::~PresenceUser(%s) [%p]",user().c_str(),this);
m_list.clear();
}
// Add a contact
void EventUser::appendContact(EventContact* c)
{
if (!c)
return;
Lock lock(this);
ObjList* o = m_list.find(c->toString());
if (o)
o->set(c);
else
m_list.append(c);
#ifdef DEBUG
String sub;
DDebug(&__plugin,DebugAll,"EventUser(%s) added contact (%p,%s) [%p]",
user().c_str(),c,c->c_str(),this);
#endif
}
// Remove a contact. Return it if found and not deleted
EventContact* EventUser::removeContact(const String& name, bool delObj)
{
ObjList* o = m_list.find(name);
if (!o)
return 0;
EventContact* c = static_cast<EventContact*>(o->get());
#ifdef DEBUG
DDebug(&__plugin,DebugAll,"EventUser(%s) removed contact (%p,%s) [%p]",
user().c_str(),c,c->c_str(),this);
#endif
o->remove(delObj);
return delObj ? 0 : c;
}
void EventUser::notify(const Message& msg, bool haveDialog)
{
for (ObjList* o = m_list.skipNull(); o; o = o->skipNext()) {
EventContact* c = static_cast<EventContact*>(o->get());
if (!c)
continue;
String notif = msg.getValue("caller");
if(notif == *c)
continue;
Message* m = new Message("resource.notify");
m->copyParams(*c);
m->setParam("notifyseq",String(c->getSeq()));
m->setParam("subscriptionstate","active");
m->setParam("expires",String(c->getTimeLeft()));
if (m->getParam("notifier-uri"))
m->setParam("notifier-uri",msg.getValue("local-uri"));
String oper = msg.getValue("operation");
bool init = (oper == "initialize");
if (haveDialog) {
m->setParam("state","full");
NamedList* nl = getParams(msg,init);
if (!nl) {
Engine::enqueue(m);
continue;
}
String dir = msg.getValue("direction");
String caller,called;
if (dir == "incoming") {
called = msg.getParam("called");
caller = msg.getParam("caller");
}
else if (dir == "outgoing") {
called = msg.getParam("caller");
caller = msg.getParam("called");
}
nl->addParam("dialog.caller",caller);
nl->addParam("dialog.called",called);
NamedPointer* p = new NamedPointer("cdr",nl);
m->addParam(p);
}
else {
m->setParam("state","full");
for (unsigned int i = 0;i < msg.count();i++) {
NamedString* ns = msg.getParam(i);
NamedPointer* p = ns ? static_cast<NamedPointer*>(ns->getObject("NamedPointer")) : 0;
if (!p)
continue;
NamedList* list = static_cast<NamedList*>(p->userData());
if (!list)
continue;
NamedList* nl = getParams(*list,init);
NamedPointer* np = new NamedPointer("cdr",nl);
m->addParam(np);
}
}
Engine::enqueue(m);
}
}
void EventUser::notifyMwi(const Message& msg)
{
for (ObjList* o = m_list.skipNull(); o; o = o->skipNext()) {
EventContact* c = static_cast<EventContact*>(o->get());
if (!c)
continue;
Message* m = new Message("resource.notify");
m->copyParams(msg);
m->copyParams(*c);
Engine::enqueue(m);
}
}
NamedList* EventUser::getParams(const NamedList& msg,bool init)
{
NamedList* nl = new NamedList("");
nl->setParam("dialog.id",msg.getValue("billid"));
String state = msg.getValue("status");
if (state == "incoming" || state == "outgoing")
state = "initiating";
String oper = msg.getValue("operation");
if (oper == "finalize")
state = "hangup";
nl->setParam("dialog.state",state);
if (init)
return nl;
nl->setParam("dialog.callid",msg.getValue("chan"));
nl->setParam("dialog.remoteuri",msg.getValue("remote-uri"));
nl->setParam("dialog.localuri",msg.getValue("local-uri"));
nl->setParam("duration",msg.getValue("duration"));
nl->setParam("dialog.direction",msg.getValue("direction"));
return nl;
}
void EventUser::expire(u_int64_t time)
{
for (ObjList* o = m_list.skipNull(); o; o = o->skipNext()) {
EventContact* c = static_cast<EventContact*>(o->get());
if (!c)
continue;
if (!c->hasExpired(time))
continue;
Debug(DebugNote,"Subscribtion terminated for Contact %s",c->c_str());
Message* m = new Message ("resource.notify");
m->addParam("subscriptionstate","terminated");
m->addParam("terminatereason","timeout");
m->copyParams(*c);
m_list.remove(c);
Engine::enqueue(m);
}
}
/*
* ExpireThread
*/
ExpireThread::ExpireThread(Priority prio)
: Thread("ExpireThread", prio)
{
XDebug(&__plugin,DebugAll,"ExpireThread created [%p]",this);
Lock lock(__plugin);
__plugin.m_expire = this;
}
ExpireThread::~ExpireThread()
{
XDebug(&__plugin,DebugAll,"ExpireThread destroyed [%p]",this);
Lock lock(__plugin);
if (__plugin.m_expire) {
__plugin.m_expire = 0;
lock.drop();
Debug(&__plugin,DebugWarn,"ExpireThread abnormally terminated [%p]",this);
}
}
void ExpireThread::run()
{
DDebug(&__plugin,DebugAll,"%s start running [%p]",currentName(),this);
while (!Engine::exiting()) {
if (s_check) {
__plugin.expireSubscriptions();
s_check = false;
}
Thread::idle(false);
if (Thread::check(false))
break;
}
Lock lock(__plugin);
__plugin.m_expire = 0;
}
/*
* UserList
*/
UserList::UserList()
: Mutex(true,__plugin.name() + ":UserList")
{
}
// Find an user. Load it from database if not found
// Returns referrenced pointer if found
PresenceUser* UserList::getUser(const String& user, bool load)
{
XDebug(&__plugin,DebugAll,"UserList::getUser(%s)",user.c_str());
Lock lock(this);
ObjList* o = m_users.find(user);
if (o) {
PresenceUser* u = static_cast<PresenceUser*>(o->get());
return u->ref() ? u : 0;
}
lock.drop();
if (!load)
return 0;
PresenceUser* u = askDatabase(user);
if (!u)
return 0;
// Check if the user was already added while unlocked
Lock lock2(this);
ObjList* tmp = m_users.find(user);
if (!tmp)
m_users.append(u);
else {
TelEngine::destruct(u);
u = static_cast<PresenceUser*>(tmp->get());
}
return u->ref() ? u : 0;
}
// Remove an user from list
void UserList::removeUser(const String& user)
{
Lock lock(this);
ObjList* o = m_users.find(user);
if (!o)
return;
#ifdef DEBUG
PresenceUser* u = static_cast<PresenceUser*>(o->get());
Debug(&__plugin,DebugAll,"UserList::removeUser() %p '%s'",u,user.c_str());
#endif
o->remove();
}
// Load an user from database. Build an PresenceUser and returns it if found
PresenceUser* UserList::askDatabase(const String& name)
{
NamedList p("");
p.addParam("username",name);
Message* m = __plugin.buildDb(__plugin.m_account,__plugin.m_userLoadQuery,p);
m = __plugin.queryDb(m);
if (!m)
return 0;
PresenceUser* u = 0;
Array* a = 0;
if (m->getIntValue("rows") >= 1)
a = static_cast<Array*>(m->userObject("Array"));
if (a) {
u = new PresenceUser(name);
int rows = a->getRows();
for (int i = 1; i < rows; i++) {
Contact* c = Contact::build(*a,i);
if (c)
u->appendContact(c);
}
}
TelEngine::destruct(m);
return u;
}
/*
* GenericUser
*/
GenericUser::GenericUser(const char* regexp)
: Mutex(true,__plugin.name() + ":GenericUser"),
m_user(regexp)
{
DDebug(&__plugin,DebugAll,"GenericUser(%s) [%p]",regexp,this);
}
GenericUser::~GenericUser()
{
DDebug(&__plugin,DebugAll,"GenericUser(%s) destroyed [%p]",m_user.c_str(),this);
m_list.clear();
}
// Find a contact matching the given string
GenericContact* GenericUser::find(const String& contact)
{
for (ObjList* o = m_list.skipNull(); o; o = o->skipNext()) {
GenericContact* c = static_cast<GenericContact*>(o->get());
if (c->matches(contact.c_str()))
return c;
}
return 0;
}
void GenericUser::destroyed()
{
m_list.clear();
RefObject::destroyed();
}
/*
* GenericUserList
*/
GenericUserList::GenericUserList()
: Mutex(true,__plugin.name() + ":GenericUserList")
{
}
// (Re)Load from database
void GenericUserList::load()
{
DDebug(&__plugin,DebugAll,"Loading generic users");
Message* m = __plugin.buildDb(__plugin.m_account,__plugin.m_genericUserLoadQuery,NamedList::empty());
m = __plugin.queryDb(m);
Lock lock(this);
clear();
if (!m)
return;
Array* a = static_cast<Array*>(m->userObject("Array"));
if (!a) {
TelEngine::destruct(m);
return;
}
int rows = a->getRows();
int cols = a->getColumns();
for (int i = 1; i < rows; i++) {
String* user = 0;
String* contact = 0;
// Get username
for (int j = 0; j < cols; j++) {
String* tmp = YOBJECT(String,a->get(j,0));
if (!tmp)
continue;
if (*tmp == "username")
user = YOBJECT(String,a->get(j,i));
else if (*tmp == "contact")
contact = YOBJECT(String,a->get(j,i));
}
if (!(user && contact))
continue;
GenericContact* c = new GenericContact(*contact);
if (!c->compile()) {
Debug(&__plugin,DebugNote,"Invalid generic contact regexp '%s' for user=%s",
contact->c_str(),user->c_str());
TelEngine::destruct(c);
continue;
}
GenericUser* u = 0;
ObjList* o = find(*user);
if (o)
u = static_cast<GenericUser*>(o->get());
else {
u = new GenericUser(*user);
if (u->compile())
append(u);
else {
Debug(&__plugin,DebugNote,"Invalid generic user regexp '%s'",user->c_str());
TelEngine::destruct(c);
TelEngine::destruct(u);
}
}
if (u) {
u->lock();
u->m_list.append(c);
u->unlock();
DDebug(&__plugin,DebugAll,"Added generic user='%s' contact='%s'",
user->c_str(),contact->c_str());
}
}
TelEngine::destruct(m);
}
// Find an user matching the given string
// Returns referenced pointer
GenericUser* GenericUserList::findUser(const String& user)
{
Lock lock(this);
for (ObjList* o = skipNull(); o; o = o->skipNext()) {
GenericUser* u = static_cast<GenericUser*>(o->get());
if (u->matches(user))
return u->ref() ? u : 0;
}
return 0;
}
/*
* SubMessageHandler
*/
SubMessageHandler::SubMessageHandler(int handler, int prio)
: MessageHandler(lookup(handler,s_msgHandler),prio),
m_handler(handler)
{
}
bool SubMessageHandler::received(Message& msg)
{
if (m_handler == ResNotify) {
if (__plugin.isModule(msg) || msg.getParam("event"))
return false;
String* oper = msg.getParam("operation");
if (TelEngine::null(oper))
return false;
// online/offline
bool online = (*oper == "update" || *oper == "online");
if (online || *oper == "delete" || *oper == "offline")
return __plugin.handleResNotify(online,msg);
if (*oper == "updatecaps") {
String* capsid = msg.getParam("caps.id");
if (!TelEngine::null(capsid))
__plugin.updateCaps(*capsid,msg);
return false;
}
String* src = msg.getParam("from");
String* dest = msg.getParam("to");
if (TelEngine::null(src) || TelEngine::null(dest))
return false;
// (un)subscribed
bool sub = (*oper == "subscribed");
if (sub || *oper == "unsubscribed")
return __plugin.handleResNotifySub(sub,*src,*dest,msg);
// probe
if (*oper == "probe")
return __plugin.handleResNotifyProbe(*src,*dest,msg);
return false;
}
if (m_handler == ResSubscribe) {
if (__plugin.isModule(msg))
return false;
String* oper = msg.getParam("operation");
String* notifier = msg.getParam("notifier");
String* subscriber = msg.getParam("subscriber");
if (TelEngine::null(oper) || TelEngine::null(subscriber) || TelEngine::null(notifier))
return false;
String* event = msg.getParam("event");
if (event) {
if (!__plugin.m_userEventQuery)
return false;
return __plugin.handleResSubscribe(*event,*subscriber,*notifier,*oper,msg);
}
bool sub = (*oper == "subscribe");
if (sub || *oper == "unsubscribe")
return __plugin.handleResSubscribe(sub,*subscriber,*notifier,msg);
if (*oper == "query")
return __plugin.handleResSubscribeQuery(*subscriber,*notifier,msg);
return false;
}
if (m_handler == UserRoster) {
if (__plugin.isModule(msg))
return false;
XDebug(&__plugin,DebugAll,"%s oper='%s' user='%s' contact='%s'",
msg.c_str(),msg.getValue("operation"),msg.getValue("username"),
msg.getValue("contact"));
String* oper = msg.getParam("operation");
if (TelEngine::null(oper))
return false;
String* user = msg.getParam("username");
String* contact = msg.getParam("contact");
if (TelEngine::null(user))
return false;
if (*oper == "query")
return __plugin.handleUserRosterQuery(*user,contact,msg);
if (TelEngine::null(contact))
return false;
if (*oper == "update")
return __plugin.handleUserRosterUpdate(*user,*contact,msg);
if (*oper == "delete")
return __plugin.handleUserRosterDelete(*user,*contact,msg);
return false;
}
if (m_handler == UserUpdate) {
String* notif = msg.getParam("notify");
if (TelEngine::null(notif) || *notif != "delete")
return false;
String* user = msg.getParam("user");
if (TelEngine::null(user))
return false;
__plugin.handleUserUpdateDelete(*user,msg);
return false;
}
if (m_handler == EngineStart) {
__plugin.m_genericUsers.load();
return false;
}
if (m_handler == CallCdr) {
String* notif = msg.getParam("external");
if (TelEngine::null(notif))
return false;
__plugin.handleCallCdr(msg,*notif);
return false;
}
if (m_handler == Mwi) {
String* oper = msg.getParam("operation");
if (*oper != "notify")
return false;
__plugin.handleMwi(msg);
return true;
}
Debug(&__plugin,DebugStub,"SubMessageHandler(%s) not handled!",msg.c_str());
return false;
}
/*
* SubscriptionModule Module
*/
SubscriptionModule::SubscriptionModule()
: Module("subscription","misc",true), m_events("")
{
Output("Loaded module Subscriptions");
}
SubscriptionModule::~SubscriptionModule()
{
Output("Unloading module Subscriptions");
}
void SubscriptionModule::initialize()
{
Output("Initializing module Subscriptions");
if (m_handlers.skipNull()) {
// Reload generic users (wait engine.start for the first load)
m_genericUsers.load();
}
else {
Configuration cfg(Engine::configFile("subscription"));
m_account = cfg.getValue("general","account");
m_userLoadQuery = cfg.getValue("general","user_roster_load");
m_userEventQuery = cfg.getValue("general","user_event_auth");
m_userDeleteQuery = cfg.getValue("general","user_roster_delete");
m_contactLoadQuery = cfg.getValue("general","contact_load");
m_contactSubSetQuery = cfg.getValue("general","contact_subscription_set");
m_contactSetQuery = cfg.getValue("general","contact_set");
m_contactDeleteQuery = cfg.getValue("general","contact_delete");
m_genericUserLoadQuery = cfg.getValue("general","generic_roster_load");
if (m_userEventQuery)
(new ExpireThread())->startup();
// Install relays
setup();
installRelay(Halt);
installRelay(ImRoute);
// Install handlers
for (const TokenDict* d = s_msgHandler; d->token; d++) {
if (d->value == SubMessageHandler::CallCdr && !m_userEventQuery)
continue;
SubMessageHandler* h = new SubMessageHandler(d->value);
Engine::install(h);
m_handlers.append(h);
}
}
}
// Enqueue a resource.notify for a given instance
// data: optional data used to override instance's data
void SubscriptionModule::notify(bool online, const String& from, const String& to,
const String& fromInst, const String& toInst, const char* data)
{
const char* what = online ? "online" : "offline";
Debug(this,DebugAll,"notify=%s notifier=%s (%s) subscriber=%s (%s)",
what,from.c_str(),fromInst.c_str(),to.c_str(),toInst.c_str());
Message* m = message("resource.notify");
m->addParam("operation",what);
m->addParam("from",from);
m->addParam("to",to);
if (fromInst)
m->addParam("from_instance",fromInst);
if (toInst)
m->addParam("to_instance",toInst);
if (!TelEngine::null(data))
m->addParam("data",data);
Engine::enqueue(m);
}
// Notify (un)subscribed
void SubscriptionModule::subscribed(bool sub, const String& from, const String& to)
{
Debug(this,DebugAll,"subscribed(%s) from=%s to=%s",
String::boolText(sub),from.c_str(),to.c_str());
Message* m = message("resource.notify");
m->addParam("operation",sub ? "subscribed" : "unsubscribed");
m->addParam("from",from);
m->addParam("to",to);
Engine::enqueue(m);
}
// Enqueue a resource.subscribe
void SubscriptionModule::subscribe(bool sub, const String& from, const String& to,
const String* instance)
{
const char* what = sub ? "subscribe" : "unsubscribe";
Debug(this,DebugAll,"Requesting %s subscriber=%s notifier=%s",
what,from.c_str(),to.c_str());
Message* m = message("resource.subscribe");
m->addParam("operation",what);
m->addParam("subscriber",from);
m->addParam("notifier",to);
if (!TelEngine::null(instance))
m->addParam("instance",*instance);
Engine::enqueue(m);
}
// Enqueue a resource.notify with operation=probe
void SubscriptionModule::probe(const char* from, const char* to)
{
Message* m = message("resource.notify");
m->addParam("operation","probe");
m->addParam("from",from);
m->addParam("to",to);
Engine::enqueue(m);
}
// Dispatch a user.roster message with operation 'update'
// Load contact data from database
// Return the database result if requested
Array* SubscriptionModule::notifyRosterUpdate(const char* username, const char* contact,
bool retData, bool sync)
{
NamedList p("");
p.addParam("username",username);
p.addParam("contact",contact);
Message* m = buildDb(m_account,m_contactLoadQuery,p);
m = queryDb(m);
Array* data = 0;
if (m && m->getIntValue("rows") >= 1) {
data = static_cast<Array*>(m->userObject("Array"));
if (data && data->ref())
m->userData(0);
else
data = 0;
}
TelEngine::destruct(m);
if (!data)
return 0;
Message* mu = message("user.roster");
mu->addParam("notify","update");
mu->addParam("username",username);
mu->addParam("contact.count","1");
String prefix("contact.1");
mu->addParam(prefix,contact);
prefix << ".";
// Add contact data
int cols = data->getColumns();
for (int col = 1; col < cols; col++) {
String* name = YOBJECT(String,data->get(col,0));
if (TelEngine::null(name) || *name == "username" || *name == "contact")
continue;
String* value = YOBJECT(String,data->get(col,1));
if (!value)
continue;
mu->addParam(prefix + *name,*value);
}
if (sync) {
Engine::dispatch(mu);
TelEngine::destruct(mu);
}
else
Engine::enqueue(mu);
if (!retData)
TelEngine::destruct(data);
return data;
}
// Handle 'resource.subscribe' for messages with event
bool SubscriptionModule::handleResSubscribe(const String& event, const String& subscriber,
const String& notifier, const String& oper,Message& msg)
{
EventUser* user = 0;
if (oper != "subscribe") {
user = getEventUser(false,notifier,event);
if (!user)
return false;
user->removeContact(subscriber,true);
//TODO should the user be removed????? // Make notification of subscription terminated
return true;
}
if (!askDB(notifier,subscriber,event))
return false;
user = getEventUser(true,notifier,event);
if (!user)
return false;
user->appendContact(new EventContact(subscriber,msg));
Message* m = 0;
if (event == "dilaog") {
m = new Message("cdr.query");
m->addParam("external",notifier);
}
else {
m = new Message("mwi.query");
m->addParam("subscriber",subscriber);
m->addParam("notifier",notifier);
m->addParam("message-summary.voicenew","0");
m->addParam("message-summary.voiceold","0");
}
if (Engine::dispatch(m))
event == "dilaog" ? user->notify(*m,false) : user->notifyMwi(*m);
else
event == "dilaog" ? user->notify(msg,false) : user->notifyMwi(msg);
TelEngine::destruct(m);
return true;
}
EventUser* SubscriptionModule::getEventUser(bool create, const String& notifier,
const String& event)
{
NamedString* p = m_events.getParam(event);
NamedPointer* po = static_cast<NamedPointer*>(p);
if (!po) {
if (!create)
return 0;
po = new NamedPointer(event,new NamedList(event));
XDebug(this,DebugAll,"Creating List for Event %s",event.c_str());
m_events.setParam(po);
}
NamedList* eventList = static_cast<NamedList*>(po->userData());
// Find Notifier list
NamedString* ns = eventList->getParam(notifier);
NamedPointer* np = static_cast<NamedPointer*>(ns);
if (!np) {
if (!create)
return 0;
np = new NamedPointer(notifier,new EventUser(notifier));
XDebug(this,DebugAll,"Creating user %s for Event %s",notifier.c_str(),event.c_str());
eventList->setParam(np);
}
return static_cast<EventUser*>(np->userData());
}
bool SubscriptionModule::askDB(const String& subscriber, const String& notifier,
const String& oper)
{
if (subscriber)
return true;
NamedList nl("");
nl.setParam("subscriber",subscriber);
nl.setParam("notifier",notifier);
nl.setParam("operation",oper);
Message* m = buildDb(m_account,m_userEventQuery,nl);
if (!m)
return false;
m = queryDb(m);
bool ok = m != 0;
TelEngine::destruct(m);
return ok;
}
void SubscriptionModule::handleCallCdr(const Message& msg, const String& notif)
{
DDebug(this,DebugAll,"handleCallCdr() notifier=%s",notif.c_str());
// TODO: lock!!!!!!!!!!!
EventUser* user = getEventUser(false,notif,"dialog");
if (user)
user->notify(msg);
PresenceUser* pu = 0;
m_users.lock();
for (ObjList* o = m_users.users().skipNull(); o; o = o->skipNext()) {
pu = static_cast<PresenceUser*>(o->get());
if (pu->user().substr(0,pu->user().find("@")) == notif) {
pu->ref();
break;
}
pu = 0;
}
m_users.unlock();
if (!pu)
return;
pu->notify(msg);
TelEngine::destruct(pu);
}
void SubscriptionModule::handleMwi(const Message& msg)
{
EventUser* user = getEventUser(false,msg.getValue("notifier"),"message-summary");
if (user)
user->notifyMwi(msg);
}
// Handle 'resource.subscribe' messages with (un)subscribe operation
bool SubscriptionModule::handleResSubscribe(bool sub, const String& subscriber,
const String& notifier, Message& msg)
{
DDebug(this,DebugAll,"handleResSubscribe(%s) subscriber=%s notifier=%s",
String::boolText(sub),subscriber.c_str(),notifier.c_str());
// Check if the subscriber and/or notifier are in the list (our server)
PresenceUser* from = m_users.getUser(subscriber);
PresenceUser* to = m_users.getUser(notifier);
bool rsp = false;
// Process the subscriber's state. Use a while() to break
while (from) {
Lock lock(from);
Contact* c = from->findContact(notifier);
Message* m = 0;
bool newContact = (c == 0);
if (c) {
if (sub) {
// Subscription request
// Not subscribed: remember pending out request
// Subscribed: reset pending out flag if set
if (c->m_subscription.to() == c->m_subscription.pendingOut()) {
if (!c->m_subscription.to())
c->m_subscription.set(SubscriptionState::PendingOut);
else
c->m_subscription.reset(SubscriptionState::PendingOut);
m = c->buildUpdateDb(subscriber);
}
}
else {
// Subscription termination request
bool changed = c->m_subscription.to() || c->m_subscription.pendingOut();
// Make sure the 'To' and 'PendingOut' are not set
c->m_subscription.reset(SubscriptionState::To | SubscriptionState::PendingOut);
if (changed)
m = c->buildUpdateDb(subscriber);
}
}
else {
if (sub) {
// Add 'notifier' to the contact list if subscription is requested
// TODO: Check credentials
c = new Contact(notifier,SubscriptionState::PendingOut);
m = c->buildUpdateDb(subscriber,true);
}
if (!c)
break;
}
lock.drop();
if (m)
m = queryDb(m);
if (m) {
bool ok = true;
if (newContact) {
// Append the new contact. Check if not already added while not locked
Lock lck(from);
ok = !from->findContact(notifier);
if (ok)
from->appendContact(c);
else
TelEngine::destruct(c);
}
// Notify changes
if (ok)
notifyRosterUpdate(subscriber,notifier);
TelEngine::destruct(m);
}
break;
}
// Process the notifier's state. Use a while() to break
while (to) {
Lock lock(to);
Contact* c = to->findContact(subscriber);
if (!c)
break;
Message* m = 0;
bool unsubscribed = !sub && c->m_subscription.from();
rsp = !sub || c->m_subscription.from();
if (sub) {
// Subscription request
// Not subscribed: remember pending in request
// Subscribed: reset pending in flag if set
if (c->m_subscription.from() == c->m_subscription.pendingIn()) {
if (!c->m_subscription.from())
c->m_subscription.set(SubscriptionState::PendingIn);
else
c->m_subscription.reset(SubscriptionState::PendingIn);
m = c->buildUpdateDb(notifier);
}
}
else {
if (c->m_subscription.from() || c->m_subscription.pendingIn()) {
c->m_subscription.reset(SubscriptionState::From | SubscriptionState::PendingIn);
m = c->buildUpdateDb(notifier);
}
}
lock.drop();
if (m)
TelEngine::destruct(queryDb(m));
// Notify subscription change and 'offline'
if (unsubscribed) {
notify(false,notifier,subscriber);
notifyRosterUpdate(notifier,subscriber);
}
// Respond on behalf of the notifier
if (rsp) {
// Internally handle the message before sending it if the destination was found
// (update destination data)
if (from) {
Message tmp("resource.notify");
handleResNotifySub(sub,notifier,subscriber,tmp);
}
subscribed(sub,notifier,subscriber);
}
break;
}
TelEngine::destruct(from);
TelEngine::destruct(to);
return rsp;
}
// Handle 'resource.subscribe' messages with query operation
bool SubscriptionModule::handleResSubscribeQuery(const String& subscriber,
const String& notifier, Message& msg)
{
DDebug(this,DebugAll,"handleResSubscribeQuery() subscriber=%s notifier=%s",
subscriber.c_str(),notifier.c_str());
if (subscriber == notifier)
return true;
bool ok = false;
// Check generic users
GenericUser* gu = m_genericUsers.findUser(notifier);
if (gu) {
gu->lock();
GenericContact* c = gu->find(subscriber);
ok = c != 0;
gu->unlock();
TelEngine::destruct(gu);
if (ok)
return true;
}
PresenceUser* u = m_users.getUser(notifier);
if (u) {
u->lock();
Contact* c = u->findContact(subscriber);
ok = c && c->m_subscription.from();
u->unlock();
TelEngine::destruct(u);
}
DDebug(this,DebugInfo,"handleResSubscribeQuery() subscriber=%s notifier=%s auth=%u",
subscriber.c_str(),notifier.c_str(),ok);
return ok;
}
// Handle online/offline resource.notify from contact
bool SubscriptionModule::handleResNotify(bool online, Message& msg)
{
String* contact = msg.getParam("contact");
if (TelEngine::null(contact)) {
// TODO: handle generic users
// TODO: handle offline without 'to' or without instance
if (!msg.getBoolValue("to_local",true))
return false;
String* inst = msg.getParam("from_instance");
if (TelEngine::null(inst))
return false;
String* from = msg.getParam("from");
String* to = msg.getParam("to");
if(TelEngine::null(from) || TelEngine::null(to))
return false;
DDebug(this,DebugAll,"handleResNotify(%s) from=%s instance=%s to=%s",
String::boolText(online),from->c_str(),inst->c_str(),to->c_str());
PresenceUser* u = m_users.getUser(*to);
if (!u)
return false;
u->lock();
Contact* c = u->findContact(*from);
if (c) {
if (online) {
Instance* i = c->m_instances.set(*inst,msg.getIntValue("priority"));
String* capsid = msg.getParam("caps.id");
if (!TelEngine::null(capsid))
i->setCaps(*capsid,msg);
}
else
c->m_instances.remove(*inst);
}
u->unlock();
TelEngine::destruct(u);
return false;
}
String* inst = msg.getParam("instance");
DDebug(this,DebugAll,"handleResNotify(%s) contact=%s instance=%s",
String::boolText(online),contact->c_str(),TelEngine::c_safe(inst));
PresenceUser* u = m_users.getUser(*contact);
if (!u)
return false;
u->lock();
bool notify = false;
bool newInstance = false;
if (online) {
// Update/add instance. Set notify
if (!TelEngine::null(inst)) {
notify = true;
int prio = msg.getIntValue("priority");
Instance* i = u->instances().set(*inst,prio,&newInstance);
String* capsid = msg.getParam("caps.id");
if (!TelEngine::null(capsid))
i->setCaps(*capsid,msg);
if (newInstance)
DDebug(this,DebugAll,"handleResNotify(online) user=%s added instance=%s prio=%d",
contact->c_str(),inst->c_str(),prio);
}
}
else {
// Remove instance or clear the list
if (!TelEngine::null(inst)) {
Instance* i = u->instances().removeInstance(*inst,false);
if (i) {
notify = true;
DDebug(this,DebugAll,"handleResNotify(offline) user=%s removed instance=%s",
contact->c_str(),inst->c_str());
TelEngine::destruct(i);
}
}
else {
notify = (0 != u->instances().skipNull());
if (notify) {
DDebug(this,DebugAll,"handleResNotify(offline) user=%s removed %u instances",
contact->c_str(),u->instances().count());
u->instances().clear();
}
}
}
if (notify) {
const char* data = msg.getValue("data");
// Notify contacts (from user) and new online user (from contacts)
// Send pending in subscription requests to user's new instance
// Re-send pending out subscription requests each time a new instance is notified
for (ObjList* o = u->m_list.skipNull(); o; o = o->skipNext()) {
Contact* c = static_cast<Contact*>(o->get());
if (newInstance && c->m_subscription.pendingIn())
subscribe(true,c->toString(),u->toString(),inst);
bool fromContact = newInstance && c->m_subscription.to();
bool pendingOut = !fromContact && newInstance && c->m_subscription.pendingOut();
if (!(c->m_subscription.from() || fromContact || pendingOut))
continue;
PresenceUser* dest = m_users.getUser(*c);
if (!dest) {
// User not found, it may belong to other domain
// Send presence and probe it if our user is online
if (c->m_subscription.from()) {
if (online)
__plugin.notify(true,u->toString(),*c,*inst,String::empty(),data);
else
__plugin.notify(false,u->toString(),*c,inst ? *inst : String::empty());
}
if (online) {
probe(u->toString(),*c);
if (pendingOut)
subscribe(true,u->toString(),c->toString());
}
continue;
}
dest->lock();
// Notify user's instance to all contact's instances
if (c->m_subscription.from())
dest->instances().notifyInstance(online,false,u->toString(),
dest->toString(),inst ? *inst : String::empty(),data);
// Notify all contact's instances to the new user's instance
if (fromContact)
dest->instances().notifyUpdate(online,dest->toString(),
u->toString(),*inst);
else if (pendingOut) {
// Both parties are known: handle pending out internally
Message tmp("resource.subscribe");
handleResSubscribe(true,u->toString(),c->toString(),tmp);
}
dest->unlock();
TelEngine::destruct(dest);
}
// Notify the instance to all other user's instance
// Notify a new instance about other user's instances
if (!TelEngine::null(inst)) {
u->instances().notifySkip(online,false,u->toString(),*inst,data);
if (newInstance && online)
u->instances().notifySkip(online,true,u->toString(),*inst,data);
}
}
u->unlock();
TelEngine::destruct(u);
return false;
}
// Handle resource.notify with operation (un)subscribed
bool SubscriptionModule::handleResNotifySub(bool sub, const String& src, const String& dest,
Message& msg)
{
DDebug(this,DebugAll,"handleResNotifySub(%s,%s,%s)",
String::boolText(sub),src.c_str(),dest.c_str());
PresenceUser* from = m_users.getUser(src);
PresenceUser* to = m_users.getUser(dest);
while (from) {
Lock lock(from);
Contact* c = from->findContact(dest);
bool notify = false;
// Add it to the list if subscribed and not found
if (!c && sub) {
c = new Contact(dest,SubscriptionState::From);
Message* m = c->buildUpdateDb(src,true);
m = queryDb(m);
if (m) {
from->appendContact(c);
TelEngine::destruct(m);
notify = true;
}
else
TelEngine::destruct(c);
}
if (!c)
break;
bool changed = c->m_subscription.pendingIn();
c->m_subscription.reset(SubscriptionState::PendingIn);
if (sub) {
if (!c->m_subscription.from()) {
c->m_subscription.set(SubscriptionState::From);
changed = true;
notify = true;
}
}
else {
if (c->m_subscription.from()) {
c->m_subscription.reset(SubscriptionState::From);
changed = true;
notify = true;
}
}
Message* m = 0;
if (changed)
m = c->buildUpdateDb(src);
lock.drop();
m = queryDb(m);
// Notify user roster change on success
if (m) {
TelEngine::destruct(m);
if (notify)
notifyRosterUpdate(src,dest);
}
// Notify user presence to contact if subscribed to its presence
if (notify) {
if (to) {
Lock2 lck(from,to);
notifyInstances(sub,*from,*to);
}
else
probe(src,dest);
}
break;
}
while (to) {
Lock lock(to);
Contact* c = to->findContact(src);
if (!c)
break;
bool changed = c->m_subscription.test(SubscriptionState::PendingOut);
c->m_subscription.reset(SubscriptionState::PendingOut);
bool notify = !sub && changed;
if (sub) {
if (!c->m_subscription.to()) {
c->m_subscription.set(SubscriptionState::To);
changed = true;
notify = true;
}
}
else {
if (c->m_subscription.to()) {
c->m_subscription.reset(SubscriptionState::To);
changed = true;
notify = true;
}
}
Message* m = 0;
if (changed)
m = c->buildUpdateDb(dest);
bool subscribed = c->m_subscription.to();
lock.drop();
m = queryDb(m);
// Notify user roster change on success
if (m) {
TelEngine::destruct(m);
if (notify)
notifyRosterUpdate(dest,src);
}
// Notify user presence to contact if subscribed to its presence
if (notify && subscribed) {
if (from) {
Lock2 lck(to,from);
notifyInstances(sub,*to,*from);
}
else
probe(dest,src);
}
break;
}
TelEngine::destruct(from);
TelEngine::destruct(to);
return false;
}
// Handle resource.notify with operation probe
bool SubscriptionModule::handleResNotifyProbe(const String& from, const String& to,
Message& msg)
{
bool toLocal = msg.getBoolValue("to_local");
DDebug(this,DebugAll,"handleResNotifyProbe(%s,%s) toLocal=%u",
from.c_str(),to.c_str(),toLocal);
const String* src = 0;
const String* dest = 0;
if (toLocal) {
src = &from;
dest = &to;
}
else {
src = &to;
dest = &from;
}
PresenceUser* user = m_users.getUser(*dest);
if (!user)
return false;
user->lock();
bool ok = false;
Contact* c = 0;
if (from != to) {
c = user->findContact(*src);
ok = c && c->m_subscription.from();
}
else
ok = true;
bool sync = msg.getBoolValue("sync");
if (ok) {
if (sync) {
unsigned int n = 0;
if (toLocal)
n = user->instances().addListParam(msg);
else if (c)
n = c->m_instances.addListParam(msg);
msg.setParam("instance.count",String(n));
}
else {
String* inst = msg.getParam("from_instance");
user->instances().notifyUpdate(true,*dest,*src,inst ? *inst : String::empty());
}
}
user->unlock();
TelEngine::destruct(user);
return ok || sync;
}
// Update capabilities for all instances with the given caps id
void SubscriptionModule::updateCaps(const String& capsid, NamedList& list)
{
m_users.lock();
for (ObjList* o = m_users.users().skipNull(); o; o = o->skipNext()) {
PresenceUser* u = static_cast<PresenceUser*>(o->get());
u->instances().updateCaps(capsid,list);
for (ObjList* c = u->m_list.skipNull(); c; c = c->skipNext())
(static_cast<Contact*>(c->get()))->m_instances.updateCaps(capsid,list);
}
m_users.unlock();
// TODO: handle generic users
}
// Handle 'user.roster' messages with operation 'query'
bool SubscriptionModule::handleUserRosterQuery(const String& user, const String* contact,
Message& msg)
{
DDebug(this,DebugAll,"handleUserRosterQuery() user=%s contact=%s",
user.c_str(),TelEngine::c_safe(contact));
Message* m = 0;
NamedList p("");
p.addParam("username",user);
if (TelEngine::null(contact))
m = buildDb(m_account,m_userLoadQuery,p);
else {
p.addParam("contact",*contact);
m = buildDb(m_account,m_contactLoadQuery,p);
}
m = queryDb(m);
if (!m)
return false;
Array* a = 0;
if (m->getIntValue("rows") >= 1)
a = static_cast<Array*>(m->userObject("Array"));
unsigned int n = 0;
if (a) {
int rows = a->getRows();
int cols = a->getColumns();
for (int row = 1; row < rows; row++) {
String cPrefix("contact.");
cPrefix << String(++n);
String prefix(cPrefix);
prefix << ".";
for (int col = 1; col < cols; col++) {
String* name = YOBJECT(String,a->get(col,0));
if (!name || *name == "username")
continue;
String* value = YOBJECT(String,a->get(col,row));
if (!value)
continue;
if (*name == "contact")
msg.addParam(cPrefix,*value);
else
msg.addParam(prefix + *name,*value);
}
}
}
if (n)
msg.addParam("contact.count",String(n));
TelEngine::destruct(m);
return true;
}
// Handle 'user.roster' messages with operation 'update'
bool SubscriptionModule::handleUserRosterUpdate(const String& user, const String& contact,
Message& msg)
{
DDebug(this,DebugAll,"handleUserRosterUpdate() user=%s contact=%s",
user.c_str(),contact.c_str());
NamedList p("");
String params("username,contact");
NamedString* cParams = msg.getParam("contact.parameters");
if (!TelEngine::null(cParams))
params.append(*cParams,",");
p.copyParams(msg,params);
Message* m = buildDb(m_account,m_contactSetQuery,p);
m = queryDb(m);
if (!m)
return false;
// Load the contact to get all its data
// The data will be used to notify changes and handle contact
// subscription related notifications
// Notify the update before notifying the instances
Array* contactData = notifyRosterUpdate(user,contact,true);
if (!contactData)
return true;
// Find the user and (re)load the contact (its subscription might change or
// it can be a new contact)
PresenceUser* u = m_users.getUser(user);
if (!u) {
TelEngine::destruct(contactData);
return true;
}
u->lock();
SubscriptionState oldSub;
Contact* c = u->findContact(contact);
bool newContact = (c == 0);
if (c) {
oldSub.replace((int)c->m_subscription);
c->set(*contactData,1);
}
else {
c = Contact::build(*contactData,1);
if (c)
u->appendContact(c);
}
TelEngine::destruct(contactData);
// Notify instances
if (c) {
PresenceUser* dest = m_users.getUser(contact);
Lock lock(dest);
bool doProbe = false;
// To contact if it's subscribed to user's presence and it's new one
// or subscription changed
if (c->m_subscription.from() && (newContact || !oldSub.from())) {
if (dest) {
if (dest->instances().skipNull() && u->instances().skipNull())
u->instances().notifyUpdate(true,user,contact,dest->instances());
}
else
doProbe = true;
}
// From contact to user
if (c->m_subscription.to()) {
if (newContact)
doProbe = (dest == 0);
else if (!oldSub.to()) {
if (dest) {
if (dest->instances().skipNull() && u->instances().skipNull())
dest->instances().notifyUpdate(true,contact,user,u->instances());
}
else
doProbe = true;
}
}
lock.drop();
TelEngine::destruct(dest);
if (doProbe && c->m_subscription.to())
probe(user,contact);
}
u->unlock();
TelEngine::destruct(u);
return true;
}
// Handle 'user.roster' messages with operation 'delete'
bool SubscriptionModule::handleUserRosterDelete(const String& user, const String& contact,
Message& msg)
{
DDebug(this,DebugAll,"handleUserRosterDelete() user=%s contact=%s",
user.c_str(),contact.c_str());
Message* m = buildDb(m_account,m_contactDeleteQuery,msg);
m = queryDb(m);
if (!m)
return false;
TelEngine::destruct(m);
// Find the user before notifying the operation: notify instances before remove
PresenceUser* u = m_users.getUser(user);
if (u) {
u->lock();
Contact* c = u->removeContact(contact,false);
if (c) {
// Notify 'offline' to both parties
if (c->m_subscription.to())
notify(false,contact,user);
if (c->m_subscription.from())
notify(false,user,contact);
// Contact is a known user: update user subscription in it's list and
// notify it if it has any instances
// Unknown user: unsubcribe it and request unsubscribe
PresenceUser* uc = m_users.getUser(contact);
if (uc) {
uc->lock();
Contact* cc = uc->findContact(user);
if (cc) {
int flgs = SubscriptionState::From | SubscriptionState::To |
SubscriptionState::PendingOut;
bool update = cc->m_subscription.test(flgs);
bool changed = update || cc->m_subscription.pendingIn();
cc->m_subscription.reset(flgs | SubscriptionState::PendingIn);
// Save data before update notification (use saved data in notification)
if (changed) {
Message* m = cc->buildUpdateDb(contact);
TelEngine::destruct(queryDb(m));
}
if (update)
notifyRosterUpdate(contact,user,false,false);
}
uc->unlock();
TelEngine::destruct(uc);
}
else {
subscribed(false,user,contact);
subscribe(false,user,contact);
}
TelEngine::destruct(uc);
TelEngine::destruct(c);
}
u->unlock();
TelEngine::destruct(u);
}
Message* mu = message("user.roster");
mu->addParam("notify","delete");
mu->addParam("username",user);
mu->addParam("contact",contact);
Engine::enqueue(mu);
return true;
}
// Handle 'user.update' messages with operation 'delete'
void SubscriptionModule::handleUserUpdateDelete(const String& user, Message& msg)
{
DDebug(this,DebugAll,"handleUserUpdateDelete() user=%s",user.c_str());
PresenceUser* u = m_users.getUser(user);
if (!u)
return;
u->lock();
for (ObjList* o = u->m_list.skipNull(); o; o = o->skipNext()) {
Contact* c = static_cast<Contact*>(o->get());
if (c->m_subscription.from())
notify(false,user,c->toString());
}
u->unlock();
TelEngine::destruct(u);
// Remove the user from memory and database roster
m_users.removeUser(user);
NamedList p("");
p.addParam("username",user);
Message* m = buildDb(m_account,m_userDeleteQuery,p);
TelEngine::destruct(queryDb(m));
}
// Handle 'msg.route' messages
bool SubscriptionModule::imRoute(Message& msg)
{
String* caller = msg.getParam("caller");
String* called = msg.getParam("called");
if (TelEngine::null(caller) || TelEngine::null(called))
return false;
DDebug(this,DebugAll,"%s caller=%s called=%s",
msg.c_str(),caller->c_str(),called->c_str());
PresenceUser* u = m_users.getUser(*called);
if (!u) {
Debug(this,DebugStub,"%s caller=%s called=%s destination is an unknown user",
msg.c_str(),caller->c_str(),called->c_str());
return false;
}
bool ok = true;
unsigned int n = 0;
u->lock();
String* tmp = msg.getParam("called_instance");
if (TelEngine::null(tmp)) {
String* skip = 0;
if (*caller == *called)
skip = msg.getParam("caller_instance");
else if (!u->findContact(*caller))
ok = false;
if (ok)
n = u->instances().addListParam(msg,skip);
}
else if (u->findContact(*caller) || *caller == *called) {
Instance* inst = u->instances().findInstance(*tmp);
if (inst)
inst->addListParam(msg,++n);
}
else
ok = false;
u->unlock();
TelEngine::destruct(u);
if (ok)
msg.addParam("instance.count",String(n));
return ok && n != 0;
}
void SubscriptionModule::expireSubscriptions()
{
u_int64_t time = Time::msecNow();
unsigned int evCount = m_events.count();
for (unsigned int i = 0;i < evCount;i ++) {
NamedPointer* p = static_cast<NamedPointer*>(m_events.getParam(i));
NamedList* nl = static_cast<NamedList*>(p->userData());
if (!nl)
continue;
unsigned int nlCount = nl->count();
for (unsigned int j = 0;j < nlCount;j++) {
NamedPointer* p1 = static_cast<NamedPointer*>(nl->getParam(j));
if (!p1)
continue;
EventUser* eu = static_cast<EventUser*>(p1->userData());
if (!eu)
continue;
eu->expire(time);
if (eu->m_list.count() == 0) {
nl->clearParam(eu->user());
j--;
}
}
}
}
// Build a database message from account and query.
// Replace query params. Return Message pointer on success
Message* SubscriptionModule::buildDb(const String& account, const String& query,
const NamedList& params)
{
XDebug(this,DebugAll,"buildDb(%s,%s)",account.c_str(),query.c_str());
if (!(account && query))
return 0;
Message* m = new Message("database");
m->addParam("account",account);
String tmp = query;
params.replaceParams(tmp,true);
m->addParam("query",tmp);
return m;
}
// Dispatch a database message
// Return Message pointer on success. Release msg on failure
Message* SubscriptionModule::queryDb(Message*& msg)
{
if (!msg)
return 0;
bool ok = Engine::dispatch(msg) && !msg->getParam("error");
if (!ok) {
Debug(this,DebugNote,"Database query=%s failed error=%s",
msg->getValue("query"),msg->getValue("error"));
TelEngine::destruct(msg);
}
return msg;
}
bool SubscriptionModule::received(Message& msg, int id)
{
switch (id) {
case Timer:
s_check = true;
break;
case ImRoute:
return imRoute(msg);
case Halt:
Lock lock(this);
if (m_expire)
m_expire->cancel(false);
lock.drop();
while (m_expire)
Thread::yield();
// Uninstall message handlers
for (ObjList* o = m_handlers.skipNull(); o; o = o->skipNext()) {
SubMessageHandler* h = static_cast<SubMessageHandler*>(o->get());
Engine::uninstall(h);
}
DDebug(this,DebugAll,"Halted");
break;
}
return Module::received(msg,id);
}
bool SubscriptionModule::commandExecute(String& retVal, const String& line)
{
String l = line;
l.startSkip(name());
l.trimSpaces();
if (l.startSkip("status")) {
l.trimSpaces();
String user = "";
// extractName(l,user);
String contact = "";
// extractName(l.substr(user.length() + 2,-1),contact);
if (user.null() || contact.null()) {
retVal << "Espected <PresenceUser,Contact> pair";
DDebug(this,DebugInfo,"Command Execute 2 : return false user->null() || contact->null()");
return false;
}
DDebug(this,DebugInfo,"Command Execute , operation status for: %s, to %s",user.c_str(),contact.c_str());
// retVal << "Subscription state for user: " << user << " and contact: "
// << contact << " is: " << m_users.getSubscription(user,contact);
return true;
}
if (l.startSkip("unsubscribe")) {
l.trimSpaces();
String* contact = new String();
String* user = new String();
ObjList* ob = l.split(' ',false);
int counter = 0;
for (ObjList* o = ob->skipNull(); o; o = o->skipNext()) {
switch (counter) {
case 0:
user = static_cast<String*>(o->get());
break;
case 1:
contact = static_cast<String*>(o->get());
break;
default:
retVal << "Espected <PresenceUser,Contact> pair";
return false;
}
counter += 1;
}
if (user->null() || contact->null()) {
retVal << "Espected <PresenceUser,Contact> pair";
return false;
}
// TODO unsubscribe the user
retVal << "PresenceUser: " << *user << " succesfuly unsubscribed from " << *contact << "'s presence";
}
return false;
}
bool SubscriptionModule::commandComplete(Message& msg, const String& partLine,
const String& partWord)
{
if (partLine.null() && partWord.null())
return false;
if (partLine.null() || (partLine == "help"))
Module::itemComplete(msg.retValue(),name(),partWord);
else if (partLine == name()) {
for (const char** list = s_cmds; *list; list++)
Module::itemComplete(msg.retValue(),*list,partWord);
return true;
}
return Module::commandComplete(msg,partLine,partWord);
}
// Notify 'from' instances to 'to'
void SubscriptionModule::notifyInstances(bool online, PresenceUser& from, PresenceUser& to)
{
if (!to.instances().skipNull())
return;
// Source has instances: notify them to destination
// Source has no instance: notify offline to destination
if (from.instances().skipNull()) {
if (online || !s_singleOffline)
from.instances().notifyUpdate(online,from.toString(),to.toString(),to.instances());
else
notify(false,from.toString(),to.toString());
}
else if (online)
to.instances().notifyInstance(false,false,from.toString(),to.toString(),String::empty(),0);
}
} /* anonymous namespace */
/* vi: set ts=8 sw=4 sts=4 noet: */