/* Copyright (c) 2015-24 Miranda NG team (https://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 = {}; gcr.dwFlags = GC_DATABASE | GC_PERSISTENT; gcr.iMaxText = 0; gcr.ptszDispName = m_tszUserName; gcr.pszModule = m_szModuleName; Chat_Register(&gcr); HookProtoEvent(ME_GC_EVENT, &CSkypeProto::OnGroupChatEventHook); HookProtoEvent(ME_GC_BUILDMENU, &CSkypeProto::OnGroupChatMenuHook); CreateProtoService(PS_JOINCHAT, &CSkypeProto::OnJoinChatRoom); CreateProtoService(PS_LEAVECHAT, &CSkypeProto::OnLeaveChatRoom); } SESSION_INFO* CSkypeProto::StartChatRoom(const wchar_t *tid, const wchar_t *tname, const char *pszVersion) { // Create the group chat session SESSION_INFO *si = Chat_NewSession(GCW_CHATROOM, m_szModuleName, tid, tname); if (!si) return nullptr; bool bFetchInfo = si->arUsers.getCount() == 0; if (pszVersion) { CMStringA oldVersion(getMStringA(si->hContact, "Version")); if (oldVersion != pszVersion) bFetchInfo = true; } if (bFetchInfo) { // Create user statuses Chat_AddGroup(si, TranslateT("Admin")); Chat_AddGroup(si, TranslateT("User")); PushRequest(new GetChatInfoRequest(tid)); } // Finish initialization Chat_Control(si, (getBool("HideChats", 1) ? WINDOW_HIDDEN : SESSION_INITDONE)); Chat_Control(si, SESSION_ONLINE); return si; } ///////////////////////////////////////////////////////////////////////////////////////// // Group chat invitation dialog class CSkypeInviteDlg : public CSkypeDlgBase { CCtrlCombo m_combo; public: MCONTACT m_hContact = 0; CSkypeInviteDlg(CSkypeProto *proto) : CSkypeDlgBase(proto, IDD_GC_INVITE), m_combo(this, IDC_CONTACT) {} bool OnInitDialog() override { for (auto &hContact : m_proto->AccContacts()) if (!m_proto->isChatRoom(hContact)) m_combo.AddString(Clist_GetContactDisplayName(hContact), hContact); return true; } bool OnApply() override { m_hContact = m_combo.GetCurData(); return true; } }; int CSkypeProto::OnGroupChatEventHook(WPARAM, LPARAM lParam) { GCHOOK *gch = (GCHOOK*)lParam; if (!gch) return 0; auto *si = gch->si; if (mir_strcmp(si->pszModule, m_szModuleName) != 0) return 0; T2Utf chat_id(si->ptszID), user_id(gch->ptszUID); switch (gch->iType) { case GC_USER_MESSAGE: SendChatMessage(si, gch->ptszText); break; case GC_USER_PRIVMESS: { MCONTACT hContact = FindContact(user_id); if (hContact == NULL) { hContact = AddContact(user_id, T2Utf(gch->ptszNick), true); setWord(hContact, "Status", ID_STATUS_ONLINE); Contact::Hide(hContact); } CallService(MS_MSG_SENDMESSAGEW, hContact, 0); } break; case GC_USER_LOGMENU: switch (gch->dwData) { case 10: { CSkypeInviteDlg dlg(this); if (dlg.DoModal()) if (dlg.m_hContact != NULL) PushRequest(new InviteUserToChatRequest(chat_id, getId(dlg.m_hContact), "User")); } break; case 20: OnLeaveChatRoom(si->hContact, NULL); break; case 30: CMStringW newTopic = ChangeTopicForm(); if (!newTopic.IsEmpty()) PushRequest(new SetChatPropertiesRequest(chat_id, "topic", T2Utf(newTopic.GetBuffer()))); break; } break; case GC_USER_NICKLISTMENU: switch (gch->dwData) { case 10: KickChatUser(chat_id, user_id); break; case 30: PushRequest(new InviteUserToChatRequest(chat_id, user_id, "Admin")); break; case 40: PushRequest(new InviteUserToChatRequest(chat_id, user_id, "User")); break; case 50: ptrW tnick_old(GetChatContactNick(si->hContact, gch->ptszUID, gch->ptszText)); ENTER_STRING pForm = {}; pForm.type = ESF_COMBO; pForm.caption = TranslateT("Enter new nickname"); pForm.szModuleName = m_szModuleName; pForm.szDataPrefix = "renamenick_"; if (EnterString(&pForm)) { if (si->hContact == NULL) break; // This probably shouldn't happen, but if chat is NULL for some reason, do nothing ptrW tnick_new(pForm.ptszResult); bool reset = mir_wstrlen(tnick_new) == 0; if (reset) { // User fill blank name, which means we reset the custom nick db_unset(si->hContact, "UsersNicks", user_id); tnick_new = GetChatContactNick(si->hContact, gch->ptszUID, gch->ptszText); } if (!mir_wstrcmp(tnick_old, tnick_new)) break; // New nick is same, do nothing GCEVENT gce = { si, GC_EVENT_NICK }; gce.dwFlags = GCEF_ADDTOLOG; gce.pszNick.w = tnick_old; gce.bIsMe = IsMe(user_id); gce.pszUID.w = gch->ptszUID; gce.pszText.w= tnick_new; gce.time = time(0); Chat_Event(&gce); if (!reset) db_set_ws(si->hContact, "UsersNicks", user_id, tnick_new); } break; } break; } return 1; } INT_PTR CSkypeProto::OnJoinChatRoom(WPARAM hContact, LPARAM) { if (hContact) { ptrW idT(getWStringA(hContact, SKYPE_SETTINGS_ID)); ptrW nameT(getWStringA(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(nullptr, 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)) { ptrW idT(getWStringA(hContact, SKYPE_SETTINGS_ID)); auto *si = Chat_Find(idT, m_szModuleName); Chat_Control(si, SESSION_OFFLINE); Chat_Terminate(si); db_delete_contact(hContact, CDF_DEL_CONTACT); } return 0; } /* CHAT EVENT */ bool CSkypeProto::OnChatEvent(const JSONNode &node) { CMStringW wszChatId(UrlToSkypeId(node["conversationLink"].as_mstring())); CMStringW szFromId(UrlToSkypeId(node["from"].as_mstring())); CMStringW wszTopic(node["threadtopic"].as_mstring()); CMStringW wszContent(node["content"].as_mstring()); SESSION_INFO *si = Chat_Find(wszChatId, m_szModuleName); if (si == nullptr) { si = StartChatRoom(wszChatId, wszTopic); if (si == nullptr) { debugLogW(L"unable to create chat %s", wszChatId.c_str()); return true; } } std::string messageType = node["messagetype"].as_string(); if (messageType == "ThreadActivity/AddMember") { // 14291862291648:initiator8:user TiXmlDocument doc; if (!doc.Parse(T2Utf(wszContent))) { if (auto *pRoot = doc.FirstChildElement("addMember")) { CMStringW target = Utf2T(XmlGetChildText(pRoot, "target")); if (!AddChatContact(si, target, L"User")) { OBJLIST arIds(1); arIds.insert(mir_u2a(target)); PushRequest(new GetChatMembersRequest(arIds, si)); } } } return true; } if (messageType == "ThreadActivity/DeleteMember") { // 14291862291648:initiator8:user TiXmlDocument doc; if (!doc.Parse(T2Utf(wszContent))) { if (auto *pRoot = doc.FirstChildElement("deletemember")) { CMStringW target = Utf2T(UrlToSkypeId(XmlGetChildText(pRoot, "target"))); CMStringW initiator = Utf2T(XmlGetChildText(pRoot, "initiator")); RemoveChatContact(si, target, true, initiator); } } return true; } if (messageType == "ThreadActivity/TopicUpdate") { // 14295327021308:usertest topic TiXmlDocument doc; if (!doc.Parse(T2Utf(wszContent))) { if (auto *pRoot = doc.FirstChildElement("topicupdate")) { CMStringW initiator = Utf2T(XmlGetChildText(pRoot, "initiator")); CMStringW value = Utf2T(XmlGetChildText(pRoot, "value")); Chat_ChangeSessionName(si, value); GCEVENT gce = { si, GC_EVENT_TOPIC }; gce.pszUID.w = initiator; gce.pszNick.w = GetSkypeNick(initiator); gce.pszText.w = wszTopic; Chat_Event(&gce); } } return true; } if (messageType == "ThreadActivity/RoleUpdate") { // 14295512583638:user8:user1admin TiXmlDocument doc; if (!doc.Parse(T2Utf(wszContent))) { if (auto *pRoot = doc.FirstChildElement("roleupdate")) { CMStringW initiator = Utf2T(UrlToSkypeId(XmlGetChildText(pRoot, "initiator"))); auto *pTarget = pRoot->FirstChildElement("target"); if (pTarget) { CMStringW id = Utf2T(UrlToSkypeId(XmlGetChildText(pTarget, "id"))); const char *role = XmlGetChildText(pTarget, "role"); GCEVENT gce = { si, !mir_strcmpi(role, "Admin") ? GC_EVENT_ADDSTATUS : GC_EVENT_REMOVESTATUS }; gce.dwFlags = GCEF_ADDTOLOG; gce.pszNick.w = id; gce.pszUID.w = id; gce.pszText.w = initiator; gce.time = time(0); gce.bIsMe = IsMe(T2Utf(id)); gce.pszStatus.w = TranslateT("Admin"); Chat_Event(&gce); } } } return true; } // some slack, let's drop it if (messageType == "ThreadActivity/HistoryDisclosedUpdate" || messageType == "ThreadActivity/JoiningEnabledUpdate") return true; return false; } ///////////////////////////////////////////////////////////////////////////////////////// void CSkypeProto::SendChatMessage(SESSION_INFO *si, const wchar_t *tszMessage) { if (!IsOnline()) return; CMStringA szMessage(ptrA(mir_utf8encodeW(tszMessage))); szMessage.TrimRight(); bool bRich = AddBbcodes(szMessage); CMStringA szUrl = "/users/ME/conversations/" + mir_urlEncode(T2Utf(si->ptszID)) + "/messages"; AsyncHttpRequest *pReq = new AsyncHttpRequest(REQUEST_POST, HOST_DEFAULT, szUrl, &CSkypeProto::OnMessageSent); JSONNode node; node << CHAR_PARAM("clientmessageid", CMStringA(::FORMAT, "%llu000", (ULONGLONG)time(0))) << CHAR_PARAM("messagetype", bRich ? "RichText" : "Text") << CHAR_PARAM("contenttype", "text") << CHAR_PARAM("content", szMessage); if (strncmp(szMessage, "/me ", 4) == 0) node << INT_PARAM("skypeemoteoffset", 4); pReq->m_szParam = node.write().c_str(); PushRequest(pReq); } ///////////////////////////////////////////////////////////////////////////////////////// void CSkypeProto::OnGetChatMembers(MHttpResponse *response, AsyncHttpRequest *pRequest) { JsonReply reply(response); if (reply.error()) return; auto &root = reply.data(); auto *si = (SESSION_INFO *)pRequest->pUserInfo; for (auto &it : root["profiles"]) { CMStringW wszUserId(Utf2T(it.name())); if (auto *pUser = g_chatApi.UM_FindUser(si, wszUserId)) { auto &pProfile = it["profile"]; if (auto &pName = pProfile["displayName"]) replaceStrW(pUser->pszNick, pName.as_mstring()); } } g_chatApi.OnChangeNick(si); } void CSkypeProto::OnGetChatInfo(MHttpResponse *response, AsyncHttpRequest*) { JsonReply reply(response); if (reply.error()) return; auto &root = reply.data(); const JSONNode &properties = root["properties"]; if (!properties["capabilities"] || properties["capabilities"].empty()) return; CMStringW wszChatId(UrlToSkypeId(root["messages"].as_mstring())); auto *si = Chat_Find(wszChatId, m_szModuleName); if (si == nullptr) return; setString(si->hContact, "Version", root["version"].as_string().c_str()); OBJLIST arIds(1); for (auto &member : root["members"]) { CMStringW username(UrlToSkypeId(member["userLink"].as_mstring())); CMStringW role = member["role"].as_mstring(); if (!AddChatContact(si, username, role, true)) arIds.insert(newStr(mir_u2a(username))); } if (arIds.getCount()) PushRequest(new GetChatMembersRequest(arIds, si)); PushRequest(new GetHistoryRequest(si->hContact, T2Utf(si->ptszID), 100, 0, true)); } wchar_t* CSkypeProto::GetChatContactNick(MCONTACT hContact, const wchar_t *id, const wchar_t *name, bool *isQualified) { if (isQualified) *isQualified = true; // Check if there is custom nick for this chat contact if (hContact) if (auto *tname = db_get_wsa(hContact, "UsersNicks", T2Utf(id))) return tname; // Check if we have this contact in database if (IsMe(id)) { // Return my nick if (auto *tname = getWStringA("Nick")) return tname; } else { hContact = FindContact(id); if (hContact != NULL) { // Primarily return custom name if (auto *tname = db_get_wsa(hContact, "CList", "MyHandle")) return tname; // If not exists, then user nick if (auto *tname = getWStringA(hContact, "Nick")) return tname; } } if (isQualified) *isQualified = false; // Return default value as nick - given name or user id if (name != nullptr) return mir_wstrdup(name); return mir_wstrdup(GetSkypeNick(id)); } bool CSkypeProto::AddChatContact(SESSION_INFO *si, const wchar_t *id, const wchar_t *role, bool isChange) { bool isQualified; ptrW szNick(GetChatContactNick(si->hContact, id, 0, &isQualified)); GCEVENT gce = { si, GC_EVENT_JOIN }; gce.dwFlags = GCEF_ADDTOLOG; gce.pszNick.w = szNick; gce.pszUID.w = id; gce.time = !isChange ? time(0) : NULL; gce.bIsMe = IsMe(id); gce.pszStatus.w = TranslateW(role); Chat_Event(&gce); return isQualified; } void CSkypeProto::RemoveChatContact(SESSION_INFO *si, const wchar_t *id, bool isKick, const wchar_t *initiator) { if (IsMe(id)) return; ptrW szNick(GetChatContactNick(si->hContact, id)); ptrW szInitiator(GetChatContactNick(si->hContact, initiator)); GCEVENT gce = { si, isKick ? GC_EVENT_KICK : GC_EVENT_PART }; gce.pszNick.w = szNick; gce.pszUID.w = id; gce.time = time(0); if (isKick) gce.pszStatus.w = szInitiator; else { gce.dwFlags += GCEF_ADDTOLOG; gce.bIsMe = IsMe(id); } Chat_Event(&gce); } void CSkypeProto::KickChatUser(const char *chatId, const char *userId) { PushRequest(new AsyncHttpRequest(REQUEST_DELETE, HOST_DEFAULT, "/threads/" + mir_urlEncode(chatId) + "/members/" + mir_urlEncode(userId))); } ///////////////////////////////////////////////////////////////////////////////////////// // Group chat creation dialog class CSkypeGCCreateDlg : public CSkypeDlgBase { CCtrlClc m_clc; public: LIST m_ContactsList; CSkypeGCCreateDlg(CSkypeProto *proto) : CSkypeDlgBase(proto, IDD_GC_CREATE), m_clc(this, IDC_CLIST), m_ContactsList(1) { m_clc.OnListRebuilt = Callback(this, &CSkypeGCCreateDlg::FilterList); } ~CSkypeGCCreateDlg() { CSkypeProto::FreeList(m_ContactsList); m_ContactsList.destroy(); } bool OnInitDialog() override { SetWindowLongPtr(m_clc.GetHwnd(), GWL_STYLE, GetWindowLongPtr(m_clc.GetHwnd(), GWL_STYLE) | CLS_CHECKBOXES | CLS_HIDEEMPTYGROUPS | CLS_USEGROUPS | CLS_GREYALTERNATE); m_clc.SendMsg(CLM_SETEXSTYLE, CLS_EX_DISABLEDRAGDROP | CLS_EX_TRACKSELECT, 0); ResetListOptions(&m_clc); return true; } bool OnApply() override { for (auto &hContact : m_proto->AccContacts()) { if (!m_proto->isChatRoom(hContact)) if (HANDLE hItem = m_clc.FindContact(hContact)) if (m_clc.GetCheck(hItem)) m_ContactsList.insert(m_proto->getId(hContact).Detach()); } m_ContactsList.insert(m_proto->m_szSkypename.GetBuffer()); return true; } void FilterList(CCtrlClc *) { for (auto &hContact : Contacts()) { char *proto = Proto_GetBaseAccountName(hContact); if (mir_strcmp(proto, m_proto->m_szModuleName) || m_proto->isChatRoom(hContact)) if (HANDLE hItem = m_clc.FindContact(hContact)) m_clc.DeleteItem(hItem); } } void ResetListOptions(CCtrlClc *) { m_clc.SetHideEmptyGroups(true); m_clc.SetHideOfflineRoot(true); } }; INT_PTR CSkypeProto::SvcCreateChat(WPARAM, LPARAM) { if (IsOnline()) { CSkypeGCCreateDlg dlg(this); if (dlg.DoModal()) { PushRequest(new CreateChatroomRequest(dlg.m_ContactsList, this)); 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[] = { { LPGENW("&Invite user..."), 10, MENU_ITEM, FALSE }, { LPGENW("&Leave chat session"), 20, MENU_ITEM, FALSE }, { LPGENW("&Change topic..."), 30, MENU_ITEM, FALSE } }; Chat_AddMenuItems(gcmi->hMenu, _countof(Items), Items, &g_plugin); } else if (gcmi->Type == MENU_ON_NICKLIST) { static const struct gc_item Items[] = { { LPGENW("Kick &user"), 10, MENU_ITEM }, { nullptr, 0, MENU_SEPARATOR }, { LPGENW("Set &role"), 20, MENU_NEWPOPUP }, { LPGENW("&Admin"), 30, MENU_POPUPITEM }, { LPGENW("&User"), 40, MENU_POPUPITEM }, { LPGENW("Change nick..."), 50, MENU_ITEM }, }; Chat_AddMenuItems(gcmi->hMenu, _countof(Items), Items, &g_plugin); } return 0; } CMStringW CSkypeProto::ChangeTopicForm() { CMStringW caption(FORMAT, L"[%s] %s", _A2T(m_szModuleName).get(), TranslateT("Enter new chatroom topic")); ENTER_STRING pForm = {}; pForm.type = ESF_MULTILINE; pForm.caption = caption; pForm.szModuleName = m_szModuleName; return (!EnterString(&pForm)) ? CMStringW() : CMStringW(ptrW(pForm.ptszResult)); }