summaryrefslogtreecommitdiff
path: root/protocols/VKontakte/src/vk_thread.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'protocols/VKontakte/src/vk_thread.cpp')
-rw-r--r--protocols/VKontakte/src/vk_thread.cpp408
1 files changed, 179 insertions, 229 deletions
diff --git a/protocols/VKontakte/src/vk_thread.cpp b/protocols/VKontakte/src/vk_thread.cpp
index 178809ddbe..87820273ad 100644
--- a/protocols/VKontakte/src/vk_thread.cpp
+++ b/protocols/VKontakte/src/vk_thread.cpp
@@ -1,5 +1,5 @@
/*
-Copyright (c) 2013-24 Miranda NG team (https://miranda-ng.org)
+Copyright (c) 2013-25 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
@@ -22,12 +22,11 @@ mir_cs CVkProto::m_csTimer;
char szBlankUrl[] = "https://oauth.vk.com/blank.html";
char szScore[] = "friends,photos,audio,docs,video,wall,messages,offline,status,notifications,groups";
-char szVKUserAgent[] = "Mozilla/5.0 (Windows NT 6.3; Trident/7.0; rv:11.0) like Gecko";
-char szVKUserAgentCH[] = "\"Microsoft Edge\";v =\"95\", \"Chromium\";v =\"95\", \";Not A Brand\";v = \"99\"";
+char szVKTokenBeg[] = "access_token=";
+char szVKCookieDomain[] = ".vk.com";
+
+
-static char szVKTokenBeg[] = "access_token=";
-static char szVKLoginDomain[] = "https://m.vk.com";
-static char szVKCookieDomain[] = ".vk.com";
static char szFieldsName[] = "id, first_name, last_name, photo_100, bdate, sex, timezone, "
"contacts, last_seen, online, status, country, city, relation, interests, activities, "
"music, movies, tv, books, games, quotes, about, domain, can_write_private_message";
@@ -119,7 +118,12 @@ static void CALLBACK VKUnsetTimer(void*)
void CVkProto::OnTimerTic()
{
- RetrieveUsersInfo(true);
+
+ if (++m_iLoadCListIntervalCounter >= m_vkOptions.iLoadCListInterval) {
+ RetrieveUsersInfo(true);
+ m_iLoadCListIntervalCounter = 0;
+ }
+
RetrieveUnreadEvents();
SetServerStatus(m_iDesiredStatus);
}
@@ -204,169 +208,46 @@ void CVkProto::OnLoggedOut()
/////////////////////////////////////////////////////////////////////////////////////////
-void CVkProto::OnOAuthAuthorize(MHttpResponse *reply, AsyncHttpRequest*)
+bool CVkProto::LoadToken(LPCSTR pszUrlSring)
{
- debugLogA("CVkProto::OnOAuthAuthorize %d", reply->resultCode);
- GrabCookies(reply, szVKCookieDomain);
-
- if (reply->resultCode == 404 && !m_bErr404Return) {
- m_bErr404Return = true;
- setString("AccessScore", szScore);
- AsyncHttpRequest* pReq = new AsyncHttpRequest(this, REQUEST_GET, "https://oauth.vk.com/authorize", false, &CVkProto::OnOAuthAuthorize);
- pReq
- << INT_PARAM("client_id", VK_APP_ID)
- << CHAR_PARAM("scope", szScore)
- << CHAR_PARAM("redirect_uri", szBlankUrl)
- << CHAR_PARAM("display", "mobile")
- << CHAR_PARAM("response_type", "token")
- << VER_API;
- pReq->m_bApiReq = false;
- pReq->bIsMainConn = true;
- ApplyCookies(pReq);
- pReq->AddHeader("User-agent", szVKUserAgent);
- Push(pReq);
- return;
- }
-
- if (reply->resultCode == 302) { // manual redirect
- LPCSTR pszLocation = reply->FindHeader("Location");
- if (pszLocation) {
- if (!_strnicmp(pszLocation, szBlankUrl, sizeof(szBlankUrl) - 1)) {
- m_szAccessToken = nullptr;
- LPCSTR p = strstr(pszLocation, szVKTokenBeg);
- if (p) {
- p += sizeof(szVKTokenBeg) - 1;
- for (LPCSTR q = p + 1; *q; q++) {
- if (*q == '&' || *q == '=' || *q == '\"') {
- m_szAccessToken = mir_strndup(p, q - p);
- break;
- }
- }
- if (m_szAccessToken == nullptr)
- m_szAccessToken = mir_strdup(p);
- setString("AccessToken", m_szAccessToken);
- RetrieveMyInfo();
- }
- else
- ConnectionFailed(LOGINERR_NOSERVER);
- }
- else {
- AsyncHttpRequest *pRedirectReq = new AsyncHttpRequest();
- pRedirectReq->requestType = REQUEST_GET;
- pRedirectReq->flags = NLHRF_DUMPASTEXT | NLHRF_HTTP11;
- pRedirectReq->m_pFunc = &CVkProto::OnOAuthAuthorize;
- pRedirectReq->AddHeader("Referer", m_szPrevUrl);
- pRedirectReq->Redirect(reply);
- if (!pRedirectReq->m_szUrl.IsEmpty()) {
- if (pRedirectReq->m_szUrl[0] == '/')
- pRedirectReq->m_szUrl = szVKLoginDomain + pRedirectReq->m_szUrl;
- ApplyCookies(pRedirectReq);
- m_szPrevUrl = pRedirectReq->m_szUrl;
- }
-
- pRedirectReq->m_bApiReq = false;
- pRedirectReq->bIsMainConn = true;
- // Headers
- pRedirectReq->AddHeader("User-agent", szVKUserAgent);
- pRedirectReq->AddHeader("dht", "1");
- pRedirectReq->AddHeader("sec-ch-ua", szVKUserAgentCH);
- pRedirectReq->AddHeader("sec-ch-ua-mobile", "?0");
- pRedirectReq->AddHeader("sec-ch-ua-platform", "Windows");
- pRedirectReq->AddHeader("sec-fetch-dest", "document");
- pRedirectReq->AddHeader("sec-fetch-mode", "navigate");
- pRedirectReq->AddHeader("sec-fetch-site", "same-site");
- pRedirectReq->AddHeader("sec-fetch-user", "?1");
- pRedirectReq->AddHeader("upgrade-insecure-requests", "1");
- //Headers
-
- Push(pRedirectReq);
- }
- }
- else
- ConnectionFailed(LOGINERR_NOSERVER);
- return;
- }
-
- if (reply->resultCode != 200 || reply->body.IsEmpty() || (!(strstr(reply->body, "method=\"post\"") || strstr(reply->body, "method=\"POST\"")) && !strstr(reply->body, "meta http-equiv=\"refresh\""))) { // something went wrong
- ConnectionFailed(LOGINERR_NOSERVER);
- return;
- }
-
- LPCSTR pBlankUrl = strstr(reply->body, szBlankUrl);
- if (pBlankUrl) {
- debugLogA("CVkProto::OnOAuthAuthorize blank ulr found");
- m_szAccessToken = nullptr;
- LPCSTR p = strstr(pBlankUrl, szVKTokenBeg);
- if (p) {
- p += sizeof(szVKTokenBeg) - 1;
- for (LPCSTR q = p + 1; *q; q++) {
- if (*q == '&' || *q == '=' || *q == '\"') {
- m_szAccessToken = mir_strndup(p, q - p);
- break;
- }
- }
- setString("AccessToken", m_szAccessToken);
- RetrieveMyInfo();
- }
- else {
- debugLogA("CVkProto::OnOAuthAuthorize blank ulr found, access_token not found");
- ConnectionFailed(LOGINERR_NOSERVER);
- }
- return;
+ LPCSTR p = strstr(pszUrlSring, szVKTokenBeg);
+ if (!p) {
+ debugLogA("CVkProto::LoadToken error");
+ return false;
}
-
- auto *pMsgWarning = strstr(reply->body, "service_msg_warning");
- if (pMsgWarning) {
- auto *p1 = strchr(pMsgWarning, '>');
- auto *p2 = strchr(pMsgWarning, '<');
- if (p1 && p2 && (p1 + 1 < p2)) {
- CMStringA szMsg(p1 + 1, (int)(p2 - p1 - 1));
- MsgPopup(ptrW(mir_utf8decodeW(szMsg)), TranslateT("Service message"), true);
- debugLogA("CVkProto::OnOAuthAuthorize %s", szMsg.c_str());
+
+ p += sizeof(szVKTokenBeg) - 1;
+ for (LPCSTR q = p + 1; *q; q++) {
+ if (*q == '&' || *q == '=' || *q == '\"') {
+ m_szAccessToken = mir_strndup(p, q - p);
+ break;
}
- ConnectionFailed(LOGINERR_WRONGPASSWORD);
- return;
}
+ if (m_szAccessToken == nullptr)
+ m_szAccessToken = mir_strdup(p);
+ setString("AccessToken", m_szAccessToken);
+ setDword("LastAccessTokenTime", time(0));
+ debugLogA("CVkProto::LoadToken OK");
+ return true;
+}
- CMStringA szAction, szBody;
- bool bSuccess = AutoFillForm(reply->body.GetBuffer(), szAction, szBody);
- if (!bSuccess || szAction.IsEmpty() || szBody.IsEmpty()) {
- if (m_bPrevError) {
- ConnectionFailed(LOGINERR_NOSERVER);
- return;
- }
- m_bPrevError = true;
- }
+void CVkProto::LogIn(LPCSTR pszUrl)
+{
+ debugLogA("CVkProto::LogIn %s", pszUrl ? pszUrl : " ");
+ CMStringA szTokenReq(
+ FORMAT,
+ "https://oauth.vk.com/authorize?client_id=%d&scope=%s&redirect_uri=%s&display=mobile&response_type=token&v=%s",
+ VK_APP_ID,
+ mir_urlEncode(szScore).c_str(),
+ mir_urlEncode(szBlankUrl).c_str(),
+ VER_API
+ );
- AsyncHttpRequest *pReq = new AsyncHttpRequest();
- pReq->requestType = REQUEST_POST;
- pReq->flags = NLHRF_DUMPASTEXT | NLHRF_HTTP11;
- pReq->m_szParam = szBody;
- pReq->m_szUrl = szAction;
- if (!pReq->m_szUrl.IsEmpty() && pReq->m_szUrl[0] == '/')
- pReq->m_szUrl = szVKLoginDomain + pReq->m_szUrl;
- m_szPrevUrl = pReq->m_szUrl;
- pReq->m_pFunc = &CVkProto::OnOAuthAuthorize;
- pReq->AddHeader("Content-Type", "application/x-www-form-urlencoded");
- pReq->Redirect(reply);
- ApplyCookies(pReq);
- // Headers
- pReq->AddHeader("User-agent", szVKUserAgent);
- pReq->AddHeader("dht", "1");
- pReq->AddHeader("origin", "https://oauth.vk.com");
- pReq->AddHeader("referer", "https://oauth.vk.com/");
- pReq->AddHeader("sec-ch-ua", szVKUserAgentCH);
- pReq->AddHeader("sec-ch-ua-mobile", "?0");
- pReq->AddHeader("sec-ch-ua-platform", "Windows");
- pReq->AddHeader("sec-fetch-dest", "document");
- pReq->AddHeader("sec-fetch-mode", "navigate");
- pReq->AddHeader("sec-fetch-site", "same-site");
- pReq->AddHeader("sec-fetch-user", "?1");
- pReq->AddHeader("upgrade-insecure-requests", "1");
- //Headers
- pReq->m_bApiReq = false;
- pReq->bIsMainConn = true;
- Push(pReq);
+ CVkTokenForm dlg(this, pszUrl ? pszUrl : szTokenReq.c_str());
+ if (dlg.DoModal() && LoadToken(dlg.Result))
+ RetrieveMyInfo();
+ else
+ ConnectionFailed(LOGINERR_NOSERVER);
}
/////////////////////////////////////////////////////////////////////////////////////////
@@ -395,8 +276,10 @@ void CVkProto::OnReceiveMyInfo(MHttpResponse *reply, AsyncHttpRequest *pReq)
JSONNode jnRoot;
const JSONNode &jnResponse = CheckJsonResponse(pReq, reply, jnRoot);
- if (!jnResponse)
+ if (!jnResponse) {
+ ConnectionFailed(LOGINERR_NOSERVER);
return;
+ }
const JSONNode &jnUser = *(jnResponse.begin());
@@ -405,11 +288,11 @@ void CVkProto::OnReceiveMyInfo(MHttpResponse *reply, AsyncHttpRequest *pReq)
WriteVKUserID(0, m_iMyUserId);
OnLoggedIn();
- RetrieveUserInfo(m_iMyUserId);
TrackVisitor();
RetrieveUnreadMessages();
RetrieveFriends(m_vkOptions.bLoadOnlyFriends);
RetrievePollingInfo();
+ RetrieveUserInfo(m_iMyUserId);
}
MCONTACT CVkProto::SetContactInfo(const JSONNode &jnItem, bool bFlag, VKContactType vkContactType)
@@ -492,12 +375,8 @@ MCONTACT CVkProto::SetContactInfo(const JSONNode &jnItem, bool bFlag, VKContactT
if (!wszValue.IsEmpty()) {
int d, m, y, iReadCount;
iReadCount = swscanf(wszValue, L"%d.%d.%d", &d, &m, &y);
- if (iReadCount > 1) {
- if (iReadCount == 3)
- setWord(hContact, "BirthYear", y);
- setByte(hContact, "BirthDay", d);
- setByte(hContact, "BirthMonth", m);
- }
+ if (iReadCount > 1)
+ Contact::SetBirthday(hContact, d, m, (iReadCount == 3) ? y : 0);
}
wszValue = jnItem["photo_100"].as_mstring();
@@ -518,6 +397,8 @@ MCONTACT CVkProto::SetContactInfo(const JSONNode &jnItem, bool bFlag, VKContactT
int iNewStatus = (jnItem["online"].as_int() == 0) ? ID_STATUS_OFFLINE : ID_STATUS_ONLINE;
setWord(hContact, "Status", iNewStatus);
+
+ db_unset(hContact, m_szModuleName, "EmptyFrameOffline");
if (iNewStatus == ID_STATUS_ONLINE) {
db_set_dw(hContact, "BuddyExpectator", "LastSeen", (uint32_t)time(0));
@@ -665,11 +546,20 @@ void CVkProto::RetrieveUserInfo(VKUserID_t iUserId)
return;
}
- Push(new AsyncHttpRequest(this, REQUEST_POST, "/method/execute.RetrieveUserInfo", true, &CVkProto::OnReceiveUserInfo)
+ CMStringA szUserId(FORMAT, "%d", iUserId);
+
+ Push(
+ new AsyncHttpRequest(
+ this,
+ REQUEST_POST,
+ "/method/execute.RetrieveUserInfo",
+ true,
+ &CVkProto::OnReceiveUserFrameInfo,
+ AsyncHttpRequest::rpLowCListEvents
+ )
<< INT_PARAM("userid", iUserId)
<< CHAR_PARAM("fields", szFieldsName)
- );
-
+ )->pUserInfo = mir_strdup(szUserId.c_str());
}
void CVkProto::RetrieveGroupInfo(VKUserID_t iGroupId)
@@ -694,101 +584,113 @@ void CVkProto::RetrieveGroupInfo(CMStringA& groupIDs)
<< CHAR_PARAM("group_ids", groupIDs));
}
-void CVkProto::RetrieveUsersInfo(bool bFreeOffline, bool bRepeat)
+void CVkProto::RetrieveUsersFrameInfo(CMStringA& szUserIds, bool bFreeOffline, bool bSendOnline, bool bRepeat)
{
- debugLogA("CVkProto::RetrieveUsersInfo");
- if (!IsOnline())
+ debugLogA("CVkProto::RetrieveUsersFrameInfo %s", szUserIds);
+ if (!IsOnline() || szUserIds.IsEmpty())
return;
-
- CMStringA userIDs;
- int i = 0;
- for (auto &hContact : AccContacts()) {
- VKUserID_t iUserId = ReadVKUserID(hContact);
- if (iUserId == VK_INVALID_USER || iUserId == VK_FEED_USER || iUserId < 0 || isChatRoom(hContact))
- continue;
-
- bool bIsFriend = !getBool(hContact, "Auth", true);
- if (bFreeOffline && !m_vkOptions.bLoadFullCList && bIsFriend)
- continue;
-
- if (!userIDs.IsEmpty())
- userIDs.AppendChar(',');
- userIDs.AppendFormat("%i", iUserId);
-
- if (i == MAX_CONTACTS_PER_REQUEST)
- break;
- i++;
- }
-
- Push(new AsyncHttpRequest(this, REQUEST_POST, "/method/execute.RetrieveUsersInfo", true, &CVkProto::OnReceiveUserInfo)
- << CHAR_PARAM("userids", userIDs)
+
+ Push(
+ new AsyncHttpRequest(
+ this,
+ REQUEST_POST,
+ "/method/execute.RetrieveUsersFrameInfo",
+ true,
+ &CVkProto::OnReceiveUserFrameInfo,
+ AsyncHttpRequest::rpLowCListEvents
+ )
+ << CHAR_PARAM("userids", szUserIds)
<< CHAR_PARAM("fields", (bFreeOffline ? "online,status,can_write_private_message" : szFieldsName))
<< INT_PARAM("norepeat", (int)bRepeat)
- << INT_PARAM("setonline", (int)m_bNeedSendOnline)
+ << INT_PARAM("setonline", (int)(m_bNeedSendOnline && bSendOnline))
<< INT_PARAM("func_v", (bFreeOffline && !m_vkOptions.bLoadFullCList) ? 1 : 2)
- );
-
+ )->pUserInfo = mir_strdup(szUserIds.c_str());
}
-void CVkProto::OnReceiveUserInfo(MHttpResponse *reply, AsyncHttpRequest *pReq)
+void CVkProto::OnReceiveUserFrameInfo(MHttpResponse* reply, AsyncHttpRequest* pReq)
{
- debugLogA("CVkProto::OnReceiveUserInfo %d", reply->resultCode);
+ debugLogA("CVkProto::OnReceiveUserFrameInfo %d", reply->resultCode);
if (reply->resultCode != 200 || !IsOnline())
return;
JSONNode jnRoot;
- const JSONNode &jnResponse = CheckJsonResponse(pReq, reply, jnRoot);
+ const JSONNode& jnResponse = CheckJsonResponse(pReq, reply, jnRoot);
if (!jnResponse)
return;
- const JSONNode &jnUsers = jnResponse["users"];
+ const JSONNode& jnUsers = jnResponse["users"];
if (!jnUsers)
return;
- if (!jnResponse["norepeat"].as_bool() && jnResponse["usercount"].as_int() == 0) {
- RetrieveUsersInfo(true, true);
- return;
+ CMStringA szUserIds;
+ if (pReq->pUserInfo) {
+ szUserIds = ((const char*)pReq->pUserInfo);
+ mir_free(pReq->pUserInfo);
}
- LIST<void> arContacts(10, PtrKeySortT);
+ bool bEmptyFrame = !jnResponse["norepeat"].as_bool() && jnResponse["usercount"].as_int() == 0;
- for (auto &hContact : AccContacts())
- if (!isChatRoom(hContact) && !IsGroupUser(hContact))
- arContacts.insert((HANDLE)hContact);
- for (auto &it : jnUsers) {
+
+ if (bEmptyFrame && m_vkOptions.bRepeatRequestAfterEmptyFrame) {
+ Sleep(5000);
+ RetrieveUsersFrameInfo(szUserIds, true, false, true);
+ return;
+ }
+
+ LIST<void> arContacts(100, PtrKeySortT);
+
+ for (auto& hContact : AccContacts())
+ if (!isChatRoom(hContact) && !IsGroupUser(hContact)) {
+ if (szUserIds.IsEmpty()) {
+ if (!getBool(hContact, "Auth", true))
+ arContacts.insert((HANDLE)hContact);
+ }
+ else {
+ VKUserID_t iUserId = ReadVKUserID(hContact);
+ char szId[40];
+ ltoa(iUserId, szId, 10);
+ if (szUserIds.Find(szId) >= 0)
+ arContacts.insert((HANDLE)hContact);
+ }
+ }
+
+ for (auto& it : jnUsers) {
MCONTACT hContact = SetContactInfo(it);
if (hContact)
arContacts.remove((HANDLE)hContact);
}
if (jnResponse["freeoffline"].as_bool())
- for (auto &it : arContacts) {
+ for (auto& it : arContacts) {
MCONTACT cc = (UINT_PTR)it;
VKUserID_t iUserId = ReadVKUserID(cc);
if (iUserId == m_iMyUserId || iUserId == VK_FEED_USER)
continue;
- int iContactStatus = getWord(cc, "Status", ID_STATUS_OFFLINE);
-
- if ((iContactStatus == ID_STATUS_ONLINE)
- || (iContactStatus == ID_STATUS_INVISIBLE && time(0) - getDword(cc, "InvisibleTS", 0) >= m_vkOptions.iInvisibleInterval * 60LL)) {
- setWord(cc, "Status", ID_STATUS_OFFLINE);
- SetMirVer(cc, -1);
- db_unset(cc, m_szModuleName, "ListeningTo");
+ if (getBool(cc, "EmptyFrameOffline")) {
+ int iContactStatus = getWord(cc, "Status", ID_STATUS_OFFLINE);
+ if ((iContactStatus == ID_STATUS_ONLINE)
+ || (iContactStatus == ID_STATUS_INVISIBLE && time(0) - getDword(cc, "InvisibleTS", 0) >= m_vkOptions.iInvisibleInterval * 60LL)) {
+ setWord(cc, "Status", ID_STATUS_OFFLINE);
+ SetMirVer(cc, -1);
+ db_unset(cc, m_szModuleName, "ListeningTo");
+ }
+ db_unset(cc, m_szModuleName, "EmptyFrameOffline");
}
+ else
+ setByte(cc, "EmptyFrameOffline", 1);
}
arContacts.destroy();
- AddFeedSpecialUser();
- const JSONNode &jnRequests = jnResponse["requests"];
+ const JSONNode& jnRequests = jnResponse["requests"];
if (!jnRequests)
return;
int iCount = jnRequests["count"].as_int();
- const JSONNode &jnItems = jnRequests["items"];
+ const JSONNode& jnItems = jnRequests["items"];
if (!iCount || !jnItems)
return;
@@ -802,12 +704,47 @@ void CVkProto::OnReceiveUserInfo(MHttpResponse *reply, AsyncHttpRequest *pReq)
if (!IsAuthContactLater(hContact)) {
RetrieveUserInfo(iUserId);
AddAuthContactLater(hContact);
- CVkDBAddAuthRequestThreadParam *param = new CVkDBAddAuthRequestThreadParam(hContact, false);
- ForkThread(&CVkProto::DBAddAuthRequestThread, (void *)param);
+ CVkDBAddAuthRequestThreadParam* param = new CVkDBAddAuthRequestThreadParam(hContact, false);
+ ForkThread(&CVkProto::DBAddAuthRequestThread, (void*)param);
}
}
}
+void CVkProto::RetrieveUsersInfo(bool bFreeOffline)
+{
+ debugLogA("CVkProto::RetrieveUsersInformation");
+ if (!IsOnline())
+ return;
+
+ CMStringA szUserIds;
+ int i = 0;
+ bool bSendOnline = true;
+
+ for (auto& hContact : AccContacts()) {
+ VKUserID_t iUserId = ReadVKUserID(hContact);
+ if (iUserId == VK_INVALID_USER || iUserId == VK_FEED_USER || iUserId < 0 || isChatRoom(hContact))
+ continue;
+
+ if (!szUserIds.IsEmpty())
+ szUserIds.AppendChar(',');
+ szUserIds.AppendFormat("%i", iUserId);
+
+ if (i < (MAX_CONTACTS_PER_REQUEST - 1))
+ i++;
+ else {
+ RetrieveUsersFrameInfo(szUserIds, bFreeOffline, bSendOnline, false);
+ i = 0;
+ szUserIds.Empty();
+ bSendOnline = false;
+ }
+ }
+
+ if(!szUserIds.IsEmpty())
+ RetrieveUsersFrameInfo(szUserIds, bFreeOffline, bSendOnline, false);
+
+ AddFeedSpecialUser();
+}
+
void CVkProto::OnReceiveGroupInfo(MHttpResponse *reply, AsyncHttpRequest *pReq)
{
debugLogA("CVkProto::OnReceiveUserInfo %d", reply->resultCode);
@@ -1142,4 +1079,17 @@ INT_PTR __cdecl CVkProto::SvcVisitProfile(WPARAM hContact, LPARAM)
Utils_OpenUrlW(wszUrl);
return 0;
+}
+
+INT_PTR __cdecl CVkProto::SvcGoToSiteIM(WPARAM hContact, LPARAM)
+{
+ debugLogA("CVkProto::SvcGoToSiteIM");
+
+ VKUserID_t iUserId = ReadVKUserID(hContact);
+ if (isChatRoom(hContact))
+ iUserId += VK_CHAT_MIN;
+ CMStringW wszUrl(FORMAT, L"https://vk.com/im/convo/%d?entrypoint=list_all", iUserId);
+
+ Utils_OpenUrlW(wszUrl);
+ return 0;
} \ No newline at end of file