diff --git a/doc/groups_draft.txt b/doc/groups_draft.txt new file mode 100644 index 00000000..37562010 --- /dev/null +++ b/doc/groups_draft.txt @@ -0,0 +1,74 @@ +twister group chat draft +======================== + +group chat is a private and encrypted timeline shared among some users +which define a specific topic (or description). + +it is tempting to use a new torrent where members would subscribe in +order to post their chat messages. however this idea does work as it is +impossible (very difficult?) to implement a distributed torrent with +multiple writers. (a clash occurs when two writers try to send the +same piece number) + +therefore, twister's group chat is a special case of direct messages +using a private key pair (outside of the blockchain). non-members do not +know about group chat creation as public key is never published. + +members are invited to group using a special (bencoded) group_invite DM: + +group_invite { + desc: "free text description", + key: "secret of the private key", +} + +after sending the group_invite DM to a given user, another DM is sent +to the group itself to notify all existing members about the new +member list. this is another special DM: + +group_members { + [xxx,yyy,...] +} + +it is true that, since every member knows both private/public keys of +the group, symmetric-key could have been used for group chat's messages. +however we reuse the same twister infrastructure of original DMs. +any group chats, invites and messages are indistinguible from normal +DMs to non-members. + +because all members have access to private key, there is no concept of +administrator. any member may invite new members and change group's +description. it is not possible to exclude a member, so frontends may +offer an option to clone/create a new group excluding whoever they +want. + +--- + +new RPCs for group chat: + +- creategroup + returns groupalias (which is added to wallet). + + group aliases are prepended with "*" so they are not valid usernames + and may not be propagated accidentaly to the network. + + group aliases are only valid locally, each twisterd instance will + create its own alias upon receiving an invitation. + +- listgroups + [ groupalias1, groupalias2, ... ] + +- getgroupinfo + {alias:xxx,description:xxx,members:[xxx,yyy,...]} + +- newgroupinvite [,,...] + DM(group_invite) => newmember + DM(group_members) => groupalias + +- newgroupdescription + DM(group_invite) => groupalias + +- leavegroup + not yet implemented + +note: use getdirectmsgs/newdirectmsgto to obtain/post new messages + to group chats. diff --git a/src/bitcoinrpc.cpp b/src/bitcoinrpc.cpp index f87bfa28..6d2de73b 100644 --- a/src/bitcoinrpc.cpp +++ b/src/bitcoinrpc.cpp @@ -271,6 +271,12 @@ static const CRPCCommand vRPCCommands[] = { "getspamposts", &getspamposts, false, true, false }, { "torrentstatus", &torrentstatus, false, true, false }, { "search", &search, false, true, false }, + { "creategroup", &creategroup, false, true, false }, + { "listgroups", &listgroups, false, true, false }, + { "getgroupinfo", &getgroupinfo, false, true, false }, + { "newgroupinvite", &newgroupinvite, false, true, false }, + { "newgroupdescription", &newgroupdescription, false, true, false }, + { "leavegroup", &leavegroup, false, true, false }, }; CRPCTable::CRPCTable() @@ -1327,6 +1333,9 @@ Array RPCConvertValues(const std::string &strMethod, const std::vector 1) ConvertTo(params[1]); if (strMethod == "getspamposts" && n > 2) ConvertTo(params[2]); if (strMethod == "search" && n > 2) ConvertTo(params[2]); + if (strMethod == "newgroupinvite" && n > 1) ConvertTo(params[1]); + if (strMethod == "newgroupinvite" && n > 3) ConvertTo(params[3]); + if (strMethod == "newgroupdescription" && n > 1) ConvertTo(params[1]); return params; } diff --git a/src/bitcoinrpc.h b/src/bitcoinrpc.h index b66287b2..31af2c01 100644 --- a/src/bitcoinrpc.h +++ b/src/bitcoinrpc.h @@ -224,5 +224,11 @@ extern json_spirit::Value gettrendinghashtags(const json_spirit::Array& params, extern json_spirit::Value getspamposts(const json_spirit::Array& params, bool fHelp); extern json_spirit::Value torrentstatus(const json_spirit::Array& params, bool fHelp); extern json_spirit::Value search(const json_spirit::Array& params, bool fHelp); +extern json_spirit::Value creategroup(const json_spirit::Array& params, bool fHelp); +extern json_spirit::Value listgroups(const json_spirit::Array& params, bool fHelp); +extern json_spirit::Value getgroupinfo(const json_spirit::Array& params, bool fHelp); +extern json_spirit::Value newgroupinvite(const json_spirit::Array& params, bool fHelp); +extern json_spirit::Value newgroupdescription(const json_spirit::Array& params, bool fHelp); +extern json_spirit::Value leavegroup(const json_spirit::Array& params, bool fHelp); #endif diff --git a/src/twister.cpp b/src/twister.cpp index de7650b4..3df1b5de 100644 --- a/src/twister.cpp +++ b/src/twister.cpp @@ -66,6 +66,7 @@ static std::string m_receivedSpamMsgStr; static std::string m_receivedSpamUserStr; static int64 m_lastSpamTime = 0; static std::map m_users; +static std::map m_groups; static CCriticalSection cs_seenHashtags; static std::map m_seenHashtags; @@ -100,6 +101,7 @@ private: #define USER_DATA_FILE "user_data" #define GLOBAL_DATA_FILE "global_data" +#define GROUP_DATA_FILE "group_data" void dhtgetMapAdd(sha1_hash &ih, alert_manager *am) { @@ -406,6 +408,9 @@ void ThreadWaitExtIP() loadUserData(userDataPath.string(), m_users); printf("loaded user_data for %zd users\n", m_users.size()); + boost::filesystem::path groupDataPath = GetDataDir() / GROUP_DATA_FILE; + loadGroupData(groupDataPath.string(), m_groups); + // add all user torrents to a std::set (all m_following) std::map::const_iterator i; for (i = m_users.begin(); i != m_users.end(); ++i) { @@ -414,6 +419,15 @@ void ThreadWaitExtIP() torrentsToStart.insert(username); } } + + // add torrents from groups + std::map::const_iterator j; + for (j = m_groups.begin(); j != m_groups.end(); ++j) { + GroupChat const &data = j->second; + BOOST_FOREACH(string username, data.m_members) { + torrentsToStart.insert(username); + } + } } // now restart the user torrents @@ -473,6 +487,10 @@ void lockAndSaveUserData() boost::filesystem::path userDataPath = GetDataDir() / USER_DATA_FILE; saveUserData(userDataPath.string(), m_users); } + if( m_groups.size() ) { + boost::filesystem::path groupDataPath = GetDataDir() / GROUP_DATA_FILE; + saveGroupData(groupDataPath.string(), m_groups); + } } int getDhtNodes(boost::int64_t *dht_global_nodes) @@ -1079,11 +1097,149 @@ bool verifySignature(std::string const &strMessage, std::string const &strUserna return (pubkeyRec.GetID() == pubkey.GetID()); } +void storeNewDM(const string &localuser, const string &dmUser, const StoredDirectMsg &stoDM) +{ + LOCK(cs_twister); + // store this dm in memory list, but prevent duplicates + std::vector &dmsFromToUser = m_users[localuser]. + m_directmsg[dmUser]; + std::vector::iterator it; + for( it = dmsFromToUser.begin(); it != dmsFromToUser.end(); ++it ) { + if( stoDM.m_utcTime == (*it).m_utcTime && + stoDM.m_text == (*it).m_text ) { + break; + } + if( stoDM.m_utcTime < (*it).m_utcTime ) { + dmsFromToUser.insert(it, stoDM); + break; + } + } + if( it == dmsFromToUser.end() ) { + dmsFromToUser.push_back(stoDM); + } +} + +void storeGroupDM(const string &groupAlias, const StoredDirectMsg &stoDM) +{ + LOCK(cs_twister); + if( !m_groups.count(groupAlias) ) + return; + GroupChat &group = m_groups[groupAlias]; + + BOOST_FOREACH(string const &member, group.m_members) { + if( m_users.count(member) ) { + storeNewDM(member,groupAlias,stoDM); + } + } +} + +string getGroupAliasByKey(const string &privKey) +{ + string groupAlias; + LOCK(cs_twister); + map::iterator i; + for (i = m_groups.begin(); i != m_groups.end(); ++i) { + if( i->second.m_privKey == privKey ) { + groupAlias = i->first; + break; + } + } + return groupAlias; +} + +void registerNewGroup(const string &privKey, const string &desc, const string &member, const string &invitedBy, int64_t utcTime) +{ + string groupAlias = getGroupAliasByKey(privKey); + if( !groupAlias.length() ) { + CBitcoinSecret vchSecret; + bool fGood = vchSecret.SetString(privKey); + if (!fGood) { + printf("registerGroupMember: Invalid private key\n"); + return; + } + CKey key = vchSecret.GetKey(); + CPubKey pubkey = key.GetPubKey(); + CKeyID vchAddress = pubkey.GetID(); + { + LOCK(pwalletMain->cs_wallet); + if (pwalletMain->HaveKey(vchAddress)) { + // already exists? reuse same alias (trying to fix inconsistency wallet x groups) + groupAlias = pwalletMain->mapKeyMetadata[vchAddress].username; + if( !groupAlias.length() || groupAlias.at(0) != '*' ) { + printf("registerGroupMember: Invalid group alias '%s' from wallet\n", groupAlias.c_str()); + return; + } + } else { + groupAlias = getRandomGroupAlias(); + } + + pwalletMain->mapKeyMetadata[vchAddress] = CKeyMetadata(GetTime(), groupAlias); + if (!pwalletMain->AddKeyPubKey(key, pubkey)) { + printf("registerGroupMember: Error adding key to wallet\n"); + return; + } + } + } + + LOCK(cs_twister); + GroupChat &group = m_groups[groupAlias]; + group.m_description = desc; + group.m_privKey = privKey; + + if( member.length() ) { + if( member == groupAlias ) { + StoredDirectMsg stoDM; + stoDM.m_fromMe = false; + stoDM.m_from = invitedBy; + // temporary hack: we must add new fields to StoredDirectMsg so text may be translated by UI + stoDM.m_text = "*** '" + invitedBy + "' changed group description to: " + desc; + stoDM.m_utcTime = utcTime; + storeGroupDM(groupAlias,stoDM); + } else { + group.m_members.insert(member); + + if( m_users.count(member) ) { + StoredDirectMsg stoDM; + stoDM.m_fromMe = false; + stoDM.m_from = invitedBy; + // temporary hack: we must add new fields to StoredDirectMsg so text may be translated by UI + stoDM.m_text = "*** Invited by '" + invitedBy + "' to group: " + desc; + stoDM.m_utcTime = utcTime; + storeNewDM(member,groupAlias,stoDM); + } + } + } +} + +void notifyNewGroupMember(string &groupAlias, string &newmember, string &invitedBy, int64_t utcTime) +{ + LOCK(cs_twister); + if( !m_groups.count(groupAlias) ) + return; + + GroupChat &group = m_groups[groupAlias]; + + if( group.m_members.count(newmember) ) + return; + + group.m_members.insert(newmember); + + StoredDirectMsg stoDM; + stoDM.m_fromMe = false; + stoDM.m_from = invitedBy; + // temporary hack: we must add new fields to StoredDirectMsg so text may be translated by UI + stoDM.m_text = "*** New member '" + newmember + "' invited by '" + invitedBy + "'"; + stoDM.m_utcTime = utcTime; + storeGroupDM(groupAlias,stoDM); +} + // try decrypting new DM received by any torrent we follow bool processReceivedDM(lazy_entry const* post) { bool result = false; + std::set torrentsToStart; + lazy_entry const* dm = post->dict_find_dict("dm"); if( dm ) { ecies_secure_t sec; @@ -1097,7 +1253,7 @@ bool processReceivedDM(lazy_entry const* post) { CKey key; if (!pwalletMain->GetKey(item.first, key)) { - printf("acceptSignedPost: private key not available trying to decrypt DM.\n"); + printf("processReceivedDM: private key not available trying to decrypt DM.\n"); } else { std::string textOut; if( key.Decrypt(sec, textOut) ) { @@ -1108,10 +1264,25 @@ bool processReceivedDM(lazy_entry const* post) textOut.c_str()); */ - std::string from = post->dict_find_string_value("n"); - std::string to = item.second.username; // default (old format) - std::string msg = textOut; // default (old format) - bool fromMe = (from == to); + int64_t utcTime = post->dict_find_int_value("time"); + std::string from = post->dict_find_string_value("n"); + std::string to = item.second.username; // default (old format) + std::string msg = textOut; // default (old format) + bool fromMe = (from == to); + bool isGroup = false; + + { + LOCK(cs_twister); + isGroup = m_groups.count(to); + if( !isGroup && m_users.count(to) && + !m_users.at(to).m_following.count(from) ) { + /* DM not allowed from users we don't follow.*/ + printf("processReceivedDM: from '%s' to '%s' not allowed (not following)\n", + from.c_str(), to.c_str()); + break; + } + } + // try bdecoding the new format (copy to self etc) { lazy_entry v; @@ -1119,6 +1290,34 @@ bool processReceivedDM(lazy_entry const* post) libtorrent::error_code ec; if (lazy_bdecode(textOut.data(), textOut.data()+textOut.size(), v, ec, &pos) == 0 && v.type() == lazy_entry::dict_t) { + + /* group_invite: register new private key and group's description. + * if sent to groupalias then it is a change description request. */ + lazy_entry const* pGroupInvite = v.dict_find_dict("group_invite"); + if (pGroupInvite) { + lazy_entry const* pDesc = pGroupInvite->dict_find_string("desc"); + lazy_entry const* pKey = pGroupInvite->dict_find_string("key"); + if (pDesc && pKey) { + string desc = pDesc->string_value(); + string privKey = pKey->string_value(); + registerNewGroup(privKey, desc, to, from, utcTime); + } + break; + } + + /* update group members list. we may need to start torrent for + * new members to receive their chat updates */ + lazy_entry const* pGroupMembers = v.dict_find_list("group_members"); + if (pGroupMembers && isGroup) { + for (int i = 0; i < pGroupMembers->list_size(); ++i) { + std::string member = pGroupMembers->list_string_value_at(i); + if (member.empty()) continue; + notifyNewGroupMember(to, member, from, utcTime); + torrentsToStart.insert(member); + } + break; + } + lazy_entry const* pMsg = v.dict_find_string("msg"); lazy_entry const* pTo = v.dict_find_string("to"); if (pMsg && pTo) { @@ -1134,33 +1333,26 @@ bool processReceivedDM(lazy_entry const* post) StoredDirectMsg stoDM; stoDM.m_fromMe = fromMe; + stoDM.m_from = from; stoDM.m_text = msg; - stoDM.m_utcTime = post->dict_find_int_value("time"); - - LOCK(cs_twister); - // store this dm in memory list, but prevent duplicates - std::vector &dmsFromToUser = m_users[item.second.username]. - m_directmsg[fromMe ? to : from]; - std::vector::iterator it; - for( it = dmsFromToUser.begin(); it != dmsFromToUser.end(); ++it ) { - if( stoDM.m_utcTime == (*it).m_utcTime && - stoDM.m_text == (*it).m_text ) { - break; - } - if( stoDM.m_utcTime < (*it).m_utcTime ) { - dmsFromToUser.insert(it, stoDM); - break; - } - } - if( it == dmsFromToUser.end() ) { - dmsFromToUser.push_back(stoDM); - } + stoDM.m_utcTime = utcTime; + if( isGroup ) { + storeGroupDM(item.second.username, stoDM); + } else { + storeNewDM(item.second.username, fromMe ? to : from, stoDM); + } break; } } } } + + // start torrents outside cs_wallet to prevent deadlocks + BOOST_FOREACH(string username, torrentsToStart) { + startTorrentUser(username, true); + } + return result; } @@ -1388,7 +1580,13 @@ bool createSignedUserpost(entry &v, std::string const &username, int k, bool createDirectMessage(entry &dm, std::string const &to, std::string const &msg) { CPubKey pubkey; - if( !getUserPubKey(to, pubkey) ) { + + /* try obtaining key from wallet first */ + CKeyID keyID; + if (pwalletMain->GetKeyIdFromUsername(to, keyID) && + pwalletMain->GetPubKey( keyID, pubkey) ) { + /* success: key obtained from wallet */ + } else if( !getUserPubKey(to, pubkey) ) { printf("createDirectMessage: no pubkey for user '%s'\n", to.c_str()); return false; } @@ -2248,6 +2446,7 @@ Value getdirectmsgs(const Array& params, bool fHelp) dmObj.push_back(Pair("time",dmsFromToUser.at(i).m_utcTime)); dmObj.push_back(Pair("text",dmsFromToUser.at(i).m_text)); dmObj.push_back(Pair("fromMe",dmsFromToUser.at(i).m_fromMe)); + dmObj.push_back(Pair("from",dmsFromToUser.at(i).m_from)); userMsgs.push_back(dmObj); } if( userMsgs.size() ) { @@ -3175,3 +3374,230 @@ Object getLibtorrentSessionStatus() // @TODO: Is there a way to get some statistics for dhtProxy? return obj; } + +Value creategroup(const Array& params, bool fHelp) +{ + if (fHelp || params.size() != 1) + throw runtime_error( + "creategroup \n" + "Create a new key pair for group chat and add it to wallet\n" + "Hint: use groupcreate to invite yourself\n" + "Returns the group alias"); + + string strDescription = params[0].get_str(); + + RandAddSeedPerfmon(); + CKey secret; + secret.MakeNewKey(true); + string privKey = CBitcoinSecret(secret).ToString(); + + string noMember; + registerNewGroup(privKey, strDescription, noMember, noMember, GetTime()); + + return getGroupAliasByKey(privKey); +} + +Value listgroups(const Array& params, bool fHelp) +{ + if (fHelp || params.size() != 0) + throw runtime_error( + "listgroups\n" + "get list of group chats"); + + Array ret; + + LOCK(cs_twister); + map::const_iterator i; + for (i = m_groups.begin(); i != m_groups.end(); ++i) { + ret.push_back(i->first); + } + + return ret; +} + +Value getgroupinfo(const Array& params, bool fHelp) +{ + if (fHelp || params.size() != 1) + throw runtime_error( + "getgroupinfo \n" + "get group description and members"); + + string strGroupAlias = params[0].get_str(); + + Object ret; + + LOCK(cs_twister); + if (!m_groups.count(strGroupAlias)) + throw JSONRPCError(RPC_INTERNAL_ERROR, "unknown group alias"); + + ret.push_back(Pair("alias",strGroupAlias)); + ret.push_back(Pair("description",m_groups.at(strGroupAlias).m_description)); + + Array membersList; + BOOST_FOREACH( std::string const &n, m_groups.at(strGroupAlias).m_members) { + membersList.push_back(n); + } + ret.push_back(Pair("members",membersList)); + + return ret; +} + +static void signAndAddDM(const std::string &strFrom, int k, const entry *dm) +{ + entry v; + if( !createSignedUserpost(v, strFrom, k, "", + NULL, NULL, dm, + std::string(""), 0) ) + throw JSONRPCError(RPC_INTERNAL_ERROR,"error signing post with private key of user"); + + std::vector buf; + bencode(std::back_inserter(buf), v); + + std::string errmsg; + if( !acceptSignedPost(buf.data(),buf.size(),strFrom,k,errmsg,NULL) ) + throw JSONRPCError(RPC_INVALID_PARAMS,errmsg); + + torrent_handle h = startTorrentUser(strFrom, true); + h.add_piece(k++,buf.data(),buf.size()); +} + +Value newgroupinvite(const Array& params, bool fHelp) +{ + if (fHelp || params.size() != 4 ) + throw runtime_error( + "newgroupinvite '[,...]'\n" + "Invite some new members for a group chat.\n" + "note: k is increased by at least 2, check getlasthave"); + + EnsureWalletIsUnlocked(); + + string strFrom = params[0].get_str(); + int k = params[1].get_int(); + string strGroupAlias = params[2].get_str(); + Array newmembers = params[3].get_array(); + + std::set membersList; + { + LOCK(cs_twister); + if (!m_groups.count(strGroupAlias)) + throw JSONRPCError(RPC_INTERNAL_ERROR, "unknown group alias"); + membersList = m_groups.at(strGroupAlias).m_members; + } + + /* create group_invite DM and send it to each new member */ + for( unsigned int u = 0; u < newmembers.size(); u++ ) { + string strMember = newmembers[u].get_str(); + membersList.insert(strMember); + entry groupInvite; + { + LOCK(cs_twister); + groupInvite["desc"] = m_groups.at(strGroupAlias).m_description; + groupInvite["key"] = m_groups.at(strGroupAlias).m_privKey; + } + entry payloadMsg; + payloadMsg["group_invite"] = groupInvite; + std::vector payloadbuf; + bencode(std::back_inserter(payloadbuf), payloadMsg); + std::string strMsgData = std::string(payloadbuf.data(),payloadbuf.size()); + + entry dmInvite; + if( !createDirectMessage(dmInvite, strMember, strMsgData) ) + throw JSONRPCError(RPC_INTERNAL_ERROR, + "error encrypting to pubkey of destination user"); + signAndAddDM(strFrom, k++, &dmInvite); + } + + /* create group_members DM and send it to group */ + { + /* FIXME: if we have too many members, this code will break! + * we must check byteCounter and split in multiple DMs as needed to not exceed max size. + */ + entry groupMembers; + int byteCounter = 0; + BOOST_FOREACH( string const &member, membersList) { + groupMembers.list().push_back(member); + byteCounter += member.length() + 3; + } + entry payloadMsg; + payloadMsg["group_members"] = groupMembers; + std::vector payloadbuf; + bencode(std::back_inserter(payloadbuf), payloadMsg); + std::string strMsgData = std::string(payloadbuf.data(),payloadbuf.size()); + + entry dmMembers; + if( !createDirectMessage(dmMembers, strGroupAlias, strMsgData) ) + throw JSONRPCError(RPC_INTERNAL_ERROR, + "error encrypting to pubkey of group alias"); + signAndAddDM(strFrom, k++, &dmMembers); + } + + return Value(); +} + +Value newgroupdescription(const Array& params, bool fHelp) +{ + if (fHelp || params.size() != 4 ) + throw runtime_error( + "newgroupdescription \n" + "Change group description by sending a new invite DM to group"); + + EnsureWalletIsUnlocked(); + + string strFrom = params[0].get_str(); + int k = params[1].get_int(); + string strGroupAlias = params[2].get_str(); + string strDescription = params[3].get_str(); + + entry groupInvite; + { + LOCK(cs_twister); + if (!m_groups.count(strGroupAlias)) + throw JSONRPCError(RPC_INTERNAL_ERROR, "unknown group alias"); + + m_groups[strGroupAlias].m_description = strDescription; + groupInvite["desc"] = m_groups.at(strGroupAlias).m_description; + groupInvite["key"] = m_groups.at(strGroupAlias).m_privKey; + } + entry payloadMsg; + payloadMsg["group_invite"] = groupInvite; + std::vector payloadbuf; + bencode(std::back_inserter(payloadbuf), payloadMsg); + std::string strMsgData = std::string(payloadbuf.data(),payloadbuf.size()); + + entry dmInvite; + if( !createDirectMessage(dmInvite, strGroupAlias, strMsgData) ) + throw JSONRPCError(RPC_INTERNAL_ERROR, + "error encrypting to pubkey of group alias"); + signAndAddDM(strFrom, k++, &dmInvite); + + return Value(); +} + +Value leavegroup(const Array& params, bool fHelp) +{ + if (fHelp || params.size() != 4 ) + throw runtime_error( + "leavegroup \n" + "Stop receive chats from group"); + + string strUser = params[0].get_str(); + string strGroupAlias = params[1].get_str(); + + LOCK(cs_twister); + if (!m_groups.count(strGroupAlias)) + throw JSONRPCError(RPC_INTERNAL_ERROR, "unknown group alias"); + + if (!m_users.count(strUser)) + throw JSONRPCError(RPC_INTERNAL_ERROR, "unknown user"); + + throw JSONRPCError(RPC_INTERNAL_ERROR, "not implemented"); + /* + * The idea is to remove strGroupAlias from m_users[strUser].m_directmsg and + * also add strGroupAlias to a m_users[strUser].m_ignoreGroups list to prevent + * being invited again. however how would one revert a leavegroup? + */ + + return Value(); +} + + diff --git a/src/twister_utils.cpp b/src/twister_utils.cpp index 96d913d4..90e8efec 100644 --- a/src/twister_utils.cpp +++ b/src/twister_utils.cpp @@ -169,6 +169,7 @@ int saveUserData(std::string const& filename, std::map con dmElem["time"] = stoDm.m_utcTime; dmElem["text"] = stoDm.m_text; dmElem["fromMe"] = stoDm.m_fromMe; + dmElem["from"] = stoDm.m_from; dmList.list().push_back(dmElem); } } @@ -246,6 +247,7 @@ int loadUserData(std::string const& filename, std::map &us stoDm.m_text = dmElem->dict_find_string_value("text"); stoDm.m_utcTime = dmElem->dict_find_int_value("time"); stoDm.m_fromMe = dmElem->dict_find_int_value("fromMe"); + stoDm.m_from = dmElem->dict_find_int_value("from"); udata.m_directmsg[dmDict->dict_at(j).first].push_back(stoDm); } } @@ -262,6 +264,74 @@ data_error: return -2; } +int saveGroupData(std::string const& filename, std::map const &groups) +{ + entry groupsDict; + + std::map::const_iterator i; + for (i = groups.begin(); i != groups.end(); ++i) { + GroupChat const &gchat = i->second; + entry &groupData = groupsDict[i->first]; + groupData["description"] = gchat.m_description; + groupData["privkey"] = gchat.m_privKey; + + if( gchat.m_members.size() ) { + entry &membersList = groupData["members"]; + BOOST_FOREACH( std::string const &n, gchat.m_members) { + membersList.list().push_back(n); + } + } + } + + std::vector buf; + if( groupsDict.type() == entry::dictionary_t ) { + bencode(std::back_inserter(buf), groupsDict); + return save_file(filename, buf); + } else { + return 0; + } +} + + +int loadGroupData(std::string const& filename, std::map &groups) +{ + std::vector in; + if (load_file(filename, in) == 0) { + lazy_entry groupsDict; + libtorrent::error_code ec; + if (lazy_bdecode(&in[0], &in[0] + in.size(), groupsDict, ec) == 0) { + if( groupsDict.type() != lazy_entry::dict_t ) goto data_error; + + for( int i = 0; i < groupsDict.dict_size(); i++) { + GroupChat gchat; + + const lazy_entry *groupData = groupsDict.dict_at(i).second; + if( groupData->type() != lazy_entry::dict_t ) goto data_error; + + gchat.m_description = groupData->dict_find_string_value("description"); + gchat.m_privKey = groupData->dict_find_string_value("privkey"); + + const lazy_entry *membersList = groupData->dict_find("members"); + if( membersList ) { + if( membersList->type() != lazy_entry::list_t ) goto data_error; + + for( int j = 0; j < membersList->list_size(); j++ ) { + gchat.m_members.insert( membersList->list_string_value_at(j) ); + } + } + groups[groupsDict.dict_at(i).first] = gchat; + } + return 0; + } + } + return -1; + +data_error: + printf("loadGroupData: unexpected bencode type - user_data corrupt!\n"); + return -2; +} + + void findAndHexcape(libtorrent::entry &e, string const& key) { if( e.type() == libtorrent::entry::dictionary_t && @@ -402,4 +472,12 @@ sha1_hash dhtTargetHash(std::string const &username, std::string const &resource return hasher(buf.data(), buf.size()).final(); } - +std::string getRandomGroupAlias() +{ + std::string groupAlias("*xxxxxxxx"); + + for(int i = 1; i < groupAlias.length(); i++) { + groupAlias[i] = 'a' + 26 * (rand() / (RAND_MAX + 1.0)); + } + return groupAlias; +} diff --git a/src/twister_utils.h b/src/twister_utils.h index ce0faaab..3c8688f6 100644 --- a/src/twister_utils.h +++ b/src/twister_utils.h @@ -14,6 +14,13 @@ struct StoredDirectMsg { int64_t m_utcTime; std::string m_text; bool m_fromMe; + std::string m_from; // used for group chat +}; + +struct GroupChat { + std::string m_description; + std::string m_privKey; + std::set m_members; }; // in-memory data per wallet user @@ -44,6 +51,9 @@ libtorrent::entry jsonToEntry(const json_spirit::Value &v); int saveUserData(std::string const& filename, std::map const &users); int loadUserData(std::string const& filename, std::map &users); +int saveGroupData(std::string const& filename, std::map const &groups); +int loadGroupData(std::string const& filename, std::map &groups); + void hexcapePost(libtorrent::entry &e); void unHexcapePost(libtorrent::entry &e); @@ -56,4 +66,6 @@ libtorrent::entry safeGetEntryDict(libtorrent::entry const &e, std::string const libtorrent::sha1_hash dhtTargetHash(std::string const &username, std::string const &resource, std::string const &type); +std::string getRandomGroupAlias(); + #endif // TWISTER_UTILS_H