/*
Copyright (c) 2015-19 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.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);
}
MCONTACT CSkypeProto::FindChatRoom(const char *chatname)
{
SESSION_INFO *si = g_chatApi.SM_FindSession(_A2T(chatname), m_szModuleName);
return si ? si->hContact : 0;
}
void CSkypeProto::StartChatRoom(const wchar_t *tid, const wchar_t *tname)
{
// Create the group chat session
SESSION_INFO *si = Chat_NewSession(GCW_CHATROOM, m_szModuleName, tid, tname);
if (!si)
return;
// Create a user statuses
Chat_AddGroup(si, TranslateT("Admin"));
Chat_AddGroup(si, TranslateT("User"));
// Finish initialization
Chat_Control(m_szModuleName, tid, (getBool("HideChats", 1) ? WINDOW_HIDDEN : SESSION_INITDONE));
Chat_Control(m_szModuleName, tid, SESSION_ONLINE);
}
void CSkypeProto::OnLoadChats(const NETLIBHTTPREQUEST *response)
{
if (response == nullptr)
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(), this), &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;
CMStringW topic(threadProperties["topic"].as_mstring());
SendRequest(new GetChatInfoRequest(id.as_string().c_str(), this), &CSkypeProto::OnGetChatInfo, topic.Detach());
}
}
/* Hooks */
int CSkypeProto::OnGroupChatEventHook(WPARAM, LPARAM lParam)
{
GCHOOK *gch = (GCHOOK*)lParam;
if (!gch)
return 1;
if (mir_strcmp(gch->pszModule, m_szModuleName) != 0)
return 0;
T2Utf chat_id(gch->ptszID), user_id(gch->ptszUID);
switch (gch->iType) {
case GC_USER_MESSAGE:
OnSendChatMessage(chat_id, gch->ptszText);
break;
case GC_USER_PRIVMESS:
{
MCONTACT hContact = FindContact(user_id);
if (hContact == NULL) {
hContact = AddContact(user_id, true);
setWord(hContact, "Status", ID_STATUS_ONLINE);
db_set_b(hContact, "CList", "Hidden", 1);
setWString(hContact, "Nick", gch->ptszUID);
db_set_dw(hContact, "Ignore", "Mask1", 0);
}
CallService(MS_MSG_SENDMESSAGEW, 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", this));
{
mir_cslock lck(m_InviteDialogsLock);
m_InviteDialogs.remove(&dlg);
}
}
break;
case 20:
OnLeaveChatRoom(FindChatRoom(chat_id), NULL);
break;
case 30:
CMStringW newTopic = ChangeTopicForm();
if (!newTopic.IsEmpty())
SendRequest(new SetChatPropertiesRequest(chat_id, "topic", T2Utf(newTopic.GetBuffer()), this));
break;
}
break;
case GC_USER_NICKLISTMENU:
switch (gch->dwData) {
case 10:
SendRequest(new KickUserRequest(chat_id, user_id, this));
break;
case 30:
SendRequest(new InviteUserToChatRequest(chat_id, user_id, "Admin", this));
break;
case 40:
SendRequest(new InviteUserToChatRequest(chat_id, user_id, "User", this));
break;
case 50:
ptrA tnick_old(GetChatContactNick(chat_id, user_id, T2Utf(gch->ptszText)));
ENTER_STRING pForm = { sizeof(pForm) };
pForm.type = ESF_COMBO;
pForm.recentCount = 0;
pForm.caption = TranslateT("Enter new nickname");
pForm.szModuleName = m_szModuleName;
pForm.szDataPrefix = "renamenick_";
if (EnterString(&pForm)) {
MCONTACT hChatContact = FindChatRoom(chat_id);
if (hChatContact == NULL)
break; // This probably shouldn't happen, but if chat is NULL for some reason, do nothing
ptrA tnick_new(mir_utf8encodeW(pForm.ptszResult));
bool reset = mir_strlen(tnick_new) == 0;
if (reset) {
// User fill blank name, which means we reset the custom nick
db_unset(hChatContact, "UsersNicks", user_id);
tnick_new = GetChatContactNick(chat_id, user_id, _T2A(gch->ptszText));
}
if (!mir_strcmp(tnick_old, tnick_new))
break; // New nick is same, do nothing
GCEVENT gce = { m_szModuleName, chat_id, GC_EVENT_NICK };
gce.dwFlags = GCEF_UTF8 + GCEF_ADDTOLOG;
gce.pszNick.a = tnick_old;
gce.bIsMe = IsMe(user_id);
gce.pszUID.a = user_id;
gce.pszText.a = tnick_new;
gce.time = time(0);
Chat_Event(&gce);
if (!reset)
db_set_utf(hChatContact, "UsersNicks", user_id, tnick_new);
}
break;
}
break;
}
return 0;
}
INT_PTR CSkypeProto::OnJoinChatRoom(WPARAM hContact, LPARAM)
{
if (hContact) {
ptrW idT(getWStringA(hContact, "ChatRoomID"));
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, "ChatRoomID"));
Chat_Control(m_szModuleName, idT, SESSION_OFFLINE);
Chat_Terminate(m_szModuleName, idT);
SendRequest(new KickUserRequest(_T2A(idT), m_szSkypename, this));
db_delete_contact(hContact);
}
return 0;
}
/* CHAT EVENT */
void CSkypeProto::OnChatEvent(const JSONNode &node)
{
CMStringA szConversationName(UrlToSkypename(node["conversationLink"].as_string().c_str()));
CMStringA szFromSkypename(UrlToSkypename(node["from"].as_string().c_str()));
CMStringW 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, this), &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(szConversationName, szFromSkypename, szClearedContent, nEmoteOffset != NULL, nEmoteOffset, timestamp);
}
else if (messageType == "ThreadActivity/AddMember") {
// 14291862291648:initiator8:user
TiXmlDocument doc;
if (0 != doc.Parse(strContent.c_str()))
return;
if (auto *pRoot = doc.FirstChildElement("addMember")) {
CMStringA target = ParseUrl(pRoot->FirstChildElement("target")->Value(), "8:");
AddChatContact(szConversationName, target, target, "User");
}
}
else if (messageType == "ThreadActivity/DeleteMember") {
// 14291862291648:initiator8:user
TiXmlDocument doc;
if (0 != doc.Parse(strContent.c_str()))
return;
if (auto *pRoot = doc.FirstChildElement("deletemember")) {
CMStringA target = ParseUrl(pRoot->FirstChildElement("target")->Value(), "8:");
CMStringA initiator = ParseUrl(pRoot->FirstChildElement("initiator")->Value(), "8:");
RemoveChatContact(szConversationName, target, target, true, initiator);
}
}
else if (messageType == "ThreadActivity/TopicUpdate") {
// 14295327021308:usertest topic
TiXmlDocument doc;
if (0 != doc.Parse(strContent.c_str()))
return;
auto *pRoot = doc.FirstChildElement("topicupdate");
if (pRoot) {
CMStringA initiator = ParseUrl(pRoot->FirstChildElement("initiator")->Value(), "8:");
CMStringA value = pRoot->FirstChildElement("value")->Value();
RenameChat(szConversationName, value);
ChangeChatTopic(szConversationName, value, initiator);
}
}
else if (messageType == "ThreadActivity/RoleUpdate") {
// 14295512583638:user8:user1admin
TiXmlDocument doc;
if (0 != doc.Parse(strContent.c_str()))
return;
auto *pRoot = doc.FirstChildElement("roleupdate");
if (pRoot) {
CMStringA initiator = ParseUrl(pRoot->FirstChildElement("initiator")->Value(), "8:");
auto *pTarget = pRoot->FirstChildElement("target");
if (pTarget) {
CMStringA id = ParseUrl(pTarget->FirstChildElement("id")->Value(), "8:");
const char *role = pTarget->FirstChildElement("role")->Value();
GCEVENT gce = { m_szModuleName, szConversationName, !mir_strcmpi(role, "Admin") ? GC_EVENT_ADDSTATUS : GC_EVENT_REMOVESTATUS };
gce.dwFlags = GCEF_ADDTOLOG + GCEF_UTF8;
gce.pszNick.a = id;
gce.pszUID.a = id;
gce.pszText.a = initiator;
gce.time = time(0);
gce.bIsMe = IsMe(id);
gce.pszStatus.a = TranslateU("Admin");
Chat_Event(&gce);
}
}
}
}
void CSkypeProto::OnSendChatMessage(const char *chat_id, const wchar_t *tszMessage)
{
if (!IsOnline())
return;
wchar_t *buf = NEWWSTR_ALLOCA(tszMessage);
rtrimw(buf);
Chat_UnescapeTags(buf);
ptrA szMessage(mir_utf8encodeW(buf));
if (strncmp(szMessage, "/me ", 4) == 0)
SendRequest(new SendChatActionRequest(chat_id, time(0), szMessage, this));
else
SendRequest(new SendChatMessageRequest(chat_id, time(0), szMessage, this));
}
void CSkypeProto::AddMessageToChat(const char *chat_id, const char *from, const char *content, bool isAction, int emoteOffset, time_t timestamp, bool isLoading)
{
ptrA tnick(GetChatContactNick(chat_id, from, from));
GCEVENT gce = { m_szModuleName, chat_id, isAction ? GC_EVENT_ACTION : GC_EVENT_MESSAGE };
gce.dwFlags = GCEF_UTF8;
gce.bIsMe = IsMe(from);
gce.pszNick.a = tnick;
gce.time = timestamp;
gce.pszUID.a = from;
CMStringA szText(content);
szText.Replace("%", "%%");
if (!isAction) {
gce.pszText.a = szText;
gce.dwFlags |= GCEF_ADDTOLOG;
}
else gce.pszText.a = szText.GetBuffer() + emoteOffset;
if (isLoading)
gce.dwFlags |= GCEF_NOTNOTIFY;
Chat_Event(&gce);
}
void CSkypeProto::OnGetChatInfo(const NETLIBHTTPREQUEST *response, void *p)
{
ptrW topic((wchar_t*)p); // memory must be freed in any case
if (response == nullptr || response->pData == nullptr)
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();
AddChatContact(chatId, username, username, role.c_str(), true);
}
PushRequest(new GetHistoryRequest(chatId, 15, true, 0, this), &CSkypeProto::OnGetServerHistory);
}
void CSkypeProto::RenameChat(const char *chat_id, const char *name)
{
ptrW tchat_id(mir_a2u(chat_id));
ptrW tname(mir_utf8decodeW(name));
Chat_ChangeSessionName(m_szModuleName, tchat_id, tname);
}
void CSkypeProto::ChangeChatTopic(const char *chat_id, const char *topic, const char *initiator)
{
GCEVENT gce = { m_szModuleName, chat_id, GC_EVENT_TOPIC };
gce.dwFlags = GCEF_UTF8;
gce.pszUID.a = initiator;
gce.pszNick.a = initiator;
gce.pszText.a = topic;
Chat_Event(&gce);
}
char* CSkypeProto::GetChatContactNick(const char *chat_id, const char *id, const char *name)
{
// Check if there is custom nick for this chat contact
if (chat_id != nullptr) {
if (char *tname = db_get_utfa(FindChatRoom(chat_id), "UsersNicks", id))
return tname;
}
// Check if we have this contact in database
if (IsMe(id)) {
// Return my nick
if (char *tname = getUStringA(NULL, "Nick"))
return tname;
}
else {
MCONTACT hContact = FindContact(id);
if (hContact != NULL) {
// Primarily return custom name
if (char *tname = db_get_utfa(hContact, "CList", "MyHandle"))
return tname;
// If not exists, then user nick
if (char *tname = getUStringA(hContact, "Nick"))
return tname;
}
}
// Return default value as nick - given name or user id
if (name != nullptr)
return mir_strdup(name);
return mir_strdup(id);
}
void CSkypeProto::AddChatContact(const char *chat_id, const char *id, const char *name, const char *role, bool isChange)
{
ptrA szNick(GetChatContactNick(chat_id, id, name));
GCEVENT gce = { m_szModuleName, chat_id, GC_EVENT_JOIN };
gce.dwFlags = GCEF_UTF8 + GCEF_ADDTOLOG;
gce.pszNick.a = szNick;
gce.pszUID.a = id;
gce.time = !isChange ? time(0) : NULL;
gce.bIsMe = IsMe(id);
gce.pszStatus.a = TranslateU(role);
Chat_Event(&gce);
}
void CSkypeProto::RemoveChatContact(const char *chat_id, const char *id, const char *name, bool isKick, const char *initiator)
{
if (IsMe(id))
return;
ptrA szNick(GetChatContactNick(chat_id, id, name));
ptrA szInitiator(GetChatContactNick(chat_id, initiator, initiator));
ptrW tid(mir_a2u(id));
GCEVENT gce = { m_szModuleName, chat_id, isKick ? GC_EVENT_KICK : GC_EVENT_PART };
gce.dwFlags = GCEF_UTF8;
gce.pszNick.a = szNick;
gce.pszUID.a = id;
gce.time = time(0);
if (isKick)
gce.pszStatus.a = szInitiator;
else {
gce.dwFlags += GCEF_ADDTOLOG;
gce.bIsMe = IsMe(id);
}
Chat_Event(&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, this));
{ 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[] =
{
{ 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), TranslateT("Enter new chatroom topic"));
ENTER_STRING pForm = { sizeof(pForm) };
pForm.type = ESF_MULTILINE;
pForm.caption = caption;
pForm.ptszInitVal = nullptr;
pForm.szModuleName = m_szModuleName;
return (!EnterString(&pForm)) ? CMStringW() : CMStringW(ptrW(pForm.ptszResult));
}