From e166b028427169375fb5da938ada6f3f3db520d2 Mon Sep 17 00:00:00 2001
From: George Hazan <ghazan@miranda.im>
Date: Wed, 22 Feb 2017 18:34:13 +0300
Subject: major rework of Discord:

- CDiscordGuild - a class to utilize all guild-related activity;
- all guild related code moved to the separate module;
- fix for online guild channel creation;
- version bump;
---
 protocols/Discord/src/dispatch.cpp  | 306 ++++++++++++++----------------------
 protocols/Discord/src/groupchat.cpp |   9 +-
 protocols/Discord/src/guilds.cpp    | 177 +++++++++++++++++++++
 protocols/Discord/src/menus.cpp     |  34 +++-
 protocols/Discord/src/proto.cpp     |   8 +-
 protocols/Discord/src/proto.h       |  61 ++++++-
 protocols/Discord/src/stdafx.h      |   1 -
 protocols/Discord/src/version.h     |   4 +-
 8 files changed, 389 insertions(+), 211 deletions(-)
 create mode 100644 protocols/Discord/src/guilds.cpp

(limited to 'protocols')

diff --git a/protocols/Discord/src/dispatch.cpp b/protocols/Discord/src/dispatch.cpp
index d960bf1cac..6a8992322b 100644
--- a/protocols/Discord/src/dispatch.cpp
+++ b/protocols/Discord/src/dispatch.cpp
@@ -76,12 +76,26 @@ GatewayHandlerFunc CDiscordProto::GetHandler(const wchar_t *pwszCommand)
 
 void CDiscordProto::OnCommandChannelCreated(const JSONNode &pRoot)
 {
-	const JSONNode &members = pRoot["recipients"];
-	for (auto it = members.begin(); it != members.end(); ++it) {
-		CDiscordUser *pUser = PrepareUser(*it);
-		pUser->lastMsg = CDiscordMessage(::getId(pRoot["last_message_id"]));
-		pUser->channelId = ::getId(pRoot["id"]);
-		setId(pUser->hContact, DB_KEY_CHANNELID, pUser->channelId);
+	SnowFlake guildId = ::getId(pRoot["guild_id"]);
+	if (guildId == 0) {
+		// private channel, created for a contact
+		const JSONNode &members = pRoot["recipients"];
+		for (auto it = members.begin(); it != members.end(); ++it) {
+			CDiscordUser *pUser = PrepareUser(*it);
+			pUser->lastMsg = CDiscordMessage(::getId(pRoot["last_message_id"]));
+			pUser->channelId = ::getId(pRoot["id"]);
+			setId(pUser->hContact, DB_KEY_CHANNELID, pUser->channelId);
+		}
+	}
+	else {
+		CDiscordGuild *pGuild = FindGuild(guildId);
+		if (pGuild == nullptr)
+			return;
+
+		// group channel for a guild
+		CDiscordUser *pUser = ProcessGuildChannel(pGuild, pRoot);
+		if (pUser != nullptr)
+			ApplyUsersToChannel(pGuild, *pUser);
 	}
 }
 
@@ -141,205 +155,104 @@ void CDiscordProto::OnCommandFriendRemoved(const JSONNode &pRoot)
 /////////////////////////////////////////////////////////////////////////////////////////
 // guild synchronization
 
-static int sttGetPresence(const JSONNode &pStatuses, const CMStringW &wszId)
-{
-	for (auto it = pStatuses.begin(); it != pStatuses.end(); ++it) {
-		const JSONNode &s = *it;
-
-		CMStringW wszUserId = s["user"]["id"].as_mstring();
-		if (wszUserId == wszId)
-			return StrToStatus(s["status"].as_mstring());
-	}
-
-	return 0;
-}
-
-static SnowFlake sttGetLastRead(const JSONNode &reads, const wchar_t *wszChannelId)
-{
-	for (auto it = reads.begin(); it != reads.end(); ++it) {
-		const JSONNode &p = *it;
-
-		if (p["id"].as_mstring() == wszChannelId)
-			return ::getId(p["last_message_id"]);
-	}
-	return 0;
-}
-
-void CDiscordProto::ProcessRole(SnowFlake guildId, const JSONNode &role)
-{
-	SnowFlake id = ::getId(role["id"]);
-	CDiscordRole *p = arRoles.find((CDiscordRole*)&id);
-	if (p == nullptr) {
-		p = new CDiscordRole();
-		p->id = id;
-		arRoles.insert(p);
-	}
-	p->guildId = guildId;
-	p->color = role["color"].as_int();
-	p->position = role["position"].as_int();
-	p->permissions = role["permissions"].as_int();
-	p->wszName = role["name"].as_mstring();
-}
-
-void CDiscordProto::ProcessGuild(const JSONNode &readState, const JSONNode &p)
-{
-	SnowFlake guildId = ::getId(p["id"]), ownerId = ::getId(p["owner_id"]);
-	GatewaySendGuildInfo(guildId);
-	CMStringW wszGuildName = p["name"].as_mstring();
-
-	GCSessionInfoBase *si = Chat_NewSession(GCW_SERVER, m_szModuleName, wszGuildName, wszGuildName);
-	Chat_Control(m_szModuleName, wszGuildName, WINDOW_HIDDEN);
-	Chat_Control(m_szModuleName, wszGuildName, SESSION_ONLINE);
-	setId(si->hContact, DB_KEY_CHANNELID, guildId);
-
-	const JSONNode &roles = p["roles"];
-	for (auto itr = roles.begin(); itr != roles.end(); ++itr)
-		ProcessRole(guildId, *itr);
-
-	const JSONNode &channels = p["channels"];
-	for (auto itc = channels.begin(); itc != channels.end(); ++itc) {
-		const JSONNode &pch = *itc;
-		if (pch["type"].as_int() != 0)
-			continue;
-
-		CMStringW wszChannelName = pch["name"].as_mstring();
-		CMStringW wszChannelId = pch["id"].as_mstring();
-		CMStringW wszTopic = pch["topic"].as_mstring();
-		SnowFlake channelId = _wtoi64(wszChannelId);
-
-		si = Chat_NewSession(GCW_CHATROOM, m_szModuleName, wszChannelId, wszGuildName + L"#" + wszChannelName);
-		setId(si->hContact, DB_KEY_OWNERID, ownerId);
-		BuildStatusList(guildId, wszChannelId);
-
-		Chat_Control(m_szModuleName, wszChannelId, WINDOW_HIDDEN);
-		Chat_Control(m_szModuleName, wszChannelId, SESSION_ONLINE);
-
-		if (!wszTopic.IsEmpty()) {
-			Chat_SetStatusbarText(m_szModuleName, wszChannelId, wszTopic);
-
-			GCDEST gcd = { m_szModuleName, wszChannelId, GC_EVENT_TOPIC };
-			GCEVENT gce = { &gcd };
-			gce.time = time(0);
-			gce.ptszText = wszTopic;
-			Chat_Event(&gce);
-		}
-
-		CDiscordUser *pUser = FindUserByChannel(channelId);
-		if (pUser == NULL) {
-			// missing channel - create it
-			pUser = new CDiscordUser(channelId);
-			pUser->bIsPrivate = false;
-			pUser->hContact = si->hContact;
-			pUser->id = channelId;
-			pUser->channelId = channelId;
-			arUsers.insert(pUser);
-		}
-		pUser->wszUsername = wszChannelId;
-		pUser->guildId = guildId;
-		pUser->lastMsg = CDiscordMessage(::getId(pch["last_message_id"]));
-		pUser->lastReadId = sttGetLastRead(readState, wszChannelId);
-
-		setId(pUser->hContact, DB_KEY_ID, channelId);
-		setId(pUser->hContact, DB_KEY_CHANNELID, channelId);
-
-		SnowFlake oldMsgId = getId(pUser->hContact, DB_KEY_LASTMSGID);
-		if (oldMsgId != 0 && pUser->lastMsg.id > oldMsgId)
-			RetrieveHistory(pUser->hContact, MSG_AFTER, oldMsgId, 99);
-	}
-}
-
 void CDiscordProto::OnCommandGuildCreated(const JSONNode &pRoot)
 {
-	ProcessGuild(JSONNode(), pRoot);
+	ProcessGuild(pRoot);
 	OnCommandGuildSync(pRoot);
 }
 
 void CDiscordProto::OnCommandGuildSync(const JSONNode &pRoot)
 {
-	const JSONNode &pStatuses = pRoot["presences"];
-	const JSONNode &pMembers = pRoot["members"];
-
-	SnowFlake guildId = ::getId(pRoot["id"]);
-
-	for (int i = 0; i < arUsers.getCount(); i++) {
-		CDiscordUser &pUser = arUsers[i];
-		if (pUser.guildId != guildId)
-			continue;
-
-		SESSION_INFO *si = pci->SM_FindSession(pUser.wszUsername, m_szModuleName);
-		if (si == NULL)
-			continue;
+	CDiscordGuild *pGuild = FindGuild(::getId(pRoot["id"]));
+	if (pGuild == nullptr)
+		return;
 
-		GCDEST gcd = { m_szModuleName, pUser.wszUsername, GC_EVENT_JOIN };
-		GCEVENT gce = { &gcd };
-		gce.time = time(0);
-		gce.dwFlags = GCEF_SILENT;
+	// store all guild members
+	const JSONNode &pMembers = pRoot["members"];
+	for (auto it = pMembers.begin(); it != pMembers.end(); ++it) {
+		const JSONNode &m = *it;
+
+		CMStringW wszUserId = m["user"]["id"].as_mstring();
+		SnowFlake userId = _wtoi64(wszUserId);
+		CDiscordGuildMember *pm = pGuild->FindUser(userId);
+		if (pm == nullptr) {
+			pm = new CDiscordGuildMember(userId);
+			pGuild->arChatUsers.insert(pm);
+		}
 
-		for (auto it = pMembers.begin(); it != pMembers.end(); ++it) {
-			const JSONNode &m = *it;
+		pm->wszNick = m["nick"].as_mstring();
+		if (pm->wszNick.IsEmpty())
+			pm->wszNick = m["user"]["username"].as_mstring() + L"#" + m["user"]["discriminator"].as_mstring();
 
+		if (userId == pGuild->ownerId)
+			pm->wszRole = L"@owner";
+		else {
 			CDiscordRole *pRole = nullptr;
 			const JSONNode &pRoles = m["roles"];
 			for (auto itr = pRoles.begin(); itr != pRoles.end(); ++itr) {
 				SnowFlake roleId = ::getId(*itr);
-				if (pRole = arRoles.find((CDiscordRole*)&roleId))
+				if (pRole = pGuild->arRoles.find((CDiscordRole*)&roleId))
 					break;
 			}
-
-			CMStringW wszNick = m["nick"].as_mstring();
-			CMStringW wszUsername = m["user"]["username"].as_mstring() + L"#" + m["user"]["discriminator"].as_mstring();
-			CMStringW wszUserId = m["user"]["id"].as_mstring();
-			SnowFlake userId = _wtoi64(wszUserId);
-			if (userId == getId(pUser.hContact, DB_KEY_OWNERID))
-				gce.ptszStatus = L"@owner";
-			else
-				gce.ptszStatus = (pRole == nullptr) ? L"@everyone" : pRole->wszName;
-
-			gce.bIsMe = (userId == m_ownId);
-			gce.ptszUID = wszUserId;
-			gce.ptszNick = wszNick.IsEmpty() ? wszUsername : wszNick;
-			Chat_Event(&gce);
-
-			int flags = GC_SSE_ONLYLISTED;
-			switch (sttGetPresence(pStatuses, wszUserId)) {
-			case ID_STATUS_ONLINE: case ID_STATUS_NA: case ID_STATUS_DND:
-				flags += GC_SSE_ONLINE;
-				break;
-			default:
-				flags += GC_SSE_OFFLINE;
-			}
-			Chat_SetStatusEx(m_szModuleName, pUser.wszUsername, flags, wszUserId);
+			pm->wszRole = (pRole == nullptr) ? L"@everyone" : pRole->wszName;
 		}
+		pm->iStatus = ID_STATUS_OFFLINE;
+	}
+
+	// parse online statuses
+	const JSONNode &pStatuses = pRoot["presences"];
+	for (auto it = pStatuses.begin(); it != pStatuses.end(); ++it) {
+		const JSONNode &s = *it;
+		CDiscordGuildMember *gm = pGuild->FindUser(::getId(s["user"]["id"]));
+		if (gm != nullptr)
+			gm->iStatus = StrToStatus(s["status"].as_mstring());
+	}
+
+	// append users to all chat rooms
+	for (int i = 0; i < arUsers.getCount(); i++) {
+		CDiscordUser &pUser = arUsers[i];
+		if (pUser.guildId == pGuild->id)
+			ApplyUsersToChannel(pGuild, pUser);
 	}
 }
 
 void CDiscordProto::OnCommandGuildDeleted(const JSONNode &pRoot)
 {
-	SnowFlake guildId = ::getId(pRoot["id"]);
+	CDiscordGuild *pGuild = FindGuild(::getId(pRoot["id"]));
+	if (pGuild == nullptr)
+		return;
 
 	for (int i = arUsers.getCount()-1; i >= 0; i--) {
 		CDiscordUser &pUser = arUsers[i];
-		if (pUser.guildId == guildId) {
+		if (pUser.guildId == pGuild->id) {
 			Chat_Terminate(m_szModuleName, pUser.wszUsername, true);
 			arUsers.remove(i);
 		}
 	}
 
 	Chat_Terminate(m_szModuleName, pRoot["name"].as_mstring(), true);
+
+	arGuilds.remove(pGuild);
 }
 
+/////////////////////////////////////////////////////////////////////////////////////////
+// guild members
+
 void CDiscordProto::OnCommandGuildMemberAdded(const JSONNode &pRoot)
 {
 }
 
 void CDiscordProto::OnCommandGuildMemberRemoved(const JSONNode &pRoot)
 {
-	SnowFlake guildId = ::getId(pRoot["guild_id"]);
+	CDiscordGuild *pGuild = FindGuild(::getId(pRoot["guild_id"]));
+	if (pGuild == nullptr)
+		return;
+	
 	CMStringW wszUserId = pRoot["user"]["id"].as_mstring();
 
 	for (int i = 0; i < arUsers.getCount(); i++) {
 		CDiscordUser &pUser = arUsers[i];
-		if (pUser.guildId != guildId)
+		if (pUser.guildId != pGuild->id)
 			continue;
 
 		GCDEST gcd = { m_szModuleName, pUser.wszUsername, GC_EVENT_PART };
@@ -352,17 +265,25 @@ void CDiscordProto::OnCommandGuildMemberRemoved(const JSONNode &pRoot)
 
 void CDiscordProto::OnCommandGuildMemberUpdated(const JSONNode &pRoot)
 {
-	SnowFlake guildId = ::getId(pRoot["guild_id"]);
+	CDiscordGuild *pGuild = FindGuild(::getId(pRoot["guild_id"]));
+	if (pGuild == nullptr)
+		return;
+
 	CMStringW wszUserId = pRoot["user"]["id"].as_mstring();
-	CMStringW wszUserNick = pRoot["nick"].as_mstring(), wszOldNick;
-	if (wszUserNick.IsEmpty())
-		wszUserNick = pRoot["user"]["username"].as_mstring() + L"#" + pRoot["user"]["discriminator"].as_mstring();
+	CDiscordGuildMember *gm = pGuild->FindUser(_wtoi64(wszUserId));
+	if (gm == NULL)
+		return;
+
+	gm->wszNick = pRoot["nick"].as_mstring();
+	if (gm->wszNick.IsEmpty())
+		gm->wszNick = pRoot["user"]["username"].as_mstring() + L"#" + pRoot["user"]["discriminator"].as_mstring();
 
 	for (int i = 0; i < arUsers.getCount(); i++) {
 		CDiscordUser &pUser = arUsers[i];
-		if (pUser.guildId != guildId)
+		if (pUser.guildId != pGuild->id)
 			continue;
 
+		CMStringW wszOldNick;
 		SESSION_INFO *si = pci->SM_FindSession(pUser.wszUsername, m_szModuleName);
 		if (si != nullptr) {
 			USERINFO *ui = pci->UM_FindUser(si->pUsers, wszUserId);
@@ -375,7 +296,7 @@ void CDiscordProto::OnCommandGuildMemberUpdated(const JSONNode &pRoot)
 		gce.time = time(0);
 		gce.ptszUID = wszUserId;
 		gce.ptszNick = wszOldNick;
-		gce.ptszText = wszUserNick;
+		gce.ptszText = gm->wszNick;
 		Chat_Event(&gce);
 	}
 }
@@ -385,34 +306,40 @@ void CDiscordProto::OnCommandGuildMemberUpdated(const JSONNode &pRoot)
 
 void CDiscordProto::OnCommandRoleCreated(const JSONNode &pRoot)
 {
-	ProcessRole(::getId(pRoot["guild_id"]), pRoot["role"]);
+	CDiscordGuild *pGuild = FindGuild(::getId(pRoot["guild_id"]));
+	if (pGuild != nullptr)
+		ProcessRole(pGuild, pRoot["role"]);
 }
 
 void CDiscordProto::OnCommandRoleDeleted(const JSONNode &pRoot)
 {
-	SnowFlake id = ::getId(pRoot["role_id"]), guildId = ::getId(pRoot["guild_id"]);
-	CDiscordRole *pRole = arRoles.find((CDiscordRole*)&id);
+	CDiscordGuild *pGuild = FindGuild(::getId(pRoot["guild_id"]));
+	if (pGuild == nullptr)
+		return;
+
+	SnowFlake id = ::getId(pRoot["role_id"]);
+	CDiscordRole *pRole = pGuild->arRoles.find((CDiscordRole*)&id);
 	if (pRole == nullptr)
 		return;
 
 	int iOldPosition = pRole->position;
-	arRoles.remove(pRole);
+	pGuild->arRoles.remove(pRole);
 
-	for (int i = 0; i < arRoles.getCount(); i++) {
-		CDiscordRole &p = arRoles[i];
-		if (p.guildId == guildId && p.position > iOldPosition)
+	for (int i = 0; i < pGuild->arRoles.getCount(); i++) {
+		CDiscordRole &p = pGuild->arRoles[i];
+		if (p.position > iOldPosition)
 			p.position--;
 	}
 
 	for (int i = 0; i < arUsers.getCount(); i++) {
 		CDiscordUser &p = arUsers[i];
-		if (p.guildId != guildId)
+		if (p.guildId != pGuild->id)
 			continue;
 
 		SESSION_INFO *si = pci->SM_FindSession(p.wszUsername, m_szModuleName);
 		if (si != nullptr) {
 			pci->TM_RemoveAll(&si->pStatuses);
-			BuildStatusList(guildId, p.wszUsername);
+			BuildStatusList(pGuild, p.wszUsername);
 		}
 	}
 }
@@ -543,12 +470,9 @@ void CDiscordProto::OnCommandReady(const JSONNode &pRoot)
 
 	m_szGatewaySessionId = pRoot["session_id"].as_mstring();
 
-	const JSONNode &readState = pRoot["read_state"];
-	const JSONNode &pStatuses = pRoot["presences"];
-
 	const JSONNode &guilds = pRoot["guilds"];
 	for (auto it = guilds.begin(); it != guilds.end(); ++it)
-		ProcessGuild(readState, *it);
+		ProcessGuild(*it);
 
 	const JSONNode &relations = pRoot["relationships"];
 	for (auto it = relations.begin(); it != relations.end(); ++it) {
@@ -556,10 +480,15 @@ void CDiscordProto::OnCommandReady(const JSONNode &pRoot)
 
 		CDiscordUser *pUser = PrepareUser(p["user"]);
 		ProcessType(pUser, p);
+	}
 
-		int iStatus = sttGetPresence(pStatuses, p["user"]["id"].as_mstring());
-		if (iStatus)
-			setWord(pUser->hContact, "Status", iStatus);
+	const JSONNode &pStatuses = pRoot["presences"];
+	for (auto it = pStatuses.begin(); it != pStatuses.end(); ++it) {
+		const JSONNode &p = *it;
+
+		CDiscordUser *pUser = FindUser(::getId(p["user"]["id"]));
+		if (pUser != nullptr)
+			setWord(pUser->hContact, "Status", StrToStatus(p["status"].as_mstring()));
 	}
 
 	const JSONNode &channels = pRoot["private_channels"];
@@ -577,7 +506,6 @@ void CDiscordProto::OnCommandReady(const JSONNode &pRoot)
 		CMStringW wszChannelId = p["id"].as_mstring();
 		pUser->channelId = _wtoi64(wszChannelId);
 		pUser->lastMsg = CDiscordMessage(::getId(p["last_message_id"]));
-		pUser->lastReadId = sttGetLastRead(readState, wszChannelId);
 		pUser->bIsPrivate = true;
 
 		setId(pUser->hContact, DB_KEY_CHANNELID, pUser->channelId);
@@ -586,6 +514,14 @@ void CDiscordProto::OnCommandReady(const JSONNode &pRoot)
 		if (pUser->lastMsg.id > oldMsgId)
 			RetrieveHistory(pUser->hContact, MSG_AFTER, oldMsgId, 99);
 	}
+
+	const JSONNode &readState = pRoot["read_state"];
+	for (auto it = readState.begin(); it != readState.end(); ++it) {
+		const JSONNode &p = *it;
+		CDiscordUser *pUser = FindUserByChannel(::getId(p["id"]));
+		if (pUser != nullptr)
+			pUser->lastReadId = ::getId(p["last_message_id"]);
+	}
 }
 
 /////////////////////////////////////////////////////////////////////////////////////////
diff --git a/protocols/Discord/src/groupchat.cpp b/protocols/Discord/src/groupchat.cpp
index 6641a4fd88..ac9cf235d5 100644
--- a/protocols/Discord/src/groupchat.cpp
+++ b/protocols/Discord/src/groupchat.cpp
@@ -25,15 +25,12 @@ enum {
 
 /////////////////////////////////////////////////////////////////////////////////////////
 
-void CDiscordProto::BuildStatusList(SnowFlake guildId, const CMStringW &wszChannelId)
+void CDiscordProto::BuildStatusList(const CDiscordGuild *pGuild, const CMStringW &wszChannelId)
 {
 	Chat_AddGroup(m_szModuleName, wszChannelId, L"@owner");
 
-	for (int i = 0; i < arRoles.getCount(); i++) {
-		CDiscordRole &r = arRoles[i];
-		if (r.guildId == guildId)
-			Chat_AddGroup(m_szModuleName, wszChannelId, r.wszName);
-	}
+	for (int i = 0; i < pGuild->arRoles.getCount(); i++)
+		Chat_AddGroup(m_szModuleName, wszChannelId, pGuild->arRoles[i].wszName);
 }
 
 /////////////////////////////////////////////////////////////////////////////////////////
diff --git a/protocols/Discord/src/guilds.cpp b/protocols/Discord/src/guilds.cpp
new file mode 100644
index 0000000000..f14caadf06
--- /dev/null
+++ b/protocols/Discord/src/guilds.cpp
@@ -0,0 +1,177 @@
+/*
+Copyright � 2016-17 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 <http://www.gnu.org/licenses/>.
+*/
+
+#include "stdafx.h"
+
+int compareUsers(const CDiscordUser *p1, const CDiscordUser *p2);
+
+static int compareRoles(const CDiscordRole *p1, const CDiscordRole *p2)
+{
+	return p1->id - p2->id;
+}
+
+static int compareChatUsers(const CDiscordGuildMember *p1, const CDiscordGuildMember *p2)
+{
+	return p1->userId - p2->userId;
+}
+
+CDiscordGuild::CDiscordGuild(SnowFlake _id)
+	: id(_id),
+	arChatUsers(30, compareChatUsers),
+	arRoles(10, compareRoles)
+{
+}
+
+CDiscordGuild::~CDiscordGuild()
+{
+}
+
+/////////////////////////////////////////////////////////////////////////////////////////
+// reads a role from json
+
+void CDiscordProto::ProcessRole(CDiscordGuild *guild, const JSONNode &role)
+{
+	SnowFlake id = ::getId(role["id"]);
+	CDiscordRole *p = guild->arRoles.find((CDiscordRole*)&id);
+	if (p == nullptr) {
+		p = new CDiscordRole();
+		p->id = id;
+		guild->arRoles.insert(p);
+	}
+
+	p->color = role["color"].as_int();
+	p->position = role["position"].as_int();
+	p->permissions = role["permissions"].as_int();
+	p->wszName = role["name"].as_mstring();
+}
+
+/////////////////////////////////////////////////////////////////////////////////////////
+
+void CDiscordProto::ProcessGuild(const JSONNode &p)
+{
+	SnowFlake guildId = ::getId(p["id"]);
+	GatewaySendGuildInfo(guildId);
+
+	CDiscordGuild *pGuild = FindGuild(guildId);
+	if (pGuild == nullptr) {
+		pGuild = new CDiscordGuild(guildId);
+		arGuilds.insert(pGuild);
+	}
+	pGuild->ownerId = ::getId(p["owner_id"]);
+	pGuild->wszName = p["name"].as_mstring();
+
+	GCSessionInfoBase *si = Chat_NewSession(GCW_SERVER, m_szModuleName, pGuild->wszName, pGuild->wszName, pGuild);
+	Chat_Control(m_szModuleName, pGuild->wszName, WINDOW_HIDDEN);
+	Chat_Control(m_szModuleName, pGuild->wszName, SESSION_ONLINE);
+	
+	pGuild->hContact = si->hContact;
+	setId(si->hContact, DB_KEY_CHANNELID, guildId);
+
+	const JSONNode &roles = p["roles"];
+	for (auto itr = roles.begin(); itr != roles.end(); ++itr)
+		ProcessRole(pGuild, *itr);
+
+	const JSONNode &channels = p["channels"];
+	for (auto itc = channels.begin(); itc != channels.end(); ++itc)
+		ProcessGuildChannel(pGuild, *itc);
+}
+
+/////////////////////////////////////////////////////////////////////////////////////////
+
+CDiscordUser* CDiscordProto::ProcessGuildChannel(CDiscordGuild *pGuild, const JSONNode &pch)
+{
+	// filter our all channels but the text ones
+	if (pch["type"].as_int() != 0)
+		return NULL;
+
+	CMStringW wszChannelName = pGuild->wszName + L"#" + pch["name"].as_mstring();
+	CMStringW wszChannelId = pch["id"].as_mstring();
+	CMStringW wszTopic = pch["topic"].as_mstring();
+	SnowFlake channelId = _wtoi64(wszChannelId);
+
+	GCSessionInfoBase *si = Chat_NewSession(GCW_CHATROOM, m_szModuleName, wszChannelId, wszChannelName);
+	BuildStatusList(pGuild, wszChannelId);
+
+	Chat_Control(m_szModuleName, wszChannelId, WINDOW_HIDDEN);
+	Chat_Control(m_szModuleName, wszChannelId, SESSION_ONLINE);
+
+	if (!wszTopic.IsEmpty()) {
+		Chat_SetStatusbarText(m_szModuleName, wszChannelId, wszTopic);
+
+		GCDEST gcd = { m_szModuleName, wszChannelId, GC_EVENT_TOPIC };
+		GCEVENT gce = { &gcd };
+		gce.time = time(0);
+		gce.ptszText = wszTopic;
+		Chat_Event(&gce);
+	}
+
+	CDiscordUser *pUser = FindUserByChannel(channelId);
+	if (pUser == NULL) {
+		// missing channel - create it
+		pUser = new CDiscordUser(channelId);
+		pUser->bIsPrivate = false;
+		pUser->hContact = si->hContact;
+		pUser->id = channelId;
+		pUser->channelId = channelId;
+		arUsers.insert(pUser);
+	}
+	pUser->wszUsername = wszChannelId;
+	pUser->guildId = pGuild->id;
+	pUser->lastMsg = CDiscordMessage(::getId(pch["last_message_id"]));
+
+	setId(pUser->hContact, DB_KEY_ID, channelId);
+	setId(pUser->hContact, DB_KEY_CHANNELID, channelId);
+
+	SnowFlake oldMsgId = getId(pUser->hContact, DB_KEY_LASTMSGID);
+	if (oldMsgId != 0 && pUser->lastMsg.id > oldMsgId)
+		RetrieveHistory(pUser->hContact, MSG_AFTER, oldMsgId, 99);
+
+	return pUser;
+}
+
+/////////////////////////////////////////////////////////////////////////////////////////
+
+void CDiscordProto::ApplyUsersToChannel(CDiscordGuild *pGuild, const CDiscordUser &pUser)
+{
+	GCDEST gcd = { m_szModuleName, pUser.wszUsername, GC_EVENT_JOIN };
+	GCEVENT gce = { &gcd };
+	gce.time = time(0);
+	gce.dwFlags = GCEF_SILENT;
+
+	for (int i = 0; i < pGuild->arChatUsers.getCount(); i++) {
+		CDiscordGuildMember &m = pGuild->arChatUsers[i];
+
+		wchar_t wszUserId[100];
+		_i64tow_s(m.userId, wszUserId, _countof(wszUserId), 10);
+
+		gce.ptszStatus = m.wszRole;
+		gce.bIsMe = (m.userId == m_ownId);
+		gce.ptszUID = wszUserId;
+		gce.ptszNick = m.wszNick;
+		Chat_Event(&gce);
+
+		int flags = GC_SSE_ONLYLISTED;
+		switch (m.iStatus) {
+		case ID_STATUS_ONLINE: case ID_STATUS_NA: case ID_STATUS_DND:
+			flags += GC_SSE_ONLINE;
+			break;
+		default:
+			flags += GC_SSE_OFFLINE;
+		}
+		Chat_SetStatusEx(m_szModuleName, pUser.wszUsername, flags, wszUserId);
+	}
+}
diff --git a/protocols/Discord/src/menus.cpp b/protocols/Discord/src/menus.cpp
index 85bdd5c670..78f78902a3 100644
--- a/protocols/Discord/src/menus.cpp
+++ b/protocols/Discord/src/menus.cpp
@@ -17,13 +17,24 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 #include "stdafx.h"
 
+INT_PTR CDiscordProto::OnMenuCreateChannel(WPARAM hContact, LPARAM)
+{
+	ENTER_STRING es = { sizeof(es), ESF_RICHEDIT, m_szModuleName, "Discord", TranslateT("Enter channel name") };
+	if (EnterString(&es)) {
+		JSONNode roles(JSON_ARRAY); roles.set_name("permission_overwrites");
+		JSONNode root; root << INT_PARAM("type", 0) << WCHAR_PARAM("name", es.ptszResult) << roles;
+		CMStringA szUrl(FORMAT, "/guilds/%lld/channels", getId(hContact, DB_KEY_CHANNELID));
+		Push(new AsyncHttpRequest(this, REQUEST_POST, szUrl, NULL, &root));
+		mir_free(es.ptszResult);
+	}
+	return 0;
+}
+
+/////////////////////////////////////////////////////////////////////////////////////////
+
 INT_PTR CDiscordProto::OnMenuJoinGuild(WPARAM, LPARAM)
 {
-	ENTER_STRING es = { sizeof(es) };
-	es.szModuleName = m_szModuleName;
-	es.szDataPrefix = "Discord";
-	es.type = ESF_MULTILINE;
-	es.caption = TranslateT("Enter invitation code you received");
+	ENTER_STRING es = { sizeof(es), ESF_RICHEDIT, m_szModuleName, "Discord", TranslateT("Enter invitation code you received") };
 	if (EnterString(&es)) {
 		CMStringA szUrl(FORMAT, "/invite/%S", es.ptszResult);
 		Push(new AsyncHttpRequest(this, REQUEST_POST, szUrl, NULL));
@@ -48,7 +59,9 @@ INT_PTR CDiscordProto::OnMenuLeaveGuild(WPARAM hContact, LPARAM)
 int CDiscordProto::OnMenuPrebuild(WPARAM hContact, LPARAM)
 {
 	// "Leave guild" menu item should be visible only for the guild contacts
-	Menu_ShowItem(m_hMenuLeaveGuild, getByte(hContact, "ChatRoom") == 2);
+	bool bIsGuild = getByte(hContact, "ChatRoom") == 2;
+	Menu_ShowItem(m_hMenuLeaveGuild, bIsGuild);
+	Menu_ShowItem(m_hMenuCreateChannel, bIsGuild);
 	return 0;
 }
 
@@ -68,6 +81,7 @@ void CDiscordProto::InitMenus()
 	mi.hIcolibItem = g_iconList[1].hIcolib;
 	Menu_AddProtoMenuItem(&mi, m_szModuleName);
 
+	// Contact menu items
 	CMenuItem mi2;
 	mi2.pszService = "/LeaveGuild";
 	CreateProtoService(mi2.pszService, &CDiscordProto::OnMenuLeaveGuild);
@@ -77,5 +91,13 @@ void CDiscordProto::InitMenus()
 	SET_UID(mi2, 0x6EF11AD6, 0x6111, 0x4E29, 0xBA, 0x8B, 0xA7, 0xB2, 0xE0, 0x22, 0xE1, 0x8C);
 	m_hMenuLeaveGuild = Menu_AddContactMenuItem(&mi2, m_szModuleName);
 
+	mi2.pszService = "/CreateChannel";
+	CreateProtoService(mi2.pszService, &CDiscordProto::OnMenuCreateChannel);
+	mi2.name.a = LPGEN("Create new channel");
+	mi2.position = -200001001;
+	mi2.hIcolibItem = Skin_GetIconHandle(SKINICON_OTHER_ADDCONTACT);
+	SET_UID(mi2, 0x6EF11AD6, 0x6111, 0x4E29, 0xBA, 0x8B, 0xA7, 0xB2, 0xE0, 0x22, 0xE1, 0x8D);
+	m_hMenuCreateChannel = Menu_AddContactMenuItem(&mi2, m_szModuleName);
+
 	HookProtoEvent(ME_CLIST_PREBUILDCONTACTMENU, &CDiscordProto::OnMenuPrebuild);
 }
diff --git a/protocols/Discord/src/proto.cpp b/protocols/Discord/src/proto.cpp
index 80331edbda..34d3ae8840 100644
--- a/protocols/Discord/src/proto.cpp
+++ b/protocols/Discord/src/proto.cpp
@@ -27,12 +27,12 @@ static int compareRequests(const AsyncHttpRequest *p1, const AsyncHttpRequest *p
 	return p1->m_iReqNum - p2->m_iReqNum;
 }
 
-static int compareRoles(const CDiscordRole *p1, const CDiscordRole *p2)
+int compareUsers(const CDiscordUser *p1, const CDiscordUser *p2)
 {
 	return p1->id - p2->id;
 }
 
-static int compareUsers(const CDiscordUser *p1, const CDiscordUser *p2)
+static int compareGuilds(const CDiscordGuild *p1, const CDiscordGuild *p2)
 {
 	return p1->id - p2->id;
 }
@@ -43,10 +43,10 @@ CDiscordProto::CDiscordProto(const char *proto_name, const wchar_t *username) :
 	m_evRequestsQueue(CreateEvent(NULL, FALSE, FALSE, NULL)),
 	m_wszDefaultGroup(this, DB_KEY_GROUP, DB_KEYVAL_GROUP),
 	m_wszEmail(this, DB_KEY_EMAIL, L""),
+	arGuilds(1, compareGuilds),
 	arMarkReadQueue(1, compareUsers),
 	arOwnMessages(1, compareMessages),
-	arRoles(10, compareRoles),
-	arUsers(50, compareUsers)
+	arUsers(10, compareUsers)
 {
 	// Services
 	CreateProtoService(PS_GETSTATUS, &CDiscordProto::GetStatus);
diff --git a/protocols/Discord/src/proto.h b/protocols/Discord/src/proto.h
index 9ab553e5a4..376f536419 100644
--- a/protocols/Discord/src/proto.h
+++ b/protocols/Discord/src/proto.h
@@ -83,7 +83,7 @@ JSONNode& operator<<(JSONNode &json, const WCHAR_PARAM &param);
 
 struct CDiscordRole : public MZeroedObject
 {
-	SnowFlake id, guildId;
+	SnowFlake id;
 	COLORREF color;
 	DWORD permissions;
 	int position;
@@ -129,6 +129,41 @@ struct CDiscordUser : public MZeroedObject
 	int       iDiscriminator;
 };
 
+/////////////////////////////////////////////////////////////////////////////////////////
+
+struct CDiscordGuildMember : public MZeroedObject
+{
+	CDiscordGuildMember(SnowFlake id) :
+		userId(id)
+	{}
+
+	~CDiscordGuildMember()
+	{}
+
+	SnowFlake userId;
+	CMStringW wszNick, wszRole;
+	int iStatus;
+};
+
+struct CDiscordGuild : public MZeroedObject
+{
+	CDiscordGuild(SnowFlake _id);
+	~CDiscordGuild();
+
+	__forceinline CDiscordGuildMember* FindUser(SnowFlake userId)
+	{	return arChatUsers.find((CDiscordGuildMember*)&userId);
+	}
+
+	SnowFlake id, ownerId;
+	CMStringW wszName;
+	MCONTACT hContact;
+
+	OBJLIST<CDiscordGuildMember> arChatUsers;
+	OBJLIST<CDiscordRole> arRoles; // guild roles
+};
+
+/////////////////////////////////////////////////////////////////////////////////////////
+
 class CDiscordProto : public PROTO<CDiscordProto>
 {
 	friend struct AsyncHttpRequest;
@@ -204,7 +239,6 @@ class CDiscordProto : public PROTO<CDiscordProto>
 	mir_cs csMarkReadQueue;
 	LIST<CDiscordUser> arMarkReadQueue;
 
-	OBJLIST<CDiscordRole> arRoles;
 	OBJLIST<CDiscordUser> arUsers;
 	OBJLIST<SnowFlake> arOwnMessages;
 	CDiscordUser* FindUser(SnowFlake id);
@@ -219,10 +253,26 @@ class CDiscordProto : public PROTO<CDiscordProto>
 
 	int __cdecl OnMenuPrebuild(WPARAM, LPARAM);
 
+	INT_PTR __cdecl OnMenuCreateChannel(WPARAM, LPARAM);
 	INT_PTR __cdecl OnMenuJoinGuild(WPARAM, LPARAM);
 	INT_PTR __cdecl OnMenuLeaveGuild(WPARAM, LPARAM);
 
-	HGENMENU m_hMenuLeaveGuild;
+	HGENMENU m_hMenuLeaveGuild, m_hMenuCreateChannel;
+
+	//////////////////////////////////////////////////////////////////////////////////////
+	// guilds
+
+	OBJLIST<CDiscordGuild> arGuilds;
+
+	__forceinline CDiscordGuild* FindGuild(SnowFlake id) const
+	{	return arGuilds.find((CDiscordGuild*)&id);
+	}
+
+	void ProcessGuild(const JSONNode&);
+	void ApplyUsersToChannel(CDiscordGuild *guild, const CDiscordUser&);
+	CDiscordUser* ProcessGuildChannel(CDiscordGuild *guild, const JSONNode&);
+	void ProcessRole(CDiscordGuild *guild, const JSONNode&);
+	void ProcessType(CDiscordUser *pUser, const JSONNode&);
 
 	//////////////////////////////////////////////////////////////////////////////////////
 	// group chats
@@ -233,7 +283,7 @@ class CDiscordProto : public PROTO<CDiscordProto>
 	void Chat_SendPrivateMessage(GCHOOK *gch);
 	void Chat_ProcessLogMenu(GCHOOK *gch);
 
-	void BuildStatusList(SnowFlake guildId, const CMStringW &wszChannelId);
+	void BuildStatusList(const CDiscordGuild *pGuild, const CMStringW &wszChannelId);
 	void ParseSpecialChars(SESSION_INFO *si, CMStringW &str);
 
 	//////////////////////////////////////////////////////////////////////////////////////
@@ -333,9 +383,6 @@ public:
 	void OnReceiveAvatar(NETLIBHTTPREQUEST*, AsyncHttpRequest*);
 
 	// Misc
-	void ProcessGuild(const JSONNode &pStatuses, const JSONNode &pRoot);
-	void ProcessRole(SnowFlake guildId, const JSONNode&);
-	void ProcessType(CDiscordUser *pUser, const JSONNode&);
 	void SetServerStatus(int iStatus);
 	void RemoveFriend(SnowFlake id);
 
diff --git a/protocols/Discord/src/stdafx.h b/protocols/Discord/src/stdafx.h
index 6a233e59b4..7e6e032d3b 100644
--- a/protocols/Discord/src/stdafx.h
+++ b/protocols/Discord/src/stdafx.h
@@ -57,7 +57,6 @@ extern HWND g_hwndHeartbeat;
 #define DB_KEY_NICK      "Nick"
 #define DB_KEY_AVHASH    "AvatarHash"
 #define DB_KEY_CHANNELID "ChannelID"
-#define DB_KEY_OWNERID   "OwnerID"
 #define DB_KEY_LASTMSGID "LastMessageID"
 #define DB_KEY_REQAUTH   "ReqAuth"
 
diff --git a/protocols/Discord/src/version.h b/protocols/Discord/src/version.h
index ebddf302fd..e8f77f39b4 100644
--- a/protocols/Discord/src/version.h
+++ b/protocols/Discord/src/version.h
@@ -1,7 +1,7 @@
 #define __MAJOR_VERSION            0
-#define __MINOR_VERSION            4
+#define __MINOR_VERSION            5
 #define __RELEASE_NUM              0
-#define __BUILD_NUM                3
+#define __BUILD_NUM                1
 
 #include <stdver.h>
 
-- 
cgit v1.2.3