diff --git a/engine/Client.cpp b/engine/Client.cpp index e56367b2..3192a15e 100644 --- a/engine/Client.cpp +++ b/engine/Client.cpp @@ -3894,6 +3894,13 @@ MucRoom* ClientAccount::findRoomByUri(const String& uri, bool ref) return findRoom(id,ref); } +// Find any contact (regular or MUC room) by its id +ClientContact* ClientAccount::findAnyContact(const String& id, bool ref) +{ + ClientContact* c = findContact(id,ref); + return c ? c : findRoom(id,ref); +} + // Build a contact and append it to the list ClientContact* ClientAccount::appendContact(const String& id, const char* name, const char* uri) @@ -3940,6 +3947,24 @@ ClientContact* ClientAccount::removeContact(const String& id, bool delObj) return c; } +// Clear MUC rooms +void ClientAccount::clearRooms(bool saved, bool temp) +{ + if (!(saved || temp)) + return; + Lock lock(this); + ListIterator iter(m_mucs); + for (GenObject* gen = 0; 0 != (gen = iter.get());) { + MucRoom* r = static_cast(gen); + if (r->local() || r->remote()) { + if (saved) + m_mucs.remove(r); + } + else if (temp) + m_mucs.remove(r); + } +} + // Build a login/logout message from account's data Message* ClientAccount::userlogin(bool login, const char* msg) { @@ -3953,6 +3978,44 @@ Message* ClientAccount::userlogin(bool login, const char* msg) return m; } +// Build a message used to update or query account userdata +Message* ClientAccount::userData(bool update, const String& data, const char* msg) +{ + Message* m = Client::buildMessage(msg,toString(),update ? "update" : "query"); + m->addParam("data",data,false); + if (!update || data != "chatrooms") + return m; + m->setParam("data.count","0"); + unsigned int n = 0; + Lock lock(this); + for (ObjList* o = m_mucs.skipNull(); o; o = o->skipNext()) { + MucRoom* r = static_cast(o->get()); + if (!r->remote()) + continue; + String prefix; + prefix << "data." << ++n; + m->addParam(prefix,r->uri()); + prefix << "."; + m->addParam(prefix + "name",r->m_name,false); + if (r->m_password) { + Base64 b((void*)r->m_password.c_str(),r->m_password.length()); + String tmp; + b.encode(tmp); + m->addParam(prefix + "password",tmp); + } + for (ObjList* o = r->groups().skipNull(); o; o = o->skipNext()) + m->addParam(prefix + "group",o->get()->toString(),false); + NamedIterator iter(r->m_params); + for (const NamedString* ns = 0; 0 != (ns = iter.get());) { + // Skip local/remote params + if (ns->name() != "local" && ns->name() != "remote") + m->addParam(prefix + ns->name(),*ns); + } + } + m->setParam("data.count",String(n)); + return m; +} + // Fill a list used to update an account list item void ClientAccount::fillItemParams(NamedList& list) { @@ -3964,6 +4027,225 @@ void ClientAccount::fillItemParams(NamedList& list) list.addParam(status); } +// Utility used in ClientAccount::setupDataDir +static bool showAccError(ClientAccount* a, String* errStr, const String& fail, + const char* what, int error = 0, const char* errorStr = 0) +{ + String tmp; + if (!errStr) + errStr = &tmp; + if (error) { + Thread::errorString(*errStr,error); + *errStr = String(error) + " " + *errStr; + } + else + *errStr = errorStr; + *errStr = fail + " '" + what + "': " + *errStr; + Debug(ClientDriver::self(),DebugWarn,"Account(%s) %s [%p]", + a->toString().c_str(),errStr->c_str(),a); + return false; +} + +// Set account data directory. Make sure it exists. +// Move all files from the old one if changed +bool ClientAccount::setupDataDir(String* errStr, bool saveAcc) +{ + String dir; + String user = m_params["username"]; + user.toLower(); + String domain = m_params.getValue("domain",m_params.getValue("server")); + domain.toLower(); + dir << protocol().hash() << "_" << user.hash() << "_" << domain.hash(); + if (dataDir() == dir) { + String s; + s << Engine::configPath(true) << Engine::pathSeparator() << dataDir(); + ObjList d; + ObjList f; + File::listDirectory(s,&d,&f); + if (d.find(dataDir())) + return true; + if (f.find(dataDir())) + return showAccError(this,errStr,"Failed to create directory",s,0, + "A file with the same name already exists"); + // Not found: clear old directory + m_params.clearParam("datadirectory"); + } + String path = Engine::configPath(true); + // Check if already there + int error = 0; + ObjList dirs; + ObjList files; + File::listDirectory(path,&dirs,&files); + if (error) + return showAccError(this,errStr,"Failed to list directory",path,error); + String fullPath = path + Engine::pathSeparator() + dir; + if (files.find(dir)) + return showAccError(this,errStr,"Failed to create directory",fullPath,0, + "A file with the same name already exists"); + const String& existing = dataDir(); + ObjList* oldDir = existing ? dirs.find(existing) : 0; + if (dirs.find(dir)) { + if (oldDir) { + // Move dirs and files from old directory + String old = path + Engine::pathSeparator() + existing; + ObjList all; + File::listDirectory(old,&all,&all,&error); + if (!error) { + bool ok = true; + for (ObjList* o = all.skipNull(); o; o = o->skipNext()) { + String* item = static_cast(o->get()); + String oldItem = old + Engine::pathSeparator() + *item; + String newItem = fullPath + Engine::pathSeparator() + *item; + File::rename(oldItem,newItem,&error); + if (!error) + continue; + ok = false; + String tmp; + Thread::errorString(tmp,error); + Debug(ClientDriver::self(),DebugWarn, + "Account(%s) failed to move '%s' to '%s': %d %s [%p]", + toString().c_str(),oldItem.c_str(),newItem.c_str(), + error,tmp.c_str(),this); + error = 0; + } + // Delete it if all moved + if (ok) { + File::rmDir(old,&error); + if (error) + showAccError(this,errStr,"Failed to delete directory",old,error); + } + } + else + showAccError(this,errStr,"Failed to list directory",old,error); + } + } + else { + if (oldDir) { + // Rename it + String old = path + Engine::pathSeparator() + existing; + File::rename(old,fullPath,&error); + if (error) + return showAccError(this,errStr,"Failed to rename existing directory", + old,error); + } + else { + // Create a new one + File::mkDir(fullPath,&error); + if (error) + return showAccError(this,errStr,"Failed to create directory", + fullPath,error); + } + } + m_params.setParam("datadirectory",dir); + if (saveAcc) { + NamedList* sect = Client::s_accounts.getSection(toString()); + if (sect) { + sect->setParam("datadirectory",dir); + Client::s_accounts.save(); + } + } + // Set account meta data + loadDataDirCfg(); + NamedList* sect = m_cfg.createSection("general"); + sect->setParam("account",toString()); + sect->copyParams(m_params,"protocol,username,domain,server"); + m_cfg.save(); + return true; +} + +// Load configuration file from data directory +bool ClientAccount::loadDataDirCfg(Configuration* cfg, const char* file) +{ + if (TelEngine::null(file)) + return false; + if (!cfg) + cfg = &m_cfg; + if (!dataDir()) + setupDataDir(0,false); + const String& dir = dataDir(); + if (!dir) + return false; + *cfg = ""; + *cfg << Engine::configPath(true) + Engine::pathSeparator() + dir; + *cfg << Engine::pathSeparator() << file; + return cfg->load(); +} + +// Clear account data directory +bool ClientAccount::clearDataDir(String* errStr) +{ + if (!dataDir()) + setupDataDir(0,false); + const String& dir = dataDir(); + if (!dir) + return false; + // Check base path + String tmp(Engine::configPath(true)); + ObjList dirs; + File::listDirectory(tmp,&dirs,0); + if (!dirs.find(dir)) + return true; + // Delete files + tmp << Engine::pathSeparator() << dir << Engine::pathSeparator(); + int error = 0; + bool ok = false; + ObjList files; + if (File::listDirectory(tmp,0,&files,&error)) { + for (ObjList* o = files.skipNull(); o; o = o->skipNext()) { + String file(tmp + o->get()->toString()); + int err = 0; + if (!File::remove(file,&err)) { + if (!error) + error = err; + } + } + if (!error) + ok = File::rmDir(tmp,&error); + } + return ok ? ok : showAccError(this,errStr,"Failed to clear data directory",tmp,error); +} + +// Load contacts from configuration file +void ClientAccount::loadContacts(Configuration* cfg) +{ + if (!cfg) + cfg = &m_cfg; + unsigned int n = cfg->sections(); + for (unsigned int i = 0; i < n; i++) { + NamedList* sect = cfg->getSection(i); + if (!(sect && sect->c_str())) + continue; + const String& type = (*sect)["type"]; + if (type == "groupchat") { + String id; + ClientContact::buildContactId(id,toString(),*sect); + MucRoom* room = findRoom(id); + if (!room) + room = new MucRoom(this,id,0,*sect); + room->groups().clear(); + NamedIterator iter(*sect); + for (const NamedString* ns = 0; 0 != (ns = iter.get());) { + if (ns->name() == "type") + continue; + if (ns->name() == "name") + room->m_name = *ns; + else if (ns->name() == "password") + room->m_password = *ns; + else if (ns->name() == "group") { + if (*ns) + room->appendGroup(*ns); + } + else + room->m_params.setParam(ns->name(),*ns); + } + room->setLocal(true); + Debug(ClientDriver::self(),DebugAll, + "Account(%s) loaded MUC room '%s' [%p]", + toString().c_str(),room->uri().c_str(),this); + } + } +} + // Remove from owner. Release data void ClientAccount::destroyed() { @@ -4087,6 +4369,16 @@ MucRoom* ClientAccountList::findRoomByMember(const String& id, bool ref) return acc ? acc->findRoom(contact,ref) : 0; } +// Find any contact (regular or MUC room) by its id +ClientContact* ClientAccountList::findAnyContact(const String& id, bool ref) +{ + String account; + ClientContact::splitContactId(id,account); + Lock lock(this); + ClientAccount* acc = findAccount(account); + return acc ? acc->findAnyContact(id,ref) : 0; +} + // Check if there is a single registered account and return it ClientAccount* ClientAccountList::findSingleRegAccount(const String* skipProto, bool ref) { @@ -4136,8 +4428,8 @@ void ClientAccountList::removeAccount(const String& id) // Constructor. Append itself to the owner's list ClientContact::ClientContact(ClientAccount* owner, const char* id, const char* name, const char* uri) - : m_name(name ? name : id), m_owner(owner), m_online(false), m_uri(uri), - m_dockedChat(false) + : m_name(name ? name : id), m_params(""), m_owner(owner), m_online(false), + m_uri(uri), m_dockedChat(false) { m_dockedChat = Client::valid() && Client::self()->getBoolOpt(Client::OptDockedChat); m_id = id ? id : uri; @@ -4152,7 +4444,7 @@ ClientContact::ClientContact(ClientAccount* owner, const char* id, const char* n // Constructor. Build a contact from a list of parameters. ClientContact::ClientContact(ClientAccount* owner, const NamedList& params, const char* id, const char* uri) - : m_name(params.getValue("name",params)), + : m_name(params.getValue("name",params)), m_params(""), m_owner(owner), m_online(false), m_uri(uri), m_dockedChat(false) { m_dockedChat = Client::valid() && Client::self()->getBoolOpt(Client::OptDockedChat); @@ -4167,7 +4459,7 @@ ClientContact::ClientContact(ClientAccount* owner, const NamedList& params, cons // Constructor. Append itself to the owner's list ClientContact::ClientContact(ClientAccount* owner, const char* id, bool mucRoom) - : m_owner(owner), m_online(false), m_id(id), m_dockedChat(false) + : m_params(""), m_owner(owner), m_online(false), m_id(id), m_dockedChat(false) { if (m_owner) m_owner->appendContact(this,mucRoom); diff --git a/engine/ClientLogic.cpp b/engine/ClientLogic.cpp index c94330cc..4f4ba3a0 100644 --- a/engine/ClientLogic.cpp +++ b/engine/ClientLogic.cpp @@ -320,6 +320,7 @@ static const String s_actionLogout = "acc_logout"; static const String s_chat = "chatcontact_chat"; static const String s_chatCall = "chatcontact_call"; static const String s_chatNew = "chatcontact_new"; +static const String s_chatRoomNew = "chatroom_new"; static const String s_chatShowLog = "chatcontact_showlog"; static const String s_chatEdit = "chatcontact_edit"; static const String s_chatDel = "chatcontact_del"; @@ -334,6 +335,7 @@ static const String s_fileSend = "send_file"; static const String s_fileSendPrefix = "send_file:"; static const String s_mucChgSubject = "room_changesubject"; static const String s_mucChgNick = "room_changenick"; +static const String s_mucSave = "room_save"; static const String s_mucInvite = "room_invite_contacts"; static const String s_mucPrivChat = "room_member_chat"; static const String s_mucKick = "room_member_kick"; @@ -472,6 +474,26 @@ static inline void dumpList(const NamedList& p, const char* text, Window* w = 0) #endif } +// Utility: check if a string changed, set it, return true if changed +static inline bool setChangedString(String& dest, const String& src) +{ + if (dest == src) + return false; + dest = src; + return true; +} + +// Utility: check if a list parametr changed, set it, return true if changed +static inline bool setChangedParam(NamedList& dest, const String& param, + const String& src) +{ + String* exist = dest.getParam(param); + if (exist) + return setChangedString(*exist,src); + dest.addParam(param,src); + return true; +} + // Build contact name: name static inline void buildContactName(String& buf, ClientContact& c) { @@ -1014,12 +1036,19 @@ static bool isPageCallsActive(Window* wnd, bool checkTab) // Retrieve a contact edit/info window. // Create it if requested and not found. // Set failExists to true to return 0 if already exists -static Window* getContactInfoEditWnd(bool edit, ClientContact* c, bool create = false, - bool failExists = false) +static Window* getContactInfoEditWnd(bool edit, bool room, ClientContact* c, + bool create = false, bool failExists = false) { if (!Client::valid()) return 0; - const char* wnd = edit ? "contactedit" : "contactinfo"; + const char* wnd = 0; + if (edit) { + if (c && c->mucRoom()) + room = true; + wnd = !room ? "contactedit" : "chatroomedit"; + } + else + wnd = "contactinfo"; String wname(wnd); wname << "_" << (c ? c->toString().c_str() : String((unsigned int)Time::msecNow()).c_str()); Window* w = Client::self()->getWindow(wname); @@ -1049,7 +1078,8 @@ static void updateChatAccountList(const String& account, bool upd) ObjList* list = Client::listWindows(); for (ObjList* o = (list ? list->skipNull() : 0); o; o = o->skipNext()) { String* id = static_cast(o->get()); - if (!id->startsWith("contactedit_")) + bool isContact = id->startsWith("contactedit_"); + if (!(isContact || id->startsWith("chatroomedit_"))) continue; Window* w = Client::self()->getWindow(*id); if (!w || w->context()) @@ -1262,12 +1292,15 @@ static ClientAccount* selectedAccount(ClientAccountList& accounts, Window* wnd = } // Retrieve the chat contact -static inline ClientContact* selectedChatContact(ClientAccountList& accounts, Window* wnd = 0) +static ClientContact* selectedChatContact(ClientAccountList& accounts, + Window* wnd = 0, bool rooms = true) { String c; if (Client::valid()) Client::self()->getSelect(s_chatContactList,c,wnd); - return c ? accounts.findContact(c) : 0; + if (!c) + return 0; + return rooms ? accounts.findAnyContact(c) : accounts.findContact(c); } // Build account action item from account id @@ -1570,22 +1603,27 @@ static bool loginAccount(ClientLogic* logic, const NamedList& account, bool logi // Fill a list used to update a chat contact UI // data: fill contact name, account ... // status: fill contact status -static void fillChatContact(NamedList& p, ClientContact& c, bool data, bool status) +static void fillChatContact(NamedList& p, ClientContact& c, bool data, bool status, + bool roomContact = false) { if (!(data || status)) return; // Fill status + if (roomContact && c.mucRoom()) + p.addParam("type","chatroom"); if (status) { ClientResource* res = c.status(); int stat = c.online() ? ClientResource::Online : ClientResource::Offline; if (res) stat = res->m_status; - String img = resStatusImage(stat); - p.addParam("image:status_image",img,false); - p.addParam("name_image",img,false); String text; - if (res) - text = res->m_text; + if (!roomContact) { + String img = resStatusImage(stat); + p.addParam("image:status_image",img,false); + p.addParam("name_image",img,false); + if (res) + text = res->m_text; + } p.addParam("status_text",text ? text.c_str() : ClientResource::statusDisplayText(stat)); p.addParam("status",lookup(stat,ClientResource::s_statusName)); } @@ -1614,17 +1652,20 @@ static void enableChatActions(ClientContact* c, bool checkVisible = true) c = 0; } const char* s = String::boolText(c != 0); + bool mucRoom = c && c->mucRoom(); NamedList p(""); p.addParam("active:" + s_chat,s); - p.addParam("active:" + s_chatCall,String::boolText(c && c->findAudioResource())); - p.addParam("active:" + s_fileSend,String::boolText(c && c->findFileTransferResource())); + p.addParam(s_chat,!mucRoom ? "Chat" : "Join"); + p.addParam("active:" + s_chatCall,String::boolText(!mucRoom && c && c->findAudioResource())); + p.addParam("active:" + s_fileSend,String::boolText(!mucRoom && c && c->findFileTransferResource())); p.addParam("active:" + s_chatShowLog,s); p.addParam("active:" + s_chatEdit,s); p.addParam("active:" + s_chatDel,s); - p.addParam("active:" + s_chatInfo,s); - p.addParam("active:" + s_chatSub,s); - p.addParam("active:" + s_chatUnsubd,s); - p.addParam("active:" + s_chatUnsub,s); + const char* noRoomOk = String::boolText(!mucRoom && c); + p.addParam("active:" + s_chatInfo,noRoomOk); + p.addParam("active:" + s_chatSub,noRoomOk); + p.addParam("active:" + s_chatUnsubd,noRoomOk); + p.addParam("active:" + s_chatUnsub,noRoomOk); Client::self()->setParams(&p); } @@ -1794,6 +1835,8 @@ static void createRoomChat(MucRoom& room, MucRoomMember* member = 0, bool active String menuName("menu_" + room.resource().toString()); NamedList* pRoom = new NamedList(menuName); pRoom->addParam("title","Room"); + pRoom->addParam("item:" + s_mucSave,""); + pRoom->addParam("item:",""); pRoom->addParam("item:" + s_mucChgNick,""); pRoom->addParam("item:" + s_mucChgSubject,""); pRoom->addParam("item:",""); @@ -1818,13 +1861,27 @@ static void createRoomChat(MucRoom& room, MucRoomMember* member = 0, bool active room.showChat(member->toString(),true,active); } +// Reset a MUC room. Destroy chat window +static void clearRoom(MucRoom* room) +{ + if (!room) + return; + if (!room->resource().offline()) { + Engine::enqueue(room->buildJoin(false)); + room->resource().setStatus(ClientResource::Offline); + } + room->resource().m_affiliation = MucRoomMember::AffNone; + room->resource().m_role = MucRoomMember::RoleNone; + room->destroyChatWindow(); +} + // Show a contact's info window // Update it and, optionally, activate it static bool updateContactInfo(ClientContact* c, bool create = false, bool activate = false) { if (!c) return false; - Window* w = getContactInfoEditWnd(false,c,create); + Window* w = getContactInfoEditWnd(false,false,c,create); if (!w) return false; NamedList p(""); @@ -1858,16 +1915,19 @@ static bool updateContactInfo(ClientContact* c, bool create = false, bool activa } // Show an edit/add chat contact window -static bool showContactEdit(ClientAccountList& accounts, ClientContact* c = 0) +static bool showContactEdit(ClientAccountList& accounts, bool room = false, + ClientContact* c = 0) { - Window* w = getContactInfoEditWnd(true,c,true,true); + Window* w = getContactInfoEditWnd(true,room,c,true,true); if (!w) { // Activate it if found - w = c ? getContactInfoEditWnd(true,c) : 0; + w = c ? getContactInfoEditWnd(true,room,c) : 0; if (w) Client::self()->setActive(w->id(),true,w); return w != 0; } + if (c && c->mucRoom()) + room = true; NamedList p(""); const char* add = String::boolText(c == 0); const char* edit = String::boolText(c != 0); @@ -1875,53 +1935,93 @@ static bool showContactEdit(ClientAccountList& accounts, ClientContact* c = 0) p.addParam("show:frame_uri",add); p.addParam("show:chatcontact_account",edit); p.addParam("show:chatcontact_uri",edit); - Client::self()->clearTable("groups",w); - // Add groups used by all accounts - NamedList upd(""); - for (ObjList* o = accounts.accounts().skipNull(); o; o = o->skipNext()) { - ClientAccount* a = static_cast(o->get()); - if (!a->hasChat()) - continue; - for (ObjList* oc = a->contacts().skipNull(); oc; oc = oc->skipNext()) { - ClientContact* cc = static_cast(oc->get()); - for (ObjList* og = cc->groups().skipNull(); og; og = og->skipNext()) { - const String& grp = og->get()->toString(); - NamedString* param = upd.getParam(grp); - NamedList* p = 0; - if (!param) { - p = new NamedList(grp); - p->addParam("group",grp); - p->addParam("check:group",String::boolText(c == cc)); - upd.addParam(new NamedPointer(grp,p,String::boolText(true))); - } - else if (c == cc) { - p = YOBJECT(NamedList,param); - if (p) - p->setParam("check:group",String::boolText(true)); + if (!room) { + Client::self()->clearTable("groups",w); + // Add groups used by all accounts + NamedList upd(""); + for (ObjList* o = accounts.accounts().skipNull(); o; o = o->skipNext()) { + ClientAccount* a = static_cast(o->get()); + if (!a->hasChat()) + continue; + for (ObjList* oc = a->contacts().skipNull(); oc; oc = oc->skipNext()) { + ClientContact* cc = static_cast(oc->get()); + for (ObjList* og = cc->groups().skipNull(); og; og = og->skipNext()) { + const String& grp = og->get()->toString(); + NamedString* param = upd.getParam(grp); + NamedList* p = 0; + if (!param) { + p = new NamedList(grp); + p->addParam("group",grp); + p->addParam("check:group",String::boolText(c == cc)); + upd.addParam(new NamedPointer(grp,p,String::boolText(true))); + } + else if (c == cc) { + p = YOBJECT(NamedList,param); + if (p) + p->setParam("check:group",String::boolText(true)); + } } } } + Client::self()->updateTableRows("groups",&upd,false,w); + p.addParam("show:request_subscribe",String::boolText(c == 0)); } - Client::self()->updateTableRows("groups",&upd,false,w); - p.addParam("show:request_subscribe",String::boolText(c == 0)); if (c) { p.addParam("context",c->toString()); - String title("Edit friend "); - if (c->m_name && (c->m_name != c->uri())) - title << "'" << c->m_name << "' "; + String title; + if (!room) { + title = "Edit friend "; + if (c->m_name && (c->m_name != c->uri())) + title << "'" << c->m_name << "' "; + } + else + title = "Edit chat room "; title << "<" << c->uri() << ">"; p.addParam("title",title); p.addParam("chatcontact_account",c->accountName()); - p.addParam("chatcontact_uri",c->uri()); p.addParam("name",c->m_name); + p.addParam("chatcontact_uri",c->uri()); + MucRoom* r = room ? c->mucRoom() : 0; + if (r) { + p.addParam("nick",r->m_params.getValue("nick")); + p.addParam("password",r->m_password); + ObjList* grp = r->groups().skipNull(); + p.addParam("group",grp ? grp->get()->toString() : String::empty()); + p.addParam("check:history",r->m_params.getValue("history")); + int val = r->m_params.getIntValue("historylast"); + p.addParam("check:historylast",String::boolText(val > 0)); + p.addParam("historylast_value",val > 0 ? String(val).c_str() : "30"); + if (r->local() || r->remote()) { + p.addParam("check:save_local",String::boolText(r->local(true))); + p.addParam("check:save_remote",String::boolText(r->remote(true))); + } + else { + // Temporary room: connected from wizard + p.addParam("check:save_local",String::boolText(true)); + p.addParam("check:save_remote",String::boolText(true)); + } + } } else { p.addParam("context",""); - p.addParam("title","Add friend"); p.addParam("username",""); p.addParam("domain",""); p.addParam("name",""); - p.addParam("check:request_subscribe",String::boolText(true)); + if (!room) { + p.addParam("title","Add friend"); + p.addParam("check:request_subscribe",String::boolText(true)); + } + else { + p.addParam("title","Add chat room"); + p.addParam("nick",""); + p.addParam("password",""); + p.addParam("group","Rooms"); + p.addParam("check:history",String::boolText(true)); + p.addParam("check:historylast",String::boolText(false)); + p.addParam("historylast_value","30"); + p.addParam("check:save_local",String::boolText(true)); + p.addParam("check:save_remote",String::boolText(true)); + } // Fill accounts. Select single account Client::self()->addOption(s_chatAccount,s_notSelected,false,String::empty(),w); for (ObjList* o = accounts.accounts().skipNull(); o; o = o->skipNext()) { @@ -2899,10 +2999,27 @@ bool JoinMucWizard::changePage(const String& page, const String& old) break; } if (page == "pageChooseRoomServer") { - if (old == "pageAccount" && !account(s_mucAccounts)) { + ClientAccount* a = account(s_mucAccounts); + if (old == "pageAccount" && !a) { showError(window(),"You must select an account"); return false; } + // Add rooms from account + Client::self()->clearTable(s_mucSavedRooms,w); + if (a) { + for (ObjList* o = a->mucs().skipNull(); o; o = o->skipNext()) { + MucRoom* r = static_cast(o->get()); + if (r->local() || r->remote()) + Client::self()->updateTableRow(s_mucSavedRooms,r->uri(),0,false,w); + } + } + // Add saved rooms + unsigned int n = s_mucRooms.sections(); + for (unsigned int i = 0; i < n; i++) { + NamedList* sect = s_mucRooms.getSection(i); + if (sect) + Client::self()->updateTableRow(s_mucSavedRooms,*sect,0,false,w); + } bool useSaved = true; if (w) { String tmp; @@ -2989,25 +3106,40 @@ bool JoinMucWizard::changePage(const String& page, const String& old) Client::self()->getSelect(s_mucSavedRooms,tmp,w); if (!tmp) return false; - bool ok = false; - NamedList* sect = s_mucRooms.getSection(tmp); - if (sect) { + MucRoom* r = acc->findRoomByUri(tmp); + if (r && !(r->local() || r->remote())) + r = 0; + NamedList* sect = !r ? s_mucRooms.getSection(tmp) : 0; + if (r) { + room = r->uri().getUser(); + server = r->uri().getHost(); + } + else if (sect) { URI uri(*sect); room = uri.getUser(); server = uri.getHost(); - ok = room && server; } + bool ok = room && server; if (ok) { - nick = (*sect)["nick"]; - pwd = (*sect)["password"]; - history = sect->getBoolValue("history",true); - if (history) { - int lm = sect->getIntValue("history.newer"); - if (lm > 0) - lastMinutes = (unsigned int)lm; + int lm = 0; + if (r) { + nick = r->m_params["nick"]; + pwd = r->m_password; + history = r->m_params.getBoolValue("history",true); + if (history) + lm = r->m_params.getIntValue("historylast"); } + else { + nick = (*sect)["nick"]; + pwd = (*sect)["password"]; + history = sect->getBoolValue("history",true); + if (history) + lm = sect->getIntValue("history.newer"); + } + if (lm > 0) + lastMinutes = (unsigned int)lm; } - else { + else if (!r) { Client::self()->delTableRow(s_mucSavedRooms,tmp,w); s_mucRooms.clearSection(tmp); s_mucRooms.save(); @@ -3112,13 +3244,20 @@ void JoinMucWizard::joinRoom() createRoomChat(*r); Engine::enqueue(m); // Save room - Client::self()->updateTableRow(s_mucSavedRooms,uri,0,false,w); - s_mucRooms.clearSection(uri); - NamedList* sect = s_mucRooms.createSection(uri); - if (sect) { - sect->addParam("nick",nick,false); - sect->addParam("password",r->m_password,false); - s_mucRooms.save(); + if (!(r->local() || r->remote())) { + r->m_params.setParam("nick",nick); + r->m_params.setParam("history",String::boolText(history)); + if (lastMinutes) + r->m_params.setParam("historylast",String(lastMinutes)); + else + r->m_params.clearParam("historylast"); + s_mucRooms.clearSection(uri); + NamedList* sect = s_mucRooms.createSection(uri); + if (sect) { + sect->addParam("nick",nick,false); + sect->addParam("password",r->m_password,false); + s_mucRooms.save(); + } } Client::self()->setVisible(toString(),false); } @@ -3621,6 +3760,39 @@ void ClientLogic::initStaticData() s_protocolsMutex.unlock(); } +// Save a contact into a configuration file. +bool ClientLogic::saveContact(Configuration& cfg, ClientContact* c, bool save) +{ + if (!c) + return false; + String sectName(c->uri()); + sectName.toLower(); + NamedList* sect = cfg.createSection(sectName); + MucRoom* room = c->mucRoom(); + if (room) { + sect->setParam("type","groupchat"); + sect->setParam("name",room->m_name); + sect->setParam("password",room->m_password); + } + else + sect->setParam("type","chat"); + sect->copyParams(c->m_params); + sect->clearParam("group"); + for (ObjList* o = c->groups().skipNull(); o; o = o->skipNext()) + sect->addParam("group",o->get()->toString(),false); + return !save || cfg.save(); +} + +// Delete a contact from a configuration file +bool ClientLogic::clearContact(Configuration& cfg, ClientContact* c, bool save) +{ + if (!c) + return false; + String sectName(c->uri()); + cfg.clearSection(sectName.toLower()); + return !save || cfg.save(); +} + // Called when the user selected a line bool ClientLogic::line(const String& name, Window* wnd) { @@ -4161,15 +4333,8 @@ bool DefaultLogic::toggle(Window* wnd, const String& name, bool active) else if (s_mucWizard->isWindow(wnd)) p.addParam("active:joinmuc_wizard",nText); else if (wnd->id() == ClientContact::s_mucsWnd) { - // Destroy all MUCS when hidden + // Hidden: destroy/close all MUCS, close log sessions if (!active) { - ObjList* o = m_accounts->accounts().skipNull(); - for (; o; o = o->skipNext()) { - ClientAccount* acc = static_cast(o->get()); - for (ObjList* l = acc->mucs().skipNull(); l; l = l->skipNext()) - logCloseMucSessions(static_cast(l->get())); - acc->mucs().clear(); - } // Remove from pending chat NamedList p(""); Client::self()->getOptions(ClientContact::s_dockedChatWidget,&p,wnd); @@ -4179,6 +4344,21 @@ bool DefaultLogic::toggle(Window* wnd, const String& name, bool active) if (ns && ns->name()) removePendingChat(ns->name()); } + ObjList* o = m_accounts->accounts().skipNull(); + for (; o; o = o->skipNext()) { + ClientAccount* acc = static_cast(o->get()); + ListIterator iter(acc->mucs()); + for (GenObject* gen = 0; 0 != (gen = iter.get());) { + MucRoom* room = static_cast(gen); + logCloseMucSessions(room); + if (room->local() || room->remote()) + clearRoom(room); + else + TelEngine::destruct(room); + } + if (acc->resource().online()) + updateChatRoomsContactList(true,acc); + } } } else if (wnd->id() == ClientContact::s_dockedChatWnd) { @@ -4345,7 +4525,7 @@ bool DefaultLogic::select(Window* wnd, const String& name, const String& item, } if (name == s_chatContactList) { - enableChatActions(item ? m_accounts->findContact(item) : 0); + enableChatActions(item ? m_accounts->findAnyContact(item) : 0); return true; } @@ -4685,9 +4865,13 @@ bool DefaultLogic::delAccount(const String& account, Window* wnd) if (w) Client::self()->closeWindow(w->toString()); clearAccountContacts(*acc); + updateChatRoomsContactList(false,acc); Client::self()->delTableRow(s_account,account); Client::self()->delTableRow(s_accountList,account); acc->save(false); + String error; + if (!acc->clearDataDir(&error) && error) + notifyGenericError(error,account); m_accounts->removeAccount(account); return true; } @@ -5319,9 +5503,11 @@ bool DefaultLogic::handleUserNotify(Message& msg, bool& stopLogic) if (tmp > stat) stat = tmp; regStat = acc->params().getValue("internal.status.text"); - // Update chat accounts - if (acc->hasChat()) + // Update chat accounts. Request MUCs + if (acc->hasChat()) { updateChatAccountList(account,true); + Engine::enqueue(acc->userData(false,"chatrooms")); + } } else { bool noFail = acc->params().getBoolValue("internal.nologinfail"); @@ -5366,6 +5552,8 @@ bool DefaultLogic::handleUserNotify(Message& msg, bool& stopLogic) if (acc->hasChat()) updateChatAccountList(account,false); } + // (Un)Load chat rooms + updateChatRoomsContactList(reg,acc); // Clear some internal params acc->m_params.clearParam("internal.nologinfail"); if (stat != ClientResource::Connecting) @@ -5474,21 +5662,15 @@ bool DefaultLogic::handleUserRoster(Message& msg, bool& stopLogic) const char* cName = msg.getValue(pref + "name",uri); bool newContact = (c == 0); bool changed = newContact; - if (c) { - changed = (c->m_name != cName); - if (changed) - c->m_name = cName; - } + if (c) + changed = setChangedString(c->m_name,cName) || changed; else { c = a->appendContact(id,cName,uri); if (!c) continue; } const String& sub = msg[pref + "subscription"]; - if (c->m_subscription != sub) { - c->m_subscription = sub; - changed = true; - } + changed = setChangedString(c->m_subscription,sub) || changed; // Get groups changed = c->setGroups(msg,pref + "group") || changed; // Update info window if displayed @@ -6111,6 +6293,8 @@ bool DefaultLogic::defaultMsgHandler(Message& msg, int id, bool& stopLogic) } if (id == Client::TransferNotify) return handleFileTransferNotify(msg,stopLogic); + if (id == Client::UserData) + return handleUserData(msg,stopLogic); return false; } @@ -6154,15 +6338,6 @@ bool DefaultLogic::initializedClient() // Load muc rooms s_mucRooms = Engine::configFile("client_mucrooms",true); s_mucRooms.load(false); - Window* w = s_mucWizard->window(); - if (w) { - unsigned int n = s_mucRooms.sections(); - for (unsigned int i = 0; i < n; i++) { - NamedList* sect = s_mucRooms.getSection(i); - if (sect) - Client::self()->updateTableRow(s_mucSavedRooms,*sect,0,false,w); - } - } Window* wMain = Client::self()->getWindow(s_wndMain); @@ -6216,6 +6391,7 @@ bool DefaultLogic::initializedClient() Client::self()->installRelay("chan.notify",Client::ChanNotify,100); Client::self()->installRelay("muc.room",Client::MucRoom,100); Client::self()->installRelay("transfer.notify",Client::TransferNotify,100); + Client::self()->installRelay("user.data",Client::UserData,100); // File transfer s_lastFileDir = Client::s_settings.getValue("filetransfer","dir"); @@ -6251,6 +6427,7 @@ bool DefaultLogic::initializedClient() NamedList pcm(s_chatContactList); NamedList* pChat = new NamedList("menu_" + s_chatContactList); pChat->addParam("item:" + s_chatNew,""); + pChat->addParam("item:" + s_chatRoomNew,""); pChat->addParam("item:",""); pChat->addParam("item:" + s_chatShowOffline,""); pChat->addParam("item:" + s_chatFlatList,""); @@ -6265,10 +6442,23 @@ bool DefaultLogic::initializedClient() pChatMenu->addParam("item:" + s_chatDel,""); pChatMenu->addParam("item:",""); pChatMenu->addParam("item:" + s_chatNew,""); + pChatMenu->addParam("item:" + s_chatRoomNew,""); pChatMenu->addParam("item:",""); pChatMenu->addParam("item:" + s_chatShowOffline,""); pChatMenu->addParam("item:" + s_chatFlatList,""); pcm.addParam(new NamedPointer("contactmenu",pChatMenu)); + NamedList* pChatRoomMenu = new NamedList("menu_" + s_chatContactList + "_chatroom"); + pChatRoomMenu->addParam("item:" + s_chat,""); + pChatRoomMenu->addParam("item:" + s_chatShowLog,""); + pChatRoomMenu->addParam("item:" + s_chatEdit,""); + pChatRoomMenu->addParam("item:" + s_chatDel,""); + pChatRoomMenu->addParam("item:",""); + pChatRoomMenu->addParam("item:" + s_chatNew,""); + pChatRoomMenu->addParam("item:" + s_chatRoomNew,""); + pChatRoomMenu->addParam("item:",""); + pChatRoomMenu->addParam("item:" + s_chatShowOffline,""); + pChatRoomMenu->addParam("item:" + s_chatFlatList,""); + pcm.addParam(new NamedPointer("chatroommenu",pChatRoomMenu)); Client::self()->setParams(&pcm); enableChatActions(0); // Set gobal account status menu @@ -6566,16 +6756,31 @@ bool DefaultLogic::deleteItem(const String& list, const String& item, Window* wn context << "deleteitem:" << list << ":" << item; // Handle known lists if (list == s_chatContactList) { - ClientContact* c = m_accounts->findContact(item); + ClientContact* c = m_accounts->findAnyContact(item); if (!c) return false; + MucRoom* r = c->mucRoom(); if (context) { - String text; - text << "Delete friend '" << c->m_name << "' from account '"; - text << c->accountName() << "'?"; + String text("Delete "); + text << (!r ? "friend " : "chat room "); + String name; + buildContactName(name,*c); + text << name << " from account '" << c->accountName() << "'?"; return showConfirm(wnd,text,context); } - Engine::enqueue(Client::buildUserRoster(false,c->accountName(),c->uri())); + if (!r) + Engine::enqueue(Client::buildUserRoster(false,c->accountName(),c->uri())); + else { + ClientAccount* acc = r->account(); + bool saveServerRooms = (acc != 0) && r->remote(); + if (acc) + ClientLogic::clearContact(acc->m_cfg,r); + updateChatRoomsContactList(false,0,r); + r->setLocal(false); + r->setRemote(false); + if (saveServerRooms) + Engine::enqueue(acc->userData(true,"chatrooms")); + } return true; } if (list == s_contactList) { @@ -6615,14 +6820,21 @@ bool DefaultLogic::deleteItem(const String& list, const String& item, Window* wn } } logCloseMucSessions(room); - TelEngine::destruct(room); - return true; + if (room->local() || room->remote()) { + clearRoom(room); + if (room->account() && room->account()->resource().online()) + updateChatRoomsContactList(true,0,room); + } + else + TelEngine::destruct(room); } else if (room) { MucRoomMember* m = room->findMemberById(item); if (m) logCloseSession(room,false,m->m_name); + Client::self()->delTableRow(list,item,wnd); } + return true; } if (wnd && wnd->id() == ClientContact::s_dockedChatWnd) { if (!s_changingDockedChat) @@ -6665,7 +6877,8 @@ bool DefaultLogic::handleTextChanged(NamedList* params, Window* wnd) if (!sender) return false; // Username changes in contact add/edit - if (wnd->id().startsWith("contactedit_")) { + if (wnd->id().startsWith("contactedit_") || + wnd->id().startsWith("chatroomedit_")) { if (!Client::valid()) return false; if (!wnd->context()) { @@ -6908,6 +7121,136 @@ bool DefaultLogic::handleFileTransferNotify(Message& msg, bool& stopLogic) return true; } +// Handle user.data messages. +bool DefaultLogic::handleUserData(Message& msg, bool& stopLogic) +{ + if (!Client::valid() || Client::isClientMsg(msg)) + return false; + if (Client::self()->postpone(msg,Client::UserData)) { + stopLogic = true; + return false; + } + const String& data = msg["data"]; + if (!data) + return false; + const String& account = msg["account"]; + ClientAccount* a = account ? m_accounts->findAccount(account) : 0; + if (!(a && a->resource().online())) + return false; + const String& oper = msg["operation"]; + if (!oper) + return false; + bool ok = (oper == "result"); + if (!ok && oper != "error") + return false; + const String& requested = msg["requested_operation"]; + bool upd = (requested == "update"); + if (ok) { + if (upd) { + // Update succeeded + return true; + } + // Handle request + if (data == "chatrooms") { + // Update MUC rooms + unsigned int n = msg.getIntValue("data.count"); + const NamedString* ns = 0; + NamedIterator iter(msg); + bool changed = false; + for (unsigned int i = 1; i <= n; i++) { + String prefix; + prefix << "data." << i; + const String& uri = msg[prefix]; + if (!uri) + continue; + prefix << "."; + String id; + ClientContact::buildContactId(id,a->toString(),uri); + MucRoom* r = a->findRoom(id); + String pwd = msg[prefix + "password"]; + if (pwd) { + Base64 b((void*)pwd.c_str(),pwd.length()); + DataBlock tmp; + b.decode(tmp); + pwd.assign((const char*)tmp.data(),tmp.length()); + } + const String& name = msg[prefix + "name"]; + if (r) { + changed = setChangedString(r->m_name,name) || changed; + changed = setChangedString(r->m_password,pwd) || changed; + } + else { + changed = true; + r = new MucRoom(a,id,name,uri); + r->m_password = pwd; + r->setLocal(false); + } + r->setRemote(true); + changed = r->setGroups(msg,prefix + "group") || changed; + // Copy other params + for (iter.reset(); 0 != (ns = iter.get());) { + if (!ns->name().startsWith(prefix)) + continue; + String param = ns->name().substr(prefix.length()); + if (param == "group") + continue; + changed = setChangedParam(r->m_params,param,*ns) || changed; + } + Debug(ClientDriver::self(),DebugAll, + "Account(%s) updated remote MUC room '%s' [%p]", + account.c_str(),r->uri().c_str(),a); + + } + if (changed) + updateChatRoomsContactList(true,a); + // Merge local/remote rooms + bool saveRemote = false; + for (ObjList* o = a->mucs().skipNull(); o; o = o->skipNext()) { + MucRoom* r = static_cast(o->get()); + if (r->local()) { + if (!r->remote()) { + r->setRemote(true); + saveRemote = true; + } + } + else if (r->remote()) { + r->setLocal(true); + saveContact(a->m_cfg,r); + } + } + if (saveRemote) + Engine::enqueue(a->userData(true,"chatrooms")); + } + } + else { + String error; + String reason = msg["error"]; + if (reason) { + error << reason; + const String& res = msg["reason"]; + if (res) + error << " (" << res << ")"; + } + else + error << msg["reason"]; + Debug(ClientDriver::self(),DebugNote, + "Account(%s) private data %s '%s' failed: %s", + account.c_str(),requested.c_str(),data.c_str(),error.c_str()); + } + return true; +} + +// Show a generic notification +void DefaultLogic::notifyGenericError(const String& text, const String& account, + const String& contact, const char* title) +{ + NamedList list(""); + NamedList* upd = buildNotifArea(list,"generic",account,contact,title); + setGenericNotif(*upd); + upd->addParam("text",text); + showNotificationArea(true,Client::self()->getWindow(s_wndMain),&list); +} + // Show/hide no audio notification (chan==0: initial check) void DefaultLogic::notifyNoAudio(bool show, bool micOk, bool speakerOk, ClientChannel* chan) @@ -6949,6 +7292,45 @@ void DefaultLogic::notifyNoAudio(bool show, bool micOk, bool speakerOk, Client::self()->setParams(&p,w); } +// Utility used in DefaultLogic::updateChatRoomsContactList +// Build (un)load a chat room parameter +static void addChatRoomParam(NamedList& upd, bool load, MucRoom* room) +{ + if (!(room && (room->local() || room->remote()))) + return; + NamedList* p = new NamedList(room->toString()); + if (load) + fillChatContact(*p,*room,true,true,true); + upd.addParam(new NamedPointer(*p,p,load ? String::boolText(true) : "")); +} + +// (Un)Load chat rooms +void DefaultLogic::updateChatRoomsContactList(bool load, ClientAccount* acc, + MucRoom* room) +{ + if (!(Client::valid() && (acc || room))) + return; + NamedList upd(""); + if (acc) { + for (ObjList* o = acc->mucs().skipNull(); o; o = o->skipNext()) + addChatRoomParam(upd,load,static_cast(o->get())); + } + else + addChatRoomParam(upd,load,room); + Client::self()->updateTableRows(s_chatContactList,&upd,false); +} + +// Utility used in updateAccount +static void updAccDelOld(ClientAccount*& old, ClientLogic* logic) +{ + if (!old) + return; + if (!old->resource().offline()) + Engine::enqueue(userLogin(old,false)); + logic->delAccount(old->toString(),0); + TelEngine::destruct(old); +} + // Add/set an account bool DefaultLogic::updateAccount(const NamedList& account, bool save, const String& replace, bool loaded) @@ -6970,16 +7352,18 @@ bool DefaultLogic::updateAccount(const NamedList& account, bool save, TelEngine::destruct(acc); acc = repl; } + String oldDataDir = acc ? acc->dataDir() : String::empty(); bool changed = false; // Update account + // Postpone old account deletion: let the new account take files + // from the old one's data directory + ClientAccount* old = 0; if (acc) { if (acc->toString() != account) { // Account id changed: // Disconnect the account, remove it and add a new one - if (!acc->resource().offline()) - Engine::enqueue(userLogin(acc,false)); - delAccount(acc->toString(),0); - TelEngine::destruct(acc); + old = acc; + acc = 0; } else { // Compare account parameters @@ -7002,6 +7386,7 @@ bool DefaultLogic::updateAccount(const NamedList& account, bool save, if (proto && user && host) id.assign(proto + ":" + user + "@" + host); else { + updAccDelOld(old,this); Debug(ClientDriver::self(),DebugNote, "Ignoring loaded account '%s' proto=%s user=%s host=%s", account.c_str(),proto.c_str(),user.c_str(),host); @@ -7027,6 +7412,7 @@ bool DefaultLogic::updateAccount(const NamedList& account, bool save, acc->m_params.setParam("savepassword", String::boolText(0 != acc->params().getParam("password"))); if (!m_accounts->appendAccount(acc)) { + updAccDelOld(old,this); Debug(ClientDriver::self(),DebugNote, "Failed to append duplicate account '%s'",acc->toString().c_str()); TelEngine::destruct(acc); @@ -7035,14 +7421,12 @@ bool DefaultLogic::updateAccount(const NamedList& account, bool save, changed = true; } if (!changed) { + updAccDelOld(old,this); TelEngine::destruct(acc); return true; } // Clear pending params acc->m_params.clearParam("internal.status",'.'); - // Save the account - if (save) - acc->save(true,acc->params().getBoolValue("savepassword")); // (Re)set account own contact setAccountContact(acc); // Update account list @@ -7068,7 +7452,25 @@ bool DefaultLogic::updateAccount(const NamedList& account, bool save, Engine::enqueue(userLogin(acc,false)); acc->m_params.setParam("internal.reconnect",String::boolText(true)); } + // Clear saved rooms + updateChatRoomsContactList(false,acc); + acc->clearRooms(true,false); + acc->m_cfg.assign(""); + acc->m_cfg.clearSection(); + // Update dir data + acc->m_params.setParam("datadirectory",oldDataDir); + String error; + if (acc->setupDataDir(&error)) { + acc->loadDataDirCfg(); + acc->loadContacts(); + } + else + notifyGenericError(error,acc->toString()); + // Save the account + if (save) + acc->save(true,acc->params().getBoolValue("savepassword")); TelEngine::destruct(acc); + updAccDelOld(old,this); return true; } @@ -7248,20 +7650,43 @@ bool DefaultLogic::handleChatContactAction(const String& name, Window* wnd) ClientContact* c = selectedChatContact(*m_accounts,wnd); if (!c) return false; - if (!c->hasChat()) { - c->createChatWindow(); - NamedList p(""); - fillChatContact(p,*c,true,true); - ClientResource* res = c->status(); - c->updateChatWindow(p,"Chat [" + c->m_name + "]", - resStatusImage(res ? res->m_status : ClientResource::Offline)); + MucRoom* r = c->mucRoom(); + if (!r) { + if (!c->hasChat()) { + c->createChatWindow(); + NamedList p(""); + fillChatContact(p,*c,true,true); + ClientResource* res = c->status(); + c->updateChatWindow(p,"Chat [" + c->m_name + "]", + resStatusImage(res ? res->m_status : ClientResource::Offline)); + } + c->showChat(true,true); + } + else { + if (r->resource().offline()) { + r->resource().m_name = r->m_params.getValue("nick"); + if (!r->resource().m_name && r->account()) { + if (r->account()->contact()) + r->resource().m_name = r->account()->contact()->uri().getUser(); + if (!r->resource().m_name) + r->resource().m_name = r->account()->params().getValue("username"); + } + bool hist = r->m_params.getBoolValue("history",true); + unsigned int lastMinutes = 0; + if (hist) + lastMinutes = r->m_params.getIntValue("historylast"); + Message* m = r->buildJoin(true,hist,lastMinutes * 60); + r->resource().m_status = ClientResource::Connecting; + updateChatRoomsContactList(true,0,r); + Engine::enqueue(m); + } + createRoomChat(*r); } - c->showChat(true,true); return true; } // Call chat contact if (name == s_chatCall) { - ClientContact* c = selectedChatContact(*m_accounts,wnd); + ClientContact* c = selectedChatContact(*m_accounts,wnd,false); if (!c) return false; ClientResource* res = c->findAudioResource(); @@ -7284,13 +7709,13 @@ bool DefaultLogic::handleChatContactAction(const String& name, Window* wnd) // Edit chat contact if (name == s_chatEdit) { ClientContact* c = selectedChatContact(*m_accounts,wnd); - return c && showContactEdit(*m_accounts,c); + return c && showContactEdit(*m_accounts,false,c); } - if (getPrefixedContact(name,s_chatEdit,id,m_accounts,&c,0) && c) { - bool ok = showContactEdit(*m_accounts,c); + if (getPrefixedContact(name,s_chatEdit,id,m_accounts,&c,&room) && c) { + bool ok = showContactEdit(*m_accounts,false,c); if (ok && wnd) { // Hide contact info window - Window* w = getContactInfoEditWnd(false,c); + Window* w = getContactInfoEditWnd(false,false,c); if (wnd == w) Client::self()->closeWindow(wnd->id()); } @@ -7298,20 +7723,22 @@ bool DefaultLogic::handleChatContactAction(const String& name, Window* wnd) } // Add chat contact if (name == s_chatNew) - return showContactEdit(*m_accounts); + return showContactEdit(*m_accounts,false); + if (name == s_chatRoomNew) + return showContactEdit(*m_accounts,true); // Remove chat contact if (name == s_chatDel) return deleteSelectedItem(s_chatContactList + ":",wnd); // Show chat contact info if (name == s_chatInfo) { - ClientContact* c = selectedChatContact(*m_accounts,wnd); + ClientContact* c = selectedChatContact(*m_accounts,wnd,false); return updateContactInfo(c,true,true); } // Subscription management bool sub = (name == s_chatSub); bool unsubd = !sub && (name == s_chatUnsubd); if (sub || unsubd || name == s_chatUnsub) { - ClientContact* c = selectedChatContact(*m_accounts,wnd); + ClientContact* c = selectedChatContact(*m_accounts,wnd,false); if (!c) return false; if (!unsubd) @@ -7321,15 +7748,19 @@ bool DefaultLogic::handleChatContactAction(const String& name, Window* wnd) return true; } // Save contact - if (name == "contactedit_ok") { + bool cedit = (name == "contactedit_ok"); + if (cedit || name == "chatroomedit_ok") { if (!(Client::valid() && wnd)) return false; String contact; - bool reqSub = false; ClientAccount* a = 0; if (wnd->context()) { // Edit - ClientContact* c = m_accounts->findContact(wnd->context()); + ClientContact* c = 0; + if (cedit) + c = m_accounts->findContact(wnd->context()); + else + c = m_accounts->findRoom(wnd->context()); if (c) { a = c->account(); contact = c->uri(); @@ -7360,7 +7791,19 @@ bool DefaultLogic::handleChatContactAction(const String& name, Window* wnd) return false; } contact << user << "@" << domain; - Client::self()->getCheck("request_subscribe",reqSub,wnd); + // Check unique + ClientContact* e = 0; + if (cedit) + e = a->findRoomByUri(contact); + else + e = a->findContactByUri(contact); + if (e) { + String error = "A "; + error << (cedit ? "chat room" : "contact"); + error << " with the same username and domain already exist"; + showError(wnd,error); + return false; + } } if (!a->resource().online()) { showError(wnd,"Selected account is not online"); @@ -7368,23 +7811,87 @@ bool DefaultLogic::handleChatContactAction(const String& name, Window* wnd) } String name; Client::self()->getText("name",name,false,wnd); - NamedList p(""); - Client::self()->getOptions("groups",&p,wnd); - Message* m = Client::buildUserRoster(true,a->toString(),contact); - m->addParam("name",name,false); - unsigned int n = p.length(); - for (unsigned int i = 0; i < n; i++) { - NamedString* ns = p.getParam(i); - if (!(ns && ns->name())) - continue; - NamedList pp(""); - Client::self()->getTableRow("groups",ns->name(),&pp,wnd); - if (pp.getBoolValue("check:group")) - m->addParam("group",ns->name(),false); + if (cedit) { + bool reqSub = false; + if (!wnd->context()) + Client::self()->getCheck("request_subscribe",reqSub,wnd); + NamedList p(""); + Client::self()->getOptions("groups",&p,wnd); + Message* m = Client::buildUserRoster(true,a->toString(),contact); + m->addParam("name",name,false); + unsigned int n = p.length(); + for (unsigned int i = 0; i < n; i++) { + NamedString* ns = p.getParam(i); + if (!(ns && ns->name())) + continue; + NamedList pp(""); + Client::self()->getTableRow("groups",ns->name(),&pp,wnd); + if (pp.getBoolValue("check:group")) + m->addParam("group",ns->name(),false); + } + Engine::enqueue(m); + if (reqSub) + Engine::enqueue(Client::buildSubscribe(true,true,a->toString(),contact)); + } + else { + String nick; + String pwd; + String grp; + Client::self()->getText("nick",nick,false,wnd); + Client::self()->getText("password",pwd,false,wnd); + Client::self()->getText("group",grp,false,wnd); + bool local = true; + bool remote = true; + Client::self()->getCheck("save_local",local,wnd); + Client::self()->getCheck("save_remote",remote,wnd); + bool reqHist = false; + String histLastValue; + Client::self()->getCheck("history",reqHist,wnd); + if (reqHist) { + bool reqHistLast = false; + Client::self()->getCheck("historylast",reqHistLast,wnd); + if (reqHistLast) + Client::self()->getText("historylast_value",histLastValue,false,wnd); + } + ClientContact::buildContactId(id,a->toString(),contact); + bool remoteChanged = remote; + bool localChanged = local; + MucRoom* room = a->findRoom(id); + if (!room) + room = new MucRoom(a,id,0,contact,0); + else { + remoteChanged = remote || room->remote(); + localChanged = local || room->local(); + } + room->m_name = name ? name : contact; + room->m_password = pwd; + room->groups().clear(); + room->appendGroup(grp); + room->m_params.setParam("nick",nick); + room->m_params.setParam("history",String::boolText(reqHist)); + room->m_params.setParam("historylast",histLastValue); + if (localChanged || remoteChanged) { + // Fake local to enable updating + room->setLocal(true); + updateChatRoomsContactList(local || remote,0,room); + } + room->setLocal(local); + room->setRemote(remote); + // Save it + if (local) { + String error; + if (!(a->setupDataDir(&error) && saveContact(a->m_cfg,room))) { + String text; + text << "Failed to save chat room " << contact; + text.append(error,"\r\n"); + notifyGenericError(text,a->toString(),contact); + } + } + else + ClientLogic::clearContact(a->m_cfg,room); + if (remoteChanged) + Engine::enqueue(a->userData(true,"chatrooms")); } - Engine::enqueue(m); - if (reqSub) - Engine::enqueue(Client::buildSubscribe(true,true,a->toString(),contact)); Client::self()->setVisible(wnd->id(),false); return true; } @@ -7483,6 +7990,9 @@ bool DefaultLogic::handleMucsAction(const String& name, Window* wnd, NamedList* } return true; } + // Save/edit chat room contact + if (getPrefixedContact(name,s_mucSave,id,m_accounts,0,&room)) + return room && showContactEdit(*m_accounts,true,room); return false; } @@ -7667,8 +8177,11 @@ bool DefaultLogic::handleMucResNotify(Message& msg, ClientAccount* acc, const St changed = true; } // Update - if (changed) + if (changed) { updateMucRoomMember(*room,*member,&msg); + if (acc->resource().online() && (room->local() || room->remote())) + updateChatRoomsContactList(true,0,room); + } return true; } diff --git a/share/skins/default/addchatroom.png b/share/skins/default/addchatroom.png new file mode 100644 index 00000000..3f6ee756 Binary files /dev/null and b/share/skins/default/addchatroom.png differ diff --git a/share/skins/default/chatroomedit.ui b/share/skins/default/chatroomedit.ui new file mode 100644 index 00000000..b94659b9 --- /dev/null +++ b/share/skins/default/chatroomedit.ui @@ -0,0 +1,676 @@ + + chatroomedit + + + + 0 + 0 + 400 + 268 + + + + + 0 + 0 + + + + + 400 + 200 + + + + + 400 + 268 + + + + muc.png + + + QWidget#chatroomedit { + background-color:#e0dfe3; +} + + + + sysmenu,title,minimize,close + + + true + + + + 4 + + + 6 + + + 4 + + + 6 + + + 4 + + + + + 0 + + + + + true + + + + 0 + 0 + + + + + 70 + 22 + + + + + 70 + 22 + + + + Account: + + + 0 + + + + + + + + 0 + 0 + + + + + 0 + 22 + + + + + 16777215 + 22 + + + + + + + + + 0 + 22 + + + + + 16777215 + 22 + + + + true + + + + + + + + + + + true + + + + 0 + 0 + + + + + 70 + 0 + + + + + 70 + 16777215 + + + + Room: + + + 0 + + + + + + + + 0 + 0 + + + + + 16 + 22 + + + + + 16777215 + 22 + + + + + 2 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + 100 + 22 + + + + + 100 + 22 + + + + true + + + + + + + + 0 + 0 + + + + @ + + + + + + + + 0 + 0 + + + + + 78 + 22 + + + + + 16777215 + 22 + + + + + + + + + + + + 0 + 22 + + + + + 16777215 + 22 + + + + true + + + + + + + + + 0 + + + + + true + + + + 0 + 0 + + + + + 70 + 22 + + + + + 70 + 22 + + + + QLabel:disabled {color: red;} + + + Name: + + + 0 + + + + + + + + 0 + 22 + + + + + 16777215 + 22 + + + + + + + + + + 0 + + + + + true + + + + 0 + 0 + + + + + 70 + 22 + + + + + 70 + 22 + + + + QLabel:disabled {color: red;} + + + Nick: + + + 0 + + + + + + + + 0 + 22 + + + + + 16777215 + 22 + + + + + + + + + + 0 + + + + + + 0 + 0 + + + + + 70 + 22 + + + + + 70 + 22 + + + + Password: + + + 0 + + + + + + + + 0 + 22 + + + + + 16777215 + 22 + + + + QLineEdit::Password + + + + + + + + + 0 + + + + + true + + + + 0 + 0 + + + + + 70 + 22 + + + + + 70 + 22 + + + + QLabel:disabled {color: red;} + + + Group: + + + 0 + + + + + + + + 0 + 22 + + + + + 16777215 + 22 + + + + + + + + + + + 0 + 0 + + + + + 0 + 22 + + + + + 16777215 + 22 + + + + Retrieve history + + + + + + + 18 + + + + + + 0 + 22 + + + + + 16777215 + 22 + + + + Retrieve last + + + + + + + + 0 + 22 + + + + + 16777215 + 22 + + + + minutes + + + 1 + + + 1440 + + + 5 + + + 30 + + + + + + + + + Qt::Vertical + + + + 20 + 0 + + + + + + + + + 16777215 + 2 + + + + Qt::Horizontal + + + + + + + 4 + + + + + Qt::Horizontal + + + + 40 + 0 + + + + + + + + Ok + + + ok.png + + + Return + + + + + + + Cancel + + + close.png + + + Esc + + + + + + + + + + diff --git a/share/skins/default/contactlist_chatroom.ui b/share/skins/default/contactlist_chatroom.ui new file mode 100644 index 00000000..622f7a52 --- /dev/null +++ b/share/skins/default/contactlist_chatroom.ui @@ -0,0 +1,146 @@ + + contactlist_chatroom + + + + 0 + 0 + 471 + 42 + + + + + 0 + 0 + + + + + 0 + 42 + + + + + 16777215 + 42 + + + + + 1 + + + 1 + + + 1 + + + 1 + + + 4 + + + + + + + + 0 + 0 + + + + + 20 + 20 + + + + + 20 + 20 + + + + muc_16.png + + + Qt::AlignCenter + + + + + + + + 0 + 0 + + + + + 0 + 20 + + + + + 16777215 + 20 + + + + QLabel { + font-size:12px; +} + + + + + + + + + 0 + + + 21 + + + + + + 0 + 0 + + + + + 0 + 12 + + + + + 16777215 + 12 + + + + QLabel { + font-size:10px; +} + + + + + + + + + + diff --git a/share/skins/default/muc_16.png b/share/skins/default/muc_16.png new file mode 100644 index 00000000..be0bb491 Binary files /dev/null and b/share/skins/default/muc_16.png differ diff --git a/share/skins/default/mucchat.ui b/share/skins/default/mucchat.ui index e58364bc..f6b01e4d 100644 --- a/share/skins/default/mucchat.ui +++ b/share/skins/default/mucchat.ui @@ -129,6 +129,34 @@ + + + + + 30 + 30 + + + + + 30 + 30 + + + + true + + + room_save + + + room_save + + + true + + + @@ -684,6 +712,17 @@ room_showlog + + + save.png + + + Save chat room contact + + + room_save + + message diff --git a/share/skins/default/qt4client.rc b/share/skins/default/qt4client.rc index bfd87743..b4cf78f9 100644 --- a/share/skins/default/qt4client.rc +++ b/share/skins/default/qt4client.rc @@ -106,6 +106,11 @@ enabled=no save=false description=contactedit.ui +[chatroomedit] +enabled=no +save=false +description=chatroomedit.ui + [contactinfo] enabled=no save=false diff --git a/share/skins/default/qt4client.ui b/share/skins/default/qt4client.ui index e56759d5..961bcca9 100644 --- a/share/skins/default/qt4client.ui +++ b/share/skins/default/qt4client.ui @@ -130,6 +130,7 @@ + @@ -203,14 +204,18 @@ property:_yate_nogroup_caption=Not set property:_yate_itemui=contact:contactlist_contact.ui property:_yate_itemui=group:contactlist_group.ui + property:_yate_itemui=chatroom:contactlist_chatroom.ui property:_yate_itemstyle=contact:QWidget#${name}{background:white;} property:_yate_itemstyle=group:QWidget#${name}{background:lightgrey;} + property:_yate_itemstyle=chatroom:QWidget#${name}{background:white;} property:_yate_itemselectedstyle=contact:QWidget#${name}{background:lightblue;} property:_yate_itemselectedstyle=group:QWidget#${name}{background:lightgrey;} + property:_yate_itemselectedstyle=chatroom:QWidget#${name}{background:lightblue;} property:_yate_itemstatewidget=group:state property:_yate_itemexpandedimage=group:expanded.png property:_yate_itemcollapsedimage=group:collapsed.png property:_yate_itemtooltip=contact:<html><head><meta name="qrichtext" content="1" /><style type="text/css">\np, li { white-space: pre-wrap; }\n</style></head><body style=" font-family:'Arial'; font-size:10pt; font-weight:400; font-style:normal;"><p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:14pt; font-weight:600;">${name}</span></p><p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">${status_text}</p><p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">${contact}</p><p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Account: ${account}</p></body></html> + property:_yate_itemtooltip=chatroom:<html><head><meta name="qrichtext" content="1" /><style type="text/css">\np, li { white-space: pre-wrap; }\n</style></head><body style=" font-family:'Arial'; font-size:10pt; font-weight:400; font-style:normal;"><p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:14pt; font-weight:600;">${name}</span></p><p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">${status_text}</p><p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">${contact}</p><p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Account: ${account}</p></body></html> groupcount=count @@ -1973,6 +1978,7 @@ QTextEdit { delete_item_action=deleteitem property:_yate_hidewndwhenempty=false property:_yate_hidewidgetwhenempty=frame_messages + property:_yate_itemui=generic:messages_generic.ui property:_yate_itemui=subscription:messages_okrejignore.ui property:_yate_itemui=mucinvite:messages_okrejignore.ui property:_yate_itemui=loginfail:messages_loginfail.ui @@ -2374,6 +2380,14 @@ QTextEdit { Show log + + + addchatroom.png + + + Add chat room + + diff --git a/share/skins/default/save.png b/share/skins/default/save.png new file mode 100644 index 00000000..d9d22b10 Binary files /dev/null and b/share/skins/default/save.png differ diff --git a/yatecbase.h b/yatecbase.h index d349a93b..bab3fb1d 100644 --- a/yatecbase.h +++ b/yatecbase.h @@ -791,6 +791,7 @@ public: MsgExecute, EngineStart, TransferNotify, + UserData, // Starting value for custom relays MsgIdCount }; @@ -2618,6 +2619,24 @@ public: */ static void initStaticData(); + /** + * Save a contact into a configuration file + * @param cfg The configuration file + * @param c The contact to save + * @param save True to save the file + * @return True on success + */ + static bool saveContact(Configuration& cfg, ClientContact* c, bool save = true); + + /** + * Delete a contact from a configuration file + * @param cfg The configuration file + * @param c The contact to delete + * @param save True to save the file + * @return True on success + */ + static bool clearContact(Configuration& cfg, ClientContact* c, bool save = true); + // Account options string list static ObjList s_accOptions; // Parameters that are applied from provider template @@ -3121,6 +3140,26 @@ protected: */ virtual bool handleFileTransferNotify(Message& msg, bool& stopLogic); + /** + * Handle user.data messages. + * @param msg The message + * @param stopLogic Stop notifying other logics if set to true on return + * @return True if handled + */ + virtual bool handleUserData(Message& msg, bool& stopLogic); + + /** + * Show a generic notification + * @param text Notification text + * @param account Optional concerned account + * @param contact Optional concerned contact + * @param title Notification title + */ + virtual void notifyGenericError(const String& text, + const String& account = String::empty(), + const String& contact = String::empty(), + const char* title = "Error"); + /** * Show/hide no audio notification * @param show Show or hide notification @@ -3131,6 +3170,15 @@ protected: virtual void notifyNoAudio(bool show, bool micOk = false, bool speakerOk = false, ClientChannel* chan = 0); + /** + * (Un)Load account's saved chat rooms or a specific room in contact list + * @param load True to load, false to unload + * @param acc The account owning the chat rooms + * @param room The room to update, ignored if acc is not 0 + */ + virtual void updateChatRoomsContactList(bool load, ClientAccount* acc, + MucRoom* room = 0); + String m_selectedChannel; // The currently selected channel String m_transferInitiated; // Tranfer initiated id @@ -3350,6 +3398,14 @@ public: */ virtual MucRoom* findRoomByUri(const String& uri, bool ref = false); + /** + * Find any contact (regular or MUC room) by its id + * @param id The id of the desired contact + * @param ref True to obtain a referenced pointer + * @return ClientContact pointer (may be account's own contact) or 0 if not found + */ + virtual ClientContact* findAnyContact(const String& id, bool ref = false); + /** * Build a contact and append it to the list * @param id The contact's id @@ -3375,6 +3431,13 @@ public: */ virtual ClientContact* removeContact(const String& id, bool delObj = true); + /** + * Clear MUC rooms. This method is thread safe + * @param saved True to clear saved rooms + * @param temp True to clear temporary rooms + */ + virtual void clearRooms(bool saved, bool temp); + /** * Build a login/logout message from account's data * @param login True to login, false to logout @@ -3383,13 +3446,65 @@ public: */ virtual Message* userlogin(bool login, const char* msg = "user.login"); + /** + * Build a message used to update or query account userdata. + * Add account MUC rooms if data is 'chatrooms' and update + * @param update True to update, false to query + * @param data Data to update or query + * @param msg Optional message name. Default to 'user.data' + * @return A valid Message pointer + */ + virtual Message* userData(bool update, const String& data, + const char* msg = "user.data"); + /** * Fill a list used to update a account's list item * @param list Parameter list to fill */ virtual void fillItemParams(NamedList& list); + /** + * Retrieve account data directory + * @return Account data directory + */ + inline const String& dataDir() const + { return m_params["datadirectory"]; } + + /** + * Set account directory in application data directory. Make sure it exists. + * Move all files from the old one if changed + * @param errStr Optional string to be filled with error string + * @param saveAcc Save data directory parameter in client accounts + * @return True on success + */ + virtual bool setupDataDir(String* errStr = 0, bool saveAcc = true); + + /** + * Load configuration file from data directory + * @param cfg Optional configuration file to load. + * Load account's conf file if 0 + * @param file File name. Defaults to 'account.conf' + * @return True on success + */ + virtual bool loadDataDirCfg(Configuration* cfg = 0, + const char* file = "account.conf"); + + /** + * Load contacts from configuration file + * @param cfg Optional configuration file to load. + * Load from account's conf file if 0 + */ + virtual void loadContacts(Configuration* cfg = 0); + + /** + * Clear account data directory + * @param errStr Optional string to be filled with error string + * @return True if all files were succesfully removed + */ + virtual bool clearDataDir(String* errStr = 0); + NamedList m_params; // Account parameters + Configuration m_cfg; // Account conf file protected: // Remove from owner. Release data @@ -3518,6 +3633,14 @@ public: */ virtual MucRoom* findRoomByMember(const String& id, bool ref = false); + /** + * Find any contact (regular or MUC room) by its id + * @param id The id of the desired contact + * @param ref True to obtain a referenced pointer + * @return ClientContact pointer (may be account's own contact) or 0 if not found + */ + virtual ClientContact* findAnyContact(const String& id, bool ref = false); + /** * Check if there is a single registered account and return it * @param skipProto Optional account protocol to skip @@ -3635,6 +3758,35 @@ public: inline ObjList& groups() { return m_groups; } + /** + * Check if the contact is locally saved + * @param defVal Default value to return if parameter is invalid + * @return True if the contact is locally saved + */ + inline bool local(bool defVal = false) const + { return m_params.getBoolValue("local",defVal); } + + /** + * Set contact locally saved flag + * @param on The new value for locally saved flag + */ + inline void setLocal(bool on) + { m_params.setParam("local",String::boolText(on)); } + + /** + * Check if the contact is saved on server + * @param defVal Default value to return if parameter is invalid + * @return True if the contact is saved on server + */ + inline bool remote(bool defVal = false) const + { return m_params.getBoolValue("remote",defVal); } + + /** + * Set contact server saved flag + * @param on The new value for server saved flag + */ + inline void setRemote(bool on) + { m_params.setParam("remote",String::boolText(on)); } /** * Set/reset the docked chat flag for non MucRoom contact @@ -3957,6 +4109,7 @@ public: String m_name; // Contact's display name String m_subscription; // Presence subscription state + NamedList m_params; // Optional contact extra params protected: /**