summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGeorge Hazan <george.hazan@gmail.com>2024-07-20 15:58:03 +0300
committerGeorge Hazan <george.hazan@gmail.com>2024-07-20 15:58:03 +0300
commitd759d08e5b180f5a872ffde53dbb764d52b73724 (patch)
treebac3227dca252f6c03943a2ea970b70b224d12a0
parentbc4d3612abc05f7b637361fbd5f23586c31d1ce5 (diff)
fixes #4548 (SkypeWeb: организовать запись истории групчатов в БД)
-rw-r--r--protocols/SkypeWeb/src/skype_chatrooms.cpp36
-rw-r--r--protocols/SkypeWeb/src/skype_db.cpp74
-rw-r--r--protocols/SkypeWeb/src/skype_history_sync.cpp100
-rw-r--r--protocols/SkypeWeb/src/skype_messages.cpp73
-rw-r--r--protocols/SkypeWeb/src/skype_polling.cpp15
-rw-r--r--protocols/SkypeWeb/src/skype_proto.h8
6 files changed, 101 insertions, 205 deletions
diff --git a/protocols/SkypeWeb/src/skype_chatrooms.cpp b/protocols/SkypeWeb/src/skype_chatrooms.cpp
index ddd627f46d..a5e6652c32 100644
--- a/protocols/SkypeWeb/src/skype_chatrooms.cpp
+++ b/protocols/SkypeWeb/src/skype_chatrooms.cpp
@@ -20,6 +20,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
void CSkypeProto::InitGroupChatModule()
{
GCREGISTER gcr = {};
+ gcr.dwFlags = GC_DATABASE | GC_PERSISTENT;
gcr.iMaxText = 0;
gcr.ptszDispName = m_tszUserName;
gcr.pszModule = m_szModuleName;
@@ -248,10 +249,6 @@ void CSkypeProto::OnChatEvent(const JSONNode &node)
CMStringW wszTopic(node["threadtopic"].as_mstring());
CMStringW wszContent(node["content"].as_mstring());
- time_t timestamp = IsoToUnixTime(node["composetime"].as_string());
-
- int nEmoteOffset = node["skypeemoteoffset"].as_int();
-
SESSION_INFO *si = Chat_Find(wszChatId, m_szModuleName);
if (si == nullptr) {
si = StartChatRoom(wszChatId, wszTopic);
@@ -262,11 +259,7 @@ void CSkypeProto::OnChatEvent(const JSONNode &node)
}
std::string messageType = node["messagetype"].as_string();
- if (messageType == "Text" || messageType == "RichText") {
- CMStringW wszClearedContent(messageType == "RichText" ? RemoveHtml(wszContent) : wszContent);
- AddMessageToChat(si, szFromId, wszClearedContent, nEmoteOffset != NULL, nEmoteOffset, timestamp);
- }
- else if (messageType == "ThreadActivity/AddMember") {
+ if (messageType == "ThreadActivity/AddMember") {
// <addmember><eventtime>1429186229164</eventtime><initiator>8:initiator</initiator><target>8:user</target></addmember>
TiXmlDocument doc;
if (0 != doc.Parse(T2Utf(wszContent)))
@@ -355,31 +348,6 @@ void CSkypeProto::SendChatMessage(SESSION_INFO *si, const wchar_t *tszMessage)
PushRequest(new SendChatMessageRequest(chat_id, time(0), szMessage));
}
-void CSkypeProto::AddMessageToChat(SESSION_INFO *si, const wchar_t *from, const wchar_t *content, bool isAction, int emoteOffset, time_t timestamp, bool isLoading)
-{
- ptrW tnick(GetChatContactNick(si->hContact, from));
-
- GCEVENT gce = { si, isAction ? GC_EVENT_ACTION : GC_EVENT_MESSAGE };
- gce.bIsMe = IsMe(from);
- gce.pszNick.w = tnick;
- gce.time = timestamp;
- gce.pszUID.w = from;
-
- CMStringW wszText(content);
- wszText.Replace(L"%", L"%%");
-
- if (!isAction) {
- gce.pszText.w = wszText;
- gce.dwFlags |= GCEF_ADDTOLOG;
- }
- else gce.pszText.w = wszText.c_str() + emoteOffset;
-
- if (isLoading)
- gce.dwFlags |= GCEF_NOTNOTIFY;
-
- Chat_Event(&gce);
-}
-
void CSkypeProto::OnGetChatInfo(MHttpResponse *response, AsyncHttpRequest*)
{
JsonReply reply(response);
diff --git a/protocols/SkypeWeb/src/skype_db.cpp b/protocols/SkypeWeb/src/skype_db.cpp
index 0ea0be0fb2..2786e7727b 100644
--- a/protocols/SkypeWeb/src/skype_db.cpp
+++ b/protocols/SkypeWeb/src/skype_db.cpp
@@ -17,7 +17,12 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
#include "stdafx.h"
-struct { int type; char *name; uint32_t flags; } g_SkypeDBTypes[] =
+struct {
+ int type;
+ char *name;
+ int flags;
+}
+static g_SkypeDBTypes[] =
{
{ SKYPE_DB_EVENT_TYPE_INCOMING_CALL, LPGEN("Incoming call"), DETF_NONOTIFY },
{ SKYPE_DB_EVENT_TYPE_EDITED_MESSAGE, LPGEN("Edited message"), 0 },
@@ -30,73 +35,6 @@ struct { int type; char *name; uint32_t flags; } g_SkypeDBTypes[] =
{ SKYPE_DB_EVENT_TYPE_UNKNOWN, LPGEN("Unknown event"), 0 },
};
-MEVENT CSkypeProto::GetMessageFromDb(const char *messageId)
-{
- if (messageId == nullptr)
- return NULL;
-
- return db_event_getById(m_szModuleName, messageId);
-}
-
-MEVENT CSkypeProto::AddDbEvent(uint16_t type, MCONTACT hContact, uint32_t timestamp, uint32_t flags, const CMStringW &content, const CMStringA &msgId)
-{
- if (MEVENT hDbEvent = GetMessageFromDb(msgId))
- return hDbEvent;
-
- T2Utf szMsg(content);
- DBEVENTINFO dbei = {};
- dbei.szModule = m_szModuleName;
- dbei.timestamp = timestamp;
- dbei.eventType = type;
- dbei.cbBlob = (uint32_t)mir_strlen(szMsg) + 1;
- dbei.pBlob = szMsg;
- dbei.flags = flags;
- dbei.szId = msgId;
- return db_event_add(hContact, &dbei);
-}
-
-void CSkypeProto::EditEvent(MEVENT hEvent, const CMStringW &szContent, time_t edit_time)
-{
- mir_cslock lck(m_AppendMessageLock);
-
- DB::EventInfo dbei(hEvent);
- if (!dbei)
- return;
-
- JSONNode jMsg = JSONNode::parse((char*)dbei.pBlob);
- if (jMsg) {
- JSONNode &jEdits = jMsg["edits"];
- if (jEdits) {
- for (auto &it : jEdits)
- if (it["time"].as_int() == edit_time)
- return;
-
- JSONNode jEdit;
- jEdit << INT_PARAM("time", (long)edit_time) << WCHAR_PARAM("text", szContent);
- jEdits << jEdit;
- }
- }
- else {
- JSONNode jOriginalMsg; jOriginalMsg.set_name("original_message");
- jOriginalMsg << INT_PARAM("time", (long)dbei.timestamp) << CHAR_PARAM("text", (char *)dbei.pBlob);
-
- jMsg = JSONNode();
- jMsg << jOriginalMsg;
-
- JSONNode jEdit;
- jEdit << INT_PARAM("time", (long)edit_time) << WCHAR_PARAM("text", szContent);
-
- JSONNode jEdits(JSON_ARRAY); jEdits.set_name("edits");
- jEdits << jEdit;
- jMsg << jEdits;
- }
-
- std::string newMsg = jMsg.write().c_str();
- dbei.cbBlob = int(newMsg.size() + 1);
- dbei.pBlob = (char *)newMsg.c_str();
- db_event_edit(hEvent, &dbei, true);
-}
-
void CSkypeProto::InitDBEvents()
{
// custom event
diff --git a/protocols/SkypeWeb/src/skype_history_sync.cpp b/protocols/SkypeWeb/src/skype_history_sync.cpp
index 7c5092c0b3..3984845d7c 100644
--- a/protocols/SkypeWeb/src/skype_history_sync.cpp
+++ b/protocols/SkypeWeb/src/skype_history_sync.cpp
@@ -27,7 +27,6 @@ void CSkypeProto::OnGetServerHistory(MHttpResponse *response, AsyncHttpRequest *
auto &root = reply.data();
const JSONNode &metadata = root["_metadata"];
- const JSONNode &conversations = root["messages"].as_array();
int totalCount = metadata["totalCount"].as_int();
std::string syncState = metadata["syncState"].as_string();
@@ -37,17 +36,16 @@ void CSkypeProto::OnGetServerHistory(MHttpResponse *response, AsyncHttpRequest *
uint32_t lastMsgTime = 0;
time_t iLocalTime = time(0);
- for (int i = (int)conversations.size(); i >= 0; i--) {
- const JSONNode &message = conversations.at(i);
-
+ auto &conv = root["messages"];
+ for (auto it = conv.rbegin(); it != conv.rend(); ++it) {
+ auto &message = *it;
CMStringA szMessageId = message["clientmessageid"] ? message["clientmessageid"].as_string().c_str() : message["skypeeditedid"].as_string().c_str();
- int userType;
- CMStringW wszChatId = UrlToSkypeId(message["conversationLink"].as_mstring(), &userType);
- CMStringW wszContent = message["content"].as_mstring();
- CMStringW wszFrom = UrlToSkypeId(message["from"].as_mstring());
+ int iUserType;
+ CMStringA szChatId = UrlToSkypeId(message["conversationLink"].as_mstring(), &iUserType);
+ CMStringA szFrom = UrlToSkypeId(message["from"].as_mstring());
- MCONTACT hContact = FindContact(wszChatId);
+ MCONTACT hContact = FindContact(szChatId);
std::string messageType = message["messagetype"].as_string();
int emoteOffset = message["skypeemoteoffset"].as_int();
@@ -55,7 +53,12 @@ void CSkypeProto::OnGetServerHistory(MHttpResponse *response, AsyncHttpRequest *
if (timestamp > getDword(hContact, "LastMsgTime", 0))
setDword(hContact, "LastMsgTime", timestamp);
- bool isEdited = message["skypeeditedid"];
+ CMStringW wszContent = message["content"].as_mstring();
+ T2Utf szMsg(wszContent);
+ if (messageType == "RichText/Contacts") {
+ ProcessContactRecv(hContact, timestamp, szMsg, szMessageId);
+ return;
+ }
uint32_t id = message["id"].as_int();
if (id > lastMsgTime)
@@ -64,53 +67,50 @@ void CSkypeProto::OnGetServerHistory(MHttpResponse *response, AsyncHttpRequest *
if (bUseLocalTime)
timestamp = iLocalTime;
- if (userType == 8 || userType == 2) {
- uint32_t iFlags = DBEF_UTF;
+ DB::EventInfo dbei(db_event_getById(m_szModuleName, szMessageId));
- if (!markAllAsUnread)
- iFlags |= DBEF_READ;
+ dbei.flags = DBEF_UTF;
+ if (!markAllAsUnread)
+ dbei.flags |= DBEF_READ;
+ if (IsMe(szFrom))
+ dbei.flags |= DBEF_SENT;
- if (IsMe(wszFrom))
- iFlags |= DBEF_SENT;
-
- if (messageType == "Text" || messageType == "RichText") {
- CMStringW szMessage(messageType == "RichText" ? RemoveHtml(wszContent) : wszContent);
- MEVENT dbevent = GetMessageFromDb(szMessageId);
- if (isEdited && dbevent != NULL)
- EditEvent(dbevent, szMessage, timestamp);
- else
- AddDbEvent(emoteOffset == 0 ? EVENTTYPE_MESSAGE : SKYPE_DB_EVENT_TYPE_ACTION, hContact, timestamp, iFlags, szMessage.c_str()+emoteOffset, szMessageId);
- }
- else if (messageType == "Event/Call") {
- AddDbEvent(SKYPE_DB_EVENT_TYPE_CALL_INFO, hContact, timestamp, iFlags, wszContent, szMessageId);
- }
- else if (messageType == "RichText/Files") {
- AddDbEvent(SKYPE_DB_EVENT_TYPE_FILETRANSFER_INFO, hContact, timestamp, iFlags, wszContent, szMessageId);
- }
- else if (messageType == "RichText/UriObject") {
- AddDbEvent(SKYPE_DB_EVENT_TYPE_URIOBJ, hContact, timestamp, iFlags, wszContent, szMessageId);
- }
- else if (messageType == "RichText/Contacts") {
- ProcessContactRecv(hContact, timestamp, T2Utf(wszContent), szMessageId);
- }
- else if (messageType == "RichText/Media_Album") {
- // do nothing
- }
- else {
- AddDbEvent(SKYPE_DB_EVENT_TYPE_UNKNOWN, hContact, timestamp, iFlags, wszContent, szMessageId);
- }
+ if (messageType == "Text" || messageType == "RichText") {
+ CMStringW szMessage(messageType == "RichText" ? RemoveHtml(wszContent) : wszContent);
+ dbei.eventType = (emoteOffset == 0) ? EVENTTYPE_MESSAGE : SKYPE_DB_EVENT_TYPE_ACTION;
+ }
+ else if (messageType == "Event/Call") {
+ dbei.eventType = SKYPE_DB_EVENT_TYPE_CALL_INFO;
+ }
+ else if (messageType == "RichText/Files") {
+ dbei.eventType = SKYPE_DB_EVENT_TYPE_FILETRANSFER_INFO;
+ }
+ else if (messageType == "RichText/UriObject") {
+ dbei.eventType = SKYPE_DB_EVENT_TYPE_URIOBJ;
+ }
+ else if (messageType == "RichText/Media_Album") {
+ // do nothing
+ }
+ else {
+ dbei.eventType = SKYPE_DB_EVENT_TYPE_UNKNOWN;
}
- else if (userType == 19) {
- auto *si = Chat_Find(wszChatId, m_szModuleName);
- if (si == nullptr)
- return;
- if (messageType == "Text" || messageType == "RichText")
- AddMessageToChat(si, wszFrom, messageType == "RichText" ? RemoveHtml(wszContent) : wszContent, emoteOffset != NULL, emoteOffset, timestamp, true);
+ dbei.szModule = m_szModuleName;
+ dbei.timestamp = timestamp;
+ dbei.cbBlob = (uint32_t)mir_strlen(szMsg);
+ dbei.pBlob = szMsg;
+ dbei.szId = szMessageId;
+ if (iUserType == 19)
+ dbei.szUserId = szFrom;
+
+ if (dbei) {
+ db_event_edit(dbei.getEvent(), &dbei, true);
+ dbei.pBlob = nullptr;
}
+ else db_event_add(hContact, &dbei);
}
- if (totalCount >= 99 || conversations.size() >= 99) {
+ if (totalCount >= 99 || conv.size() >= 99) {
CMStringA szUrl(pRequest->m_szUrl);
int i1 = szUrl.Find("startTime=");
int i2 = szUrl.Find("&", i1);
diff --git a/protocols/SkypeWeb/src/skype_messages.cpp b/protocols/SkypeWeb/src/skype_messages.cpp
index 17fc4ebc70..4c57fa2322 100644
--- a/protocols/SkypeWeb/src/skype_messages.cpp
+++ b/protocols/SkypeWeb/src/skype_messages.cpp
@@ -87,19 +87,21 @@ int CSkypeProto::OnPreCreateMessage(WPARAM, LPARAM lParam)
/* MESSAGE EVENT */
-void CSkypeProto::OnPrivateMessageEvent(const JSONNode &node)
+void CSkypeProto::ProcessNewMessage(const JSONNode &node)
{
- CMStringA szMessageId = node["clientmessageid"] ? node["clientmessageid"].as_string().c_str() : node["skypeeditedid"].as_string().c_str();
+ int iUserType;
+ UrlToSkypeId(node["conversationLink"].as_string().c_str(), &iUserType);
+
+ CMStringA szMessageId = node["clientmessageid"] ? node["clientmessageid"].as_mstring() : node["skypeeditedid"].as_mstring();
CMStringA szConversationName(UrlToSkypeId(node["conversationLink"].as_string().c_str()));
- CMStringA szFromSkypename(UrlToSkypeId(node["from"].as_string().c_str()));
-
+ CMStringA szFromSkypename(UrlToSkypeId(node["from"].as_mstring()));
+
CMStringW wszContent = node["content"].as_mstring();
std::string strMessageType = node["messagetype"].as_string();
if (strMessageType == "RichText")
wszContent = RemoveHtml(wszContent);
- bool bEdited = node["skypeeditedid"];
time_t timestamp = time(0); // fuck the server time, we need to place events in the order of our local time
int nEmoteOffset = node["skypeemoteoffset"].as_int();
@@ -115,11 +117,24 @@ void CSkypeProto::OnPrivateMessageEvent(const JSONNode &node)
if (strMessageType == "Control/Typing") {
CallService(MS_PROTO_CONTACTISTYPING, hContact, PROTOTYPE_CONTACTTYPING_INFINITE);
+ return;
}
- else if (strMessageType == "Control/ClearTyping") {
+ if (strMessageType == "Control/ClearTyping") {
CallService(MS_PROTO_CONTACTISTYPING, hContact, PROTOTYPE_CONTACTTYPING_OFF);
+ return;
}
- else if (strMessageType == "Text" || strMessageType == "RichText") {
+
+ T2Utf szMsg(wszContent);
+
+ DB::EventInfo dbei(db_event_getById(m_szModuleName, szMessageId));
+ dbei.timestamp = timestamp;
+ dbei.pBlob = szMsg;
+ dbei.cbBlob = (uint32_t)mir_strlen(szMsg);
+ dbei.szId = szMessageId;
+ if (iUserType == 19)
+ dbei.szUserId = szFromSkypename;
+
+ if (strMessageType == "Text" || strMessageType == "RichText") {
if (IsMe(szFromSkypename)) {
HANDLE hMessage = (HANDLE)atoi(szMessageId);
if (m_OutMessages.getIndex(hMessage) != -1) {
@@ -127,50 +142,48 @@ void CSkypeProto::OnPrivateMessageEvent(const JSONNode &node)
mir_cslock lck(m_lckOutMessagesList);
m_OutMessages.remove(hMessage);
- }
- else AddDbEvent(nEmoteOffset == 0 ? EVENTTYPE_MESSAGE : SKYPE_DB_EVENT_TYPE_ACTION, hContact, timestamp, dwFlags, wszContent.c_str()+nEmoteOffset, szMessageId);
- }
- else {
- CallService(MS_PROTO_CONTACTISTYPING, hContact, PROTOTYPE_CONTACTTYPING_OFF);
-
- MEVENT hDbEvent = GetMessageFromDb(szMessageId);
- if (bEdited && hDbEvent != NULL)
- EditEvent(hDbEvent, wszContent, timestamp);
- else {
- T2Utf szMsg(wszContent);
- DB::EventInfo dbei;
- dbei.timestamp = timestamp;
- dbei.pBlob = szMsg;
- dbei.cbBlob = nEmoteOffset;
- dbei.szId = szMessageId;
- ProtoChainRecvMsg(hContact, dbei);
+ return;
}
}
+ else CallService(MS_PROTO_CONTACTISTYPING, hContact, PROTOTYPE_CONTACTTYPING_OFF);
+
+ dbei.eventType = nEmoteOffset == 0 ? EVENTTYPE_MESSAGE : SKYPE_DB_EVENT_TYPE_ACTION;
}
else if (strMessageType == "Event/Call") {
- AddDbEvent(SKYPE_DB_EVENT_TYPE_CALL_INFO, hContact, timestamp, dwFlags, wszContent, szMessageId);
+ dbei.eventType = SKYPE_DB_EVENT_TYPE_CALL_INFO;
}
else if (strMessageType == "RichText/Files") {
- AddDbEvent(SKYPE_DB_EVENT_TYPE_FILETRANSFER_INFO, hContact, timestamp, dwFlags, wszContent , szMessageId);
+ dbei.eventType = SKYPE_DB_EVENT_TYPE_FILETRANSFER_INFO;
}
else if (strMessageType == "RichText/UriObject") {
- AddDbEvent(SKYPE_DB_EVENT_TYPE_URIOBJ, hContact, timestamp, dwFlags, wszContent, szMessageId);
+ dbei.eventType = SKYPE_DB_EVENT_TYPE_URIOBJ;
}
else if (strMessageType == "RichText/Contacts") {
ProcessContactRecv(hContact, timestamp, T2Utf(wszContent), szMessageId);
+ return;
}
else if (strMessageType == "RichText/Media_FlikMsg") {
- AddDbEvent(SKYPE_DB_EVENT_TYPE_MOJI, hContact, timestamp, dwFlags, wszContent, szMessageId);
+ dbei.eventType = SKYPE_DB_EVENT_TYPE_MOJI;
}
else if (strMessageType == "RichText/Media_GenericFile") {
- AddDbEvent(SKYPE_DB_EVENT_TYPE_FILE, hContact, timestamp, dwFlags, wszContent, szMessageId);
+ dbei.eventType = SKYPE_DB_EVENT_TYPE_FILE;
}
else if (strMessageType == "RichText/Media_Album") {
// do nothing
}
+ else if (iUserType == 19) {
+ OnChatEvent(node);
+ return;
+ }
else {
- AddDbEvent(SKYPE_DB_EVENT_TYPE_UNKNOWN, hContact, timestamp, dwFlags, wszContent, szMessageId);
+ dbei.eventType = SKYPE_DB_EVENT_TYPE_UNKNOWN;
+ }
+
+ if (dbei) {
+ db_event_edit(dbei.getEvent(), &dbei, true);
+ dbei.pBlob = nullptr;
}
+ else ProtoChainRecvMsg(hContact, dbei);
}
void CSkypeProto::OnMarkRead(MCONTACT hContact, MEVENT hDbEvent)
diff --git a/protocols/SkypeWeb/src/skype_polling.cpp b/protocols/SkypeWeb/src/skype_polling.cpp
index e0dac080bc..5a680dab30 100644
--- a/protocols/SkypeWeb/src/skype_polling.cpp
+++ b/protocols/SkypeWeb/src/skype_polling.cpp
@@ -189,20 +189,5 @@ void CSkypeProto::ProcessUserPresence(const JSONNode &node)
}
}
-void CSkypeProto::ProcessNewMessage(const JSONNode &node)
-{
- debugLogA(__FUNCTION__);
-
- std::string conversationLink = node["conversationLink"].as_string();
-
- int iUserType;
- UrlToSkypeId(conversationLink.c_str(), &iUserType);
-
- if (iUserType == 2 || iUserType == 8)
- OnPrivateMessageEvent(node);
- else if (iUserType == 19)
- OnChatEvent(node);
-}
-
void CSkypeProto::ProcessConversationUpdate(const JSONNode &) {}
void CSkypeProto::ProcessThreadUpdate(const JSONNode &) {}
diff --git a/protocols/SkypeWeb/src/skype_proto.h b/protocols/SkypeWeb/src/skype_proto.h
index f6fa6faf31..a4f9d7850f 100644
--- a/protocols/SkypeWeb/src/skype_proto.h
+++ b/protocols/SkypeWeb/src/skype_proto.h
@@ -180,7 +180,6 @@ private:
mir_cs m_lckOutMessagesList;
mir_cs messageSyncLock;
mir_cs m_StatusLock;
- mir_cs m_AppendMessageLock;
bool m_bThreadsTerminated;
@@ -253,16 +252,10 @@ private:
// messages
std::map<ULONGLONG, HANDLE> m_mpOutMessagesIds;
- MEVENT GetMessageFromDb(const char *messageId);
- MEVENT AddDbEvent(uint16_t type, MCONTACT hContact, uint32_t timestamp, uint32_t flags, const CMStringW &content, const CMStringA &msgId);
- void EditEvent(MEVENT hEvent, const CMStringW &content, time_t edit_time);
-
int __cdecl OnPreCreateMessage(WPARAM, LPARAM lParam);
void MarkMessagesRead(MCONTACT hContact, MEVENT hDbEvent);
- void OnPrivateMessageEvent(const JSONNode &node);
-
void ProcessContactRecv(MCONTACT hContact, time_t timestamp, const char *szContent, const char *szMessageId);
// chats
@@ -278,7 +271,6 @@ private:
void OnChatEvent(const JSONNode &node);
wchar_t* GetChatContactNick(MCONTACT hContact, const wchar_t *id, const wchar_t *name = nullptr);
- void AddMessageToChat(SESSION_INFO *si, const wchar_t *from, const wchar_t *content, bool isAction, int emoteOffset, time_t timestamp, bool isLoading = false);
void AddChatContact(SESSION_INFO *si, const wchar_t *id, const wchar_t *role, bool isChange = false);
void RemoveChatContact(SESSION_INFO *si, const wchar_t *id, bool isKick = false, const wchar_t *initiator = L"");
void SendChatMessage(SESSION_INFO *si, const wchar_t *tszMessage);