/*
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));
}