/* Copyright (c) 2015-16 Miranda NG project (http://miranda-ng.org) This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation version 2 of the License. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "stdafx.h" void CSkypeProto::InitGroupChatModule() { GCREGISTER gcr = { sizeof(gcr) }; gcr.iMaxText = 0; gcr.ptszDispName = m_tszUserName; gcr.pszModule = m_szModuleName; CallServiceSync(MS_GC_REGISTER, 0, (LPARAM)&gcr); HookProtoEvent(ME_GC_EVENT, &CSkypeProto::OnGroupChatEventHook); HookProtoEvent(ME_GC_BUILDMENU, &CSkypeProto::OnGroupChatMenuHook); CreateProtoService(PS_JOINCHAT, &CSkypeProto::OnJoinChatRoom); CreateProtoService(PS_LEAVECHAT, &CSkypeProto::OnLeaveChatRoom); } void CSkypeProto::CloseAllChatChatSessions() { GC_INFO gci = { 0 }; gci.Flags = GCF_BYINDEX | GCF_ID | GCF_DATA; gci.pszModule = m_szModuleName; int count = pci->SM_GetCount(m_szModuleName); for (int i = 0; i < count; i++) { gci.iItem = i; if (!CallServiceSync(MS_GC_GETINFO, 0, (LPARAM)&gci)) { GCDEST gcd = { m_szModuleName, gci.pszID, GC_EVENT_CONTROL }; GCEVENT gce = { sizeof(gce), &gcd }; CallServiceSync(MS_GC_EVENT, SESSION_OFFLINE, (LPARAM)&gce); CallServiceSync(MS_GC_EVENT, SESSION_TERMINATE, (LPARAM)&gce); } } } MCONTACT CSkypeProto::FindChatRoom(const char *chatname) { SESSION_INFO *si = pci->SM_FindSession(_A2T(chatname), m_szModuleName); return si ? si->hContact : 0; } void CSkypeProto::StartChatRoom(const TCHAR *tid, const TCHAR *tname) { // Create the group chat session GCSESSION gcw = { sizeof(gcw) }; gcw.iType = GCW_CHATROOM; gcw.ptszID = tid; gcw.pszModule = m_szModuleName; gcw.ptszName = tname; CallServiceSync(MS_GC_NEWSESSION, 0, (LPARAM)&gcw); // Send setting events GCDEST gcd = { m_szModuleName, tid, GC_EVENT_ADDGROUP }; GCEVENT gce = { sizeof(gce), &gcd }; // Create a user statuses gce.ptszStatus = TranslateT("Admin"); CallServiceSync(MS_GC_EVENT, NULL, reinterpret_cast(&gce)); gce.ptszStatus = TranslateT("User"); CallServiceSync(MS_GC_EVENT, NULL, reinterpret_cast(&gce)); // Finish initialization gcd.iType = GC_EVENT_CONTROL; gce.time = time(NULL); gce.pDest = &gcd; bool hideChats = getBool("HideChats", 1); CallServiceSync(MS_GC_EVENT, (hideChats ? WINDOW_HIDDEN : SESSION_INITDONE), reinterpret_cast(&gce)); CallServiceSync(MS_GC_EVENT, SESSION_ONLINE, reinterpret_cast(&gce)); } void CSkypeProto::OnLoadChats(const NETLIBHTTPREQUEST *response) { if (response == NULL) return; JSONNode root = JSONNode::parse(response->pData); if (!root) return; const JSONNode &metadata = root["_metadata"]; const JSONNode &conversations = root["conversations"].as_array(); int totalCount = metadata["totalCount"].as_int(); std::string syncState = metadata["syncState"].as_string(); if (totalCount >= 99 || conversations.size() >= 99) PushRequest(new SyncHistoryFirstRequest(syncState.c_str(), li), &CSkypeProto::OnSyncHistory); for (size_t i = 0; i < conversations.size(); i++) { const JSONNode &conversation = conversations.at(i); const JSONNode &threadProperties = conversation["threadProperties"]; const JSONNode &id = conversation["id"]; if (!conversation["lastMessage"]) continue; CMString topic(threadProperties["topic"].as_mstring()); SendRequest(new GetChatInfoRequest(id.as_string().c_str(), li), &CSkypeProto::OnGetChatInfo, topic.Detach()); } } /* Hooks */ int CSkypeProto::OnGroupChatEventHook(WPARAM, LPARAM lParam) { GCHOOK *gch = (GCHOOK*)lParam; if (!gch) { return 1; } else if (mir_strcmp(gch->pDest->pszModule, m_szModuleName) != 0) { return 0; } _T2A chat_id(gch->pDest->ptszID); switch (gch->pDest->iType) { case GC_USER_MESSAGE: { OnSendChatMessage(gch->pDest->ptszID, gch->ptszText); break; } case GC_USER_PRIVMESS: { MCONTACT hContact = FindContact(_T2A(gch->ptszUID)); if (hContact == NULL) { hContact = AddContact(_T2A(gch->ptszUID), true); setWord(hContact, "Status", ID_STATUS_ONLINE); db_set_b(hContact, "CList", "Hidden", 1); setTString(hContact, "Nick", gch->ptszUID); db_set_dw(hContact, "Ignore", "Mask1", 0); } CallService(MS_MSG_SENDMESSAGET, hContact, 0); break; } case GC_USER_LOGMENU: { switch (gch->dwData) { case 10: { CSkypeInviteDlg dlg(this); { mir_cslock lck(m_InviteDialogsLock); m_InviteDialogs.insert(&dlg); } if(!dlg.DoModal()) break; MCONTACT hContact = dlg.m_hContact; if (hContact != NULL) { SendRequest(new InviteUserToChatRequest(chat_id, Contacts[hContact], "User", li)); } { mir_cslock lck(m_InviteDialogsLock); m_InviteDialogs.remove(&dlg); } break; } case 20: OnLeaveChatRoom(FindChatRoom(chat_id), NULL); break; case 30: CMString newTopic = ChangeTopicForm(); if (!newTopic.IsEmpty()) SendRequest(new SetChatPropertiesRequest(chat_id, "topic", T2Utf(newTopic.GetBuffer()), li)); break; } break; } case GC_USER_NICKLISTMENU: { _T2A user_id(gch->ptszUID); switch (gch->dwData) { case 10: SendRequest(new KickUserRequest(chat_id, user_id, li)); break; case 30: SendRequest(new InviteUserToChatRequest(chat_id, user_id, "Admin", li)); break; case 40: SendRequest(new InviteUserToChatRequest(chat_id, user_id, "User", li)); break; case 50: ENTER_STRING pForm = { sizeof(pForm) }; pForm.type = ESF_COMBO; pForm.recentCount = 0; pForm.caption = TranslateT("Enter new nickname"); pForm.ptszInitVal = gch->ptszUID; pForm.szModuleName = m_szModuleName; pForm.szDataPrefix = "renamenick_"; GCDEST gcd = { m_szModuleName, gch->ptszUID, GC_EVENT_NICK }; GCEVENT gce = { sizeof(GCEVENT), &gcd }; EnterString(&pForm); gce.ptszNick = gch->ptszUID; gce.bIsMe = IsMe(user_id); gce.ptszUID = gch->ptszUID; gce.ptszText = pForm.ptszResult; gce.dwFlags = GCEF_ADDTOLOG; gce.time = time(NULL); CallServiceSync(MS_GC_EVENT, 0, (LPARAM)&gce); db_set_ts(FindChatRoom(chat_id), "UsersNicks", _T2A(gch->ptszUID), pForm.ptszResult); break; } break; } } return 0; } INT_PTR CSkypeProto::OnJoinChatRoom(WPARAM hContact, LPARAM) { if (hContact) { ptrT idT(getTStringA(hContact, "ChatRoomID")); ptrT nameT(getTStringA(hContact, "Nick")); StartChatRoom(idT, nameT != NULL ? nameT : idT); } return 0; } INT_PTR CSkypeProto::OnLeaveChatRoom(WPARAM hContact, LPARAM) { if (!IsOnline()) return 1; if (hContact && IDYES == MessageBox(NULL, TranslateT("This chat is going to be destroyed forever with all its contents. This action cannot be undone. Are you sure?"), TranslateT("Warning"), MB_YESNO | MB_ICONQUESTION)) { ptrT idT(getTStringA(hContact, "ChatRoomID")); GCDEST gcd = { m_szModuleName, NULL, GC_EVENT_CONTROL }; gcd.ptszID = idT; GCEVENT gce = { sizeof(gce), &gcd }; gce.time = ::time(NULL); CallServiceSync(MS_GC_EVENT, SESSION_OFFLINE, reinterpret_cast(&gce)); CallServiceSync(MS_GC_EVENT, SESSION_TERMINATE, reinterpret_cast(&gce)); SendRequest(new KickUserRequest(_T2A(idT), li.szSkypename, li)); CallService(MS_DB_CONTACT_DELETE, (WPARAM)hContact, 0); } return 0; } /* CHAT EVENT */ void CSkypeProto::OnChatEvent(const JSONNode &node) { //CMStringA szMessageId = node["clientmessageid"] ? node["clientmessageid"].as_string().c_str() : node["skypeeditedid"].as_string().c_str(); CMStringA szConversationName(UrlToSkypename(node["conversationLink"].as_string().c_str())); CMStringA szFromSkypename(UrlToSkypename(node["from"].as_string().c_str())); CMString szTopic(node["threadtopic"].as_mstring()); time_t timestamp = IsoToUnixTime(node["composetime"].as_string().c_str()); std::string strContent = node["content"].as_string(); int nEmoteOffset = node["skypeemoteoffset"].as_int(); if (FindChatRoom(szConversationName) == NULL) SendRequest(new GetChatInfoRequest(szConversationName, li), &CSkypeProto::OnGetChatInfo, szTopic.Detach()); std::string messageType = node["messagetype"].as_string(); if (messageType == "Text" || messageType == "RichText") { ptrA szClearedContent(messageType == "RichText" ? RemoveHtml(strContent.c_str()) : mir_strdup(strContent.c_str())); AddMessageToChat(_A2T(szConversationName), _A2T(szFromSkypename), szClearedContent, nEmoteOffset != NULL, nEmoteOffset, timestamp); } else if (messageType == "ThreadActivity/AddMember") { ptrA xinitiator, xtarget, initiator; //content = 14291862291648:initiator8:user HXML xml = xmlParseString(ptrT(mir_utf8decodeT(strContent.c_str())), 0, _T("addmember")); if (xml == NULL) return; for (int i = 0; i < xmlGetChildCount(xml); i++) { HXML xmlNode = xmlGetNthChild(xml, L"target", i); if (xmlNode == NULL) break; xtarget = mir_t2a(xmlGetText(xmlNode)); CMStringA target = ParseUrl(xtarget, "8:"); AddChatContact(_A2T(szConversationName), target, target, L"User"); } xmlDestroyNode(xml); } else if (messageType == "ThreadActivity/DeleteMember") { ptrA xinitiator, xtarget; //content = 14291862291648:initiator8:user HXML xml = xmlParseString(ptrT(mir_utf8decodeT(strContent.c_str())), 0, _T("deletemember")); if (xml != NULL) { HXML xmlNode = xmlGetChildByPath(xml, _T("initiator"), 0); xinitiator = node != NULL ? mir_t2a(xmlGetText(xmlNode)) : NULL; xmlNode = xmlGetChildByPath(xml, _T("target"), 0); xtarget = xmlNode != NULL ? mir_t2a(xmlGetText(xmlNode)) : NULL; xmlDestroyNode(xml); } if (xtarget == NULL) return; CMStringA target = ParseUrl(xtarget, "8:"); CMStringA initiator = ParseUrl(xinitiator, "8:"); RemoveChatContact(_A2T(szConversationName), target, target, true, initiator); } else if (messageType == "ThreadActivity/TopicUpdate") { //content=14295327021308:usertest topic ptrA xinitiator, value; HXML xml = xmlParseString(ptrT(mir_utf8decodeT(strContent.c_str())), 0, _T("topicupdate")); if (xml != NULL) { HXML xmlNode = xmlGetChildByPath(xml, _T("initiator"), 0); xinitiator = xmlNode != NULL ? mir_t2a(xmlGetText(xmlNode)) : NULL; xmlNode = xmlGetChildByPath(xml, _T("value"), 0); value = xmlNode != NULL ? mir_t2a(xmlGetText(xmlNode)) : NULL; xmlDestroyNode(xml); } CMStringA initiator = ParseUrl(xinitiator, "8:"); RenameChat(szConversationName, value); ChangeChatTopic(szConversationName, value, initiator); } else if (messageType == "ThreadActivity/RoleUpdate") { //content=14295512583638:user8:user1admin ptrA xinitiator, xId, xRole; HXML xml = xmlParseString(ptrT(mir_utf8decodeT(strContent.c_str())), 0, _T("roleupdate")); if (xml != NULL) { HXML xmlNode = xmlGetChildByPath(xml, _T("initiator"), 0); xinitiator = xmlNode != NULL ? mir_t2a(xmlGetText(xmlNode)) : NULL; xmlNode = xmlGetChildByPath(xml, _T("target"), 0); if (xmlNode != NULL) { HXML xmlId = xmlGetChildByPath(xmlNode, _T("id"), 0); HXML xmlRole = xmlGetChildByPath(xmlNode, _T("role"), 0); xId = xmlId != NULL ? mir_t2a(xmlGetText(xmlId)) : NULL; xRole = xmlRole != NULL ? mir_t2a(xmlGetText(xmlRole)) : NULL; } xmlDestroyNode(xml); CMStringA initiator = ParseUrl(xinitiator, "8:"); CMStringA id = ParseUrl(xId, "8:"); GCDEST gcd = { m_szModuleName, _A2T(szConversationName), !mir_strcmpi(xRole, "Admin") ? GC_EVENT_ADDSTATUS : GC_EVENT_REMOVESTATUS }; GCEVENT gce = { sizeof(gce), &gcd }; ptrT tszId(mir_a2t(id)); ptrT tszRole(mir_a2t(xRole)); ptrT tszInitiator(mir_a2t(initiator)); gce.pDest = &gcd; gce.dwFlags = GCEF_ADDTOLOG; gce.ptszNick = tszId; gce.ptszUID = tszId; gce.ptszText = tszInitiator; gce.time = time(NULL); gce.bIsMe = IsMe(id); gce.ptszStatus = TranslateT("Admin"); CallServiceSync(MS_GC_EVENT, 0, (LPARAM)&gce); } } } TCHAR* UnEscapeChatTags(TCHAR* str_in) { TCHAR *s = str_in, *d = str_in; while (*s) { if (*s == '%' && s[1] == '%') s++; *d++ = *s++; } *d = 0; return str_in; } void CSkypeProto::OnSendChatMessage(const TCHAR *chat_id, const TCHAR * tszMessage) { if (!IsOnline()) return; TCHAR *buf = NEWTSTR_ALLOCA(tszMessage); rtrimt(buf); UnEscapeChatTags(buf); ptrA szChatId(mir_t2a(chat_id)); ptrA szMessage(mir_utf8encodeT(buf)); if (strncmp(szMessage, "/me ", 4) == 0) SendRequest(new SendChatActionRequest(szChatId, time(NULL), szMessage, li)); else SendRequest(new SendChatMessageRequest(szChatId, time(NULL), szMessage, li)); } void CSkypeProto::AddMessageToChat(const TCHAR *chat_id, const TCHAR *from, const char *content, bool isAction, int emoteOffset, time_t timestamp, bool isLoading) { GCDEST gcd = { m_szModuleName, chat_id, isAction ? GC_EVENT_ACTION : GC_EVENT_MESSAGE }; GCEVENT gce = { sizeof(GCEVENT), &gcd }; gce.bIsMe = IsMe(_T2A(from)); gce.ptszNick = from; gce.time = timestamp; gce.ptszUID = from; CMString tszText(ptrT(mir_utf8decodeT(content))); tszText.Replace(L"%", L"%%"); if (!isAction) { gce.ptszText = tszText; gce.dwFlags = GCEF_ADDTOLOG; } else { gce.ptszText = &(tszText.GetBuffer())[emoteOffset]; } if (isLoading) gce.dwFlags = GCEF_NOTNOTIFY; CallServiceSync(MS_GC_EVENT, 0, (LPARAM)&gce); } void CSkypeProto::OnGetChatInfo(const NETLIBHTTPREQUEST *response, void *p) { ptrT topic((TCHAR*)p); // memory must be freed in any case if (response == NULL || response->pData == NULL) return; JSONNode root = JSONNode::parse(response->pData); if (!root) return; const JSONNode &members = root["members"]; const JSONNode &properties = root["properties"]; if (!properties["capabilities"] || properties["capabilities"].empty()) return; CMStringA chatId(UrlToSkypename(root["messages"].as_string().c_str())); StartChatRoom(_A2T(chatId), topic); for (size_t i = 0; i < members.size(); i++) { const JSONNode &member = members.at(i); CMStringA username(UrlToSkypename(member["userLink"].as_string().c_str())); std::string role = member["role"].as_string(); if (!IsChatContact(_A2T(chatId), username)) AddChatContact(_A2T(chatId), username, username, _A2T(role.c_str()), true); } PushRequest(new GetHistoryRequest(chatId, 15, true, 0, li), &CSkypeProto::OnGetServerHistory); } void CSkypeProto::RenameChat(const char *chat_id, const char *name) { ptrT tchat_id(mir_a2t(chat_id)); ptrT tname(mir_utf8decodeT(name)); GCDEST gcd = { m_szModuleName, tchat_id, GC_EVENT_CHANGESESSIONAME }; GCEVENT gce = { sizeof(gce), &gcd }; gce.ptszText = tname; CallService(MS_GC_EVENT, 0, reinterpret_cast(&gce)); } void CSkypeProto::ChangeChatTopic(const char *chat_id, const char *topic, const char *initiator) { ptrT tchat_id(mir_a2t(chat_id)); ptrT tname(mir_a2t(initiator)); ptrT ttopic(mir_utf8decodeT(topic)); GCDEST gcd = { m_szModuleName, tchat_id, GC_EVENT_TOPIC }; GCEVENT gce = { sizeof(gce), &gcd }; gce.ptszUID = tname; gce.ptszNick = tname; gce.ptszText = ttopic; CallService(MS_GC_EVENT, 0, reinterpret_cast(&gce)); } bool CSkypeProto::IsChatContact(const TCHAR *chat_id, const char *id) { ptrA users(GetChatUsers(chat_id)); return (users != NULL && strstr(users, id) != NULL); } char *CSkypeProto::GetChatUsers(const TCHAR *chat_id) { GC_INFO gci = { 0 }; gci.Flags = GCF_USERS; gci.pszModule = m_szModuleName; gci.pszID = chat_id; CallService(MS_GC_GETINFO, 0, (LPARAM)&gci); return gci.pszUsers; } void CSkypeProto::AddChatContact(const TCHAR *tchat_id, const char *id, const char *name, const TCHAR *role, bool isChange) { if (IsChatContact(tchat_id, id)) return; ptrT tnick(mir_a2t_cp(name, CP_UTF8)); if (TCHAR *tmp = db_get_tsa(FindChatRoom(_T2A(tchat_id)), "UsersNicks", id)) tnick = tmp; ptrT tid(mir_a2t(id)); GCDEST gcd = { m_szModuleName, tchat_id, GC_EVENT_JOIN }; GCEVENT gce = { sizeof(gce), &gcd }; gce.pDest = &gcd; gce.dwFlags = GCEF_ADDTOLOG; gce.ptszNick = tnick; gce.ptszUID = tid; gce.time = !isChange ? time(NULL) : NULL; gce.bIsMe = IsMe(id); gce.ptszStatus = TranslateTS(role); CallServiceSync(MS_GC_EVENT, 0, reinterpret_cast(&gce)); } void CSkypeProto::RemoveChatContact(const TCHAR *tchat_id, const char *id, const char *name, bool isKick, const char *initiator) { if (IsMe(id)) return; ptrT tnick(mir_a2t_cp(name, CP_UTF8)); ptrT tid(mir_a2t(id)); ptrT tinitiator(mir_a2t(initiator)); GCDEST gcd = { m_szModuleName, tchat_id, isKick ? GC_EVENT_KICK : GC_EVENT_PART }; GCEVENT gce = { sizeof(gce), &gcd }; if (isKick) { gce.ptszUID = tid; gce.ptszNick = tnick; gce.ptszStatus = tinitiator; gce.time = time(NULL); } else { gce.dwFlags = GCEF_ADDTOLOG; gce.ptszNick = tnick; gce.ptszUID = tid; gce.time = time(NULL); gce.bIsMe = IsMe(id); } CallServiceSync(MS_GC_EVENT, 0, reinterpret_cast(&gce)); } INT_PTR CSkypeProto::SvcCreateChat(WPARAM, LPARAM) { if (IsOnline()) { CSkypeGCCreateDlg dlg(this); { mir_cslock lck(m_GCCreateDialogsLock); m_GCCreateDialogs.insert(&dlg); } if (!dlg.DoModal()) { return 1; } SendRequest(new CreateChatroomRequest(dlg.m_ContactsList, li)); { mir_cslock lck(m_GCCreateDialogsLock); m_GCCreateDialogs.remove(&dlg); } return 0; } return 1; } /* Menus */ int CSkypeProto::OnGroupChatMenuHook(WPARAM, LPARAM lParam) { GCMENUITEMS *gcmi = (GCMENUITEMS*)lParam; if (mir_strcmpi(gcmi->pszModule, m_szModuleName)) return 0; if (gcmi->Type == MENU_ON_LOG) { static const struct gc_item Items[] = { { LPGENT("&Invite user..."), 10, MENU_ITEM, FALSE }, { LPGENT("&Leave chat session"), 20, MENU_ITEM, FALSE }, { LPGENT("&Change topic"), 30, MENU_ITEM, FALSE } }; gcmi->nItems = _countof(Items); gcmi->Item = (gc_item*)Items; } else if (gcmi->Type == MENU_ON_NICKLIST) { /* if (IsMe(_T2A(gcmi->pszUID))) { gcmi->nItems = 0; gcmi->Item = NULL; } else {*/ static const struct gc_item Items[] = { { LPGENT("Kick &user"), 10, MENU_ITEM }, { NULL, 0, MENU_SEPARATOR }, { LPGENT("Set &role"), 20, MENU_NEWPOPUP }, { LPGENT("&Admin"), 30, MENU_POPUPITEM }, { LPGENT("&User"), 40, MENU_POPUPITEM }, { LPGENT("Change nick"), 50, MENU_ITEM }, }; gcmi->nItems = _countof(Items); gcmi->Item = (gc_item*)Items; // } } return 0; } CMString CSkypeProto::ChangeTopicForm() { CMString caption(FORMAT, _T("[%s] %s"), _A2T(m_szModuleName), TranslateT("Enter new chatroom topic")); ENTER_STRING pForm = { sizeof(pForm) }; pForm.type = ESF_MULTILINE; pForm.caption = caption; pForm.ptszInitVal = NULL; pForm.szModuleName = m_szModuleName; return (!EnterString(&pForm)) ? CMString() : CMString(ptrT(pForm.ptszResult)); }