/* Facebook plugin for Miranda Instant Messenger _____________________________________________ Copyright © 2011-17 Robert Pösel, 2017-18 Miranda NG team This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include "stdafx.h" #include #include enum ChatMenuItems { IDM_INVITE = 10, IDM_EXIT, IDM_DETAILS = 20, IDM_HISTORY }; static const struct gc_item LogMenuItems[] = { { LPGENW("&Invite user..."), IDM_INVITE, MENU_ITEM, FALSE }, { LPGENW("E&xit chat session"), IDM_EXIT, MENU_ITEM, FALSE }, }; static const struct gc_item NickMenuItems[] = { { LPGENW("User &details"), IDM_DETAILS, MENU_ITEM, FALSE }, { LPGENW("User &history"), IDM_HISTORY, MENU_ITEM, FALSE }, }; void FacebookProto::UpdateChat(const char *chat_id, const char *id, const char *name, const char *message, DWORD timestamp, bool is_old) { // replace % to %% to not interfere with chat color codes std::string smessage = message; utils::text::replace_all(&smessage, "%", "%%"); ptrW tid(mir_a2u(id)); ptrW tnick(mir_a2u_cp(name, CP_UTF8)); ptrW ttext(mir_a2u_cp(smessage.c_str(), CP_UTF8)); ptrW tchat_id(mir_a2u(chat_id)); GCEVENT gce = { m_szModuleName, tchat_id, GC_EVENT_MESSAGE }; gce.ptszText = ttext; gce.time = timestamp ? timestamp : ::time(nullptr); if (id != nullptr) gce.bIsMe = !mir_strcmp(id, facy.self_.user_id.c_str()); gce.dwFlags |= GCEF_ADDTOLOG; if (is_old) { gce.dwFlags |= GCEF_NOTNOTIFY; gce.dwFlags &= ~GCEF_ADDTOLOG; } gce.ptszNick = tnick; gce.ptszUID = tid; Chat_Event(&gce); facy.erase_reader(ChatIDToHContact(chat_id)); } void FacebookProto::RenameChat(const char *chat_id, const char *name) { ptrW tchat_id(mir_a2u(chat_id)); ptrW tname(mir_a2u_cp(name, CP_UTF8)); Chat_ChangeSessionName(m_szModuleName, tchat_id, tname); } int FacebookProto::OnGCEvent(WPARAM, LPARAM lParam) { GCHOOK *hook = reinterpret_cast(lParam); if (mir_strcmp(hook->pszModule, m_szModuleName)) return 0; // Ignore for special chatrooms if (!mir_wstrcmp(hook->ptszID, _A2W(FACEBOOK_NOTIFICATIONS_CHATROOM))) return 0; switch (hook->iType) { case GC_USER_MESSAGE: if (isOnline()) { debugLogA(" > Chat - Outgoing message"); std::string msg = _T2A(hook->ptszText, CP_UTF8); std::string chat_id = _T2A(hook->ptszID, CP_UTF8); ForkThread(&FacebookProto::SendChatMsgWorker, new send_chat(chat_id, msg)); } break; case GC_USER_PRIVMESS: { facebook_user fbu; fbu.user_id = _T2A(hook->ptszUID, CP_UTF8); // Find this contact in list or add new temporary contact MCONTACT hContact = AddToContactList(&fbu, false, true); if (!hContact) break; CallService(MS_MSG_SENDMESSAGEW, hContact); } break; case GC_USER_LOGMENU: switch (hook->dwData) { case IDM_INVITE: break; case IDM_EXIT: { std::string thread_id = _T2A(hook->ptszID, CP_UTF8); auto it = facy.chat_rooms.find(thread_id); if (it != facy.chat_rooms.end()) facy.sendRequest(facy.exitThreadRequest(it->second)); } break; } break; case GC_USER_NICKLISTMENU: MCONTACT hContact = 0; if (hook->dwData == 10 || hook->dwData == 20) { facebook_user fbu; fbu.user_id = _T2A(hook->ptszUID, CP_UTF8); // Find this contact in list or add new temporary contact hContact = AddToContactList(&fbu, false, true); if (!hContact) break; } switch (hook->dwData) { case IDM_DETAILS: CallService(MS_USERINFO_SHOWDIALOG, hContact); break; case IDM_HISTORY: CallService(MS_HISTORY_SHOWCONTACTHISTORY, hContact); break; } break; } return 0; } void FacebookProto::AddChatContact(const char *chat_id, const chatroom_participant &user, bool addToLog) { // Don't add user if it's already there if (IsChatContact(chat_id, user.user_id.c_str())) return; ptrW tchat_id(mir_a2u(chat_id)); ptrW tnick(mir_a2u_cp(user.nick.c_str(), CP_UTF8)); ptrW tid(mir_a2u(user.user_id.c_str())); GCEVENT gce = { m_szModuleName, tchat_id, GC_EVENT_JOIN }; gce.dwFlags = addToLog ? GCEF_ADDTOLOG : 0; gce.ptszNick = tnick; gce.ptszUID = tid; gce.time = ::time(nullptr); gce.bIsMe = (user.role == ROLE_ME); if (user.is_former) { gce.ptszStatus = TranslateT("Former"); } else { switch (user.role) { case ROLE_ME: gce.ptszStatus = TranslateT("Myself"); break; case ROLE_FRIEND: gce.ptszStatus = TranslateT("Friend"); break; case ROLE_NONE: gce.ptszStatus = TranslateT("User"); break; } } Chat_Event(&gce); } void FacebookProto::RemoveChatContact(const char *chat_id, const char *id, const char *name) { ptrW tchat_id(mir_a2u(chat_id)); ptrW tnick(mir_a2u_cp(name, CP_UTF8)); ptrW tid(mir_a2u(id)); GCEVENT gce = { m_szModuleName, tchat_id, GC_EVENT_PART }; gce.dwFlags = GCEF_ADDTOLOG; gce.ptszNick = tnick; gce.ptszUID = tid; gce.time = ::time(nullptr); gce.bIsMe = false; Chat_Event(&gce); } /** Caller must free result */ char *FacebookProto::GetChatUsers(const char *chat_id) { ptrW ptszChatID(mir_a2u(chat_id)); GC_INFO gci = { 0 }; gci.Flags = GCF_USERS; gci.pszModule = m_szModuleName; gci.pszID = ptszChatID; Chat_GetInfo(&gci); // mir_free(gci.pszUsers); return gci.pszUsers; } bool FacebookProto::IsChatContact(const char *chat_id, const char *id) { ptrA users(GetChatUsers(chat_id)); return (users != nullptr && strstr(users, id) != nullptr); } void FacebookProto::AddChat(const char *id, const wchar_t *tname) { ptrW tid(mir_a2u(id)); // Create the group chat session Chat_NewSession(GCW_PRIVMESS, m_szModuleName, tid, tname); // Send setting events Chat_AddGroup(m_szModuleName, tid, TranslateT("Myself")); Chat_AddGroup(m_szModuleName, tid, TranslateT("Friend")); Chat_AddGroup(m_szModuleName, tid, TranslateT("User")); Chat_AddGroup(m_szModuleName, tid, TranslateT("Former")); // Finish initialization bool hideChats = getBool(FACEBOOK_KEY_HIDE_CHATS, DEFAULT_HIDE_CHATS); Chat_Control(m_szModuleName, tid, (hideChats ? WINDOW_HIDDEN : SESSION_INITDONE)); Chat_Control(m_szModuleName, tid, SESSION_ONLINE); } INT_PTR FacebookProto::OnJoinChat(WPARAM hContact, LPARAM) { if (isOffline() || facy.dtsg_.empty() || !m_enableChat || IsSpecialChatRoom(hContact)) return 0; ptrW idT(getWStringA(hContact, "ChatRoomID")); ptrA threadId(getStringA(hContact, FACEBOOK_KEY_TID)); if (!idT || !threadId) return 0; facebook_chatroom *fbc; std::string thread_id = threadId; auto it = facy.chat_rooms.find(thread_id); if (it != facy.chat_rooms.end()) fbc = it->second; else { // We don't have this chat loaded in memory yet, lets load some info (name, list of users) fbc = new facebook_chatroom(thread_id); LoadChatInfo(fbc); facy.chat_rooms.insert(std::make_pair(thread_id, fbc)); // Update loaded info about this chat setByte(hContact, FACEBOOK_KEY_CHAT_CAN_REPLY, fbc->can_reply); setByte(hContact, FACEBOOK_KEY_CHAT_READ_ONLY, fbc->read_only); setByte(hContact, FACEBOOK_KEY_CHAT_IS_ARCHIVED, fbc->is_archived); setByte(hContact, FACEBOOK_KEY_CHAT_IS_SUBSCRIBED, fbc->is_subscribed); } // RM TODO: better use check if chatroom exists/is in db/is online... no? // like: if (ChatIDToHContact(thread_id) == nullptr) { ptrA users(GetChatUsers(thread_id.c_str())); if (users == nullptr) { // Add chatroom AddChat(fbc->thread_id.c_str(), fbc->chat_name.c_str()); // Add chat contacts for (auto &jt : fbc->participants) AddChatContact(fbc->thread_id.c_str(), jt.second, false); // Load last messages delSetting(hContact, FACEBOOK_KEY_MESSAGE_ID); // We're creating new chatroom so we want load all recent messages ForkThread(&FacebookProto::LoadLastMessages, new MCONTACT(hContact)); } return 0; } INT_PTR FacebookProto::OnLeaveChat(WPARAM wParam, LPARAM) { ptrW idT(wParam ? getWStringA(wParam, "ChatRoomID") : nullptr); Chat_Control(m_szModuleName, idT, SESSION_OFFLINE); Chat_Terminate(m_szModuleName, idT); if (!wParam) facy.clear_chatrooms(); else if (!IsSpecialChatRoom(wParam)) { ptrA threadId(getStringA(wParam, FACEBOOK_KEY_TID)); if (!threadId) return 0; auto it = facy.chat_rooms.find(std::string(threadId)); if (it != facy.chat_rooms.end()) { delete it->second; facy.chat_rooms.erase(it); } } return 0; } int FacebookProto::OnGCMenuHook(WPARAM, LPARAM lParam) { GCMENUITEMS *gcmi = (GCMENUITEMS*)lParam; if (mir_strcmp(gcmi->pszModule, m_szModuleName)) return 0; if (gcmi->Type == MENU_ON_LOG) Chat_AddMenuItems(gcmi->hMenu, _countof(LogMenuItems), LogMenuItems); else if (gcmi->Type == MENU_ON_NICKLIST) Chat_AddMenuItems(gcmi->hMenu, _countof(NickMenuItems), NickMenuItems); return 0; } bool FacebookProto::IsSpecialChatRoom(MCONTACT hContact) { if (!isChatRoom(hContact)) return false; ptrA id(getStringA(hContact, "ChatRoomID")); return id && !mir_strcmp(id, FACEBOOK_NOTIFICATIONS_CHATROOM); } void FacebookProto::PrepareNotificationsChatRoom() { if (!getBool(FACEBOOK_KEY_NOTIFICATIONS_CHATROOM, DEFAULT_NOTIFICATIONS_CHATROOM)) return; // Prepare notifications chatroom if not exists MCONTACT hNotificationsChatRoom = ChatIDToHContact(FACEBOOK_NOTIFICATIONS_CHATROOM); if (hNotificationsChatRoom == 0 || getDword(hNotificationsChatRoom, "Status", ID_STATUS_OFFLINE) != ID_STATUS_ONLINE) { wchar_t nameT[200]; mir_snwprintf(nameT, L"%s: %s", m_tszUserName, TranslateT("Notifications")); // Create the group chat session Chat_NewSession(GCW_PRIVMESS, m_szModuleName, _A2W(FACEBOOK_NOTIFICATIONS_CHATROOM), nameT); // Send setting events Chat_Control(m_szModuleName, _A2W(FACEBOOK_NOTIFICATIONS_CHATROOM), WINDOW_HIDDEN); Chat_Control(m_szModuleName, _A2W(FACEBOOK_NOTIFICATIONS_CHATROOM), SESSION_ONLINE); } } void FacebookProto::UpdateNotificationsChatRoom(facebook_notification *notification) { if (!getBool(FACEBOOK_KEY_NOTIFICATIONS_CHATROOM, DEFAULT_NOTIFICATIONS_CHATROOM)) return; std::stringstream text; text << notification->text << "\n\n" << PrepareUrl(notification->link); std::string message = text.str(); utils::text::replace_all(&message, "%", "%%"); ptrW idT(mir_wstrdup(_A2W(FACEBOOK_NOTIFICATIONS_CHATROOM))); ptrW messageT(mir_a2u_cp(message.c_str(), CP_UTF8)); GCEVENT gce = { m_szModuleName, _A2W(FACEBOOK_NOTIFICATIONS_CHATROOM), GC_EVENT_MESSAGE }; gce.ptszText = messageT; gce.time = notification->time ? notification->time : ::time(nullptr); gce.bIsMe = false; gce.dwFlags |= GCEF_ADDTOLOG; gce.ptszNick = TranslateT("Notifications"); gce.ptszUID = idT; Chat_Event(&gce); } std::string FacebookProto::GenerateChatName(facebook_chatroom *fbc) { std::string name = ""; unsigned int namesUsed = 0; for (auto &it : fbc->participants) { std::string participant = it.second.nick; // Ignore self contact, empty and numeric only participant names if (it.second.role == ROLE_ME || participant.empty() || participant.find_first_not_of("0123456789") == std::string::npos) continue; if (namesUsed > 0) name += ", "; name += utils::text::prepare_name(participant, false); if (++namesUsed >= FACEBOOK_CHATROOM_NAMES_COUNT) break; } // Participants.size()-1 because we ignore self contact if (fbc->participants.size() - 1 > namesUsed) { wchar_t more[200]; mir_snwprintf(more, TranslateT("%s and more (%d)"), fbc->chat_name.c_str(), fbc->participants.size() - 1 - namesUsed); // -1 because we ignore self contact fbc->chat_name = more; } // If there are no participants to create a name from, use just thread_id if (name.empty()) name = fbc->thread_id.c_str(); return name; } void FacebookProto::LoadParticipantsNames(facebook_chatroom *fbc) { std::vector namelessIds; // TODO: We could load all names from server at once by skipping this for cycle and using namelessIds as all in participants list, but we would lost our local names of our contacts. But maybe that's not a problem? for (auto &it : fbc->participants) { const char *id = it.first.c_str(); chatroom_participant &user = it.second; if (!user.loaded) { if (!mir_strcmp(id, facy.self_.user_id.c_str())) { user.nick = facy.self_.real_name; user.role = ROLE_ME; user.loaded = true; } else { MCONTACT hContact = ContactIDToHContact(id); if (hContact != 0) { DBVARIANT dbv; if (!getStringUtf(hContact, FACEBOOK_KEY_NICK, &dbv)) { user.nick = dbv.pszVal; db_free(&dbv); } if (user.role == ROLE_NONE) { int type = getByte(hContact, FACEBOOK_KEY_CONTACT_TYPE); if (type == CONTACT_FRIEND) user.role = ROLE_FRIEND; else user.role = ROLE_NONE; } user.loaded = true; } if (!user.loaded) namelessIds.push_back(id); } } } if (!namelessIds.empty()) { // we have some contacts without name, let's load them all from the server LIST userIds(1); for (std::string::size_type i = 0; i < namelessIds.size(); i++) userIds.insert(mir_strdup(namelessIds.at(i).c_str())); http::response resp = facy.sendRequest(facy.userInfoRequest(userIds)); FreeList(userIds); userIds.destroy(); if (resp.code == HTTP_CODE_OK) { try { // TODO: We can cache these results and next time (e.g. for different chatroom) we can use that already cached names ParseChatParticipants(&resp.data, &fbc->participants); debugLogA("*** Participant names processed"); } catch (const std::exception &e) { debugLogA("*** Error processing participant names: %s", e.what()); } } } } void FacebookProto::JoinChatrooms() { for (MCONTACT hContact = db_find_first(m_szModuleName); hContact; hContact = db_find_next(hContact, m_szModuleName)) { if (!isChatRoom(hContact)) continue; // Ignore archived and unsubscribed chats if (getBool(hContact, FACEBOOK_KEY_CHAT_IS_ARCHIVED, false) || !getBool(hContact, FACEBOOK_KEY_CHAT_IS_SUBSCRIBED, true)) continue; OnJoinChat(hContact, 0); } } void FacebookProto::LoadChatInfo(facebook_chatroom *fbc) { if (isOffline()) return; // request info about chat thread http::response resp = facy.sendRequest(facy.threadInfoRequest(true, fbc->thread_id.c_str())); if (resp.code != HTTP_CODE_OK) { facy.handle_error("LoadChatInfo"); return; } try { ParseChatInfo(&resp.data, fbc); // Load missing participants names LoadParticipantsNames(fbc); // If chat has no name, create name from participants list if (fbc->chat_name.empty()) { std::string newName = GenerateChatName(fbc); fbc->chat_name = _A2T(newName.c_str(), CP_UTF8); } debugLogA("*** Chat thread info processed"); } catch (const std::exception &e) { debugLogA("*** Error processing chat thread info: %s", e.what()); } facy.handle_success("LoadChatInfo"); } int FacebookProto::ParseChatInfo(std::string *data, facebook_chatroom* fbc) { size_t len = data->find("\r\n"); if (len != data->npos) data->erase(len); JSONNode root = JSONNode::parse(data->c_str()); if (!root) return EXIT_FAILURE; const JSONNode &thread = root["o0"]["data"]["message_thread"]; if (!thread) return EXIT_FAILURE; const JSONNode &thread_fbid_ = thread["thread_key"]["thread_fbid"]; const JSONNode &name_ = thread["name"]; if (!thread_fbid_) return EXIT_FAILURE; std::string tid = "id." + thread_fbid_.as_string(); if (fbc->thread_id != tid) return EXIT_FAILURE; chatroom_participant user; user.is_former = false; user.role = ROLE_NONE; const JSONNode &participants = thread["all_participants"]["nodes"]; for (auto &jt : participants) { user.user_id = jt["messaging_actor"]["id"].as_string(); fbc->participants.insert(std::make_pair(user.user_id, user)); } fbc->can_reply = thread["can_reply"].as_bool(); fbc->is_archived = thread["has_viewer_archived"].as_bool(); fbc->is_subscribed = thread["is_viewer_subscribed"].as_bool(); fbc->read_only = thread["read_only"].as_bool(); fbc->chat_name = std::wstring(ptrW(mir_utf8decodeW(name_.as_string().c_str()))); return EXIT_SUCCESS; }