/*
Copyright (c) 2013-23 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"
static const char *szGiftTypes[] = { "thumb_256", "thumb_96", "thumb_48" };
static const char *szVKUrls[] = { "http://vk.com/", "https://vk.com/", "http://new.vk.com/", "https://new.vk.com/", "http://m.vk.com/", "https://m.vk.com/" };
static const char *szAttachmentMasks[] = { "wall%d_%d", "video%d_%d", "photo%d_%d", "audio%d_%d", "doc%d_%d", "market%d_%d", "story%d_%d" };
static const char *szVKLinkParam[] = { "?z=", "?w=", "&z=", "&w=" };
JSONNode nullNode(JSON_NULL);
bool IsEmpty(LPCWSTR str)
{
return (str == nullptr || str[0] == 0);
}
bool IsEmpty(LPCSTR str)
{
return (str == nullptr || str[0] == 0);
}
bool wlstrstr(wchar_t *_s1, wchar_t *_s2)
{
wchar_t s1[1024], s2[1024];
wcsncpy_s(s1, _s1, _TRUNCATE);
CharLowerBuff(s1, _countof(s1));
wcsncpy_s(s2, _s2, _TRUNCATE);
CharLowerBuff(s2, _countof(s2));
return wcsstr(s1, s2) != nullptr;
}
/////////////////////////////////////////////////////////////////////////////////////////
static IconItem iconList[] =
{
{ LPGEN("Notification icon"), "notification", IDI_NOTIFICATION },
{ LPGEN("Read message icon"), "read", IDI_READMSG },
{ LPGEN("Visit profile icon"), "profile", IDI_VISITPROFILE },
{ LPGEN("Load server history icon"), "history", IDI_HISTORY },
{ LPGEN("Add to friend list icon"), "addfriend", IDI_FRIENDADD },
{ LPGEN("Delete from friend list icon"), "delfriend", IDI_FRIENDDEL },
{ LPGEN("Report abuse icon"), "abuse", IDI_ABUSE },
{ LPGEN("Ban user icon"), "ban", IDI_BAN },
{ LPGEN("Broadcast icon"), "broadcast", IDI_BROADCAST },
{ LPGEN("Status icon"), "status", IDI_STATUS },
{ LPGEN("Wall message icon"), "wall", IDI_WALL },
{ LPGEN("Mark messages as read icon"), "markread", IDI_MARKMESSAGESASREAD }
};
void InitIcons()
{
g_plugin.registerIcon(LPGEN("Protocols") "/" LPGEN("VKontakte"), iconList, "VKontakte");
}
/////////////////////////////////////////////////////////////////////////////////////////
char* ExpUrlEncode(const char *szUrl, bool strict)
{
const char szHexDigits[] = "0123456789ABCDEF";
if (szUrl == nullptr)
return nullptr;
const uint8_t *s;
int outputLen;
for (outputLen = 0, s = (const uint8_t*)szUrl; *s; s++)
if ((*s & 0x80 && !strict) || // UTF-8 multibyte
('0' <= *s && *s <= '9') || //0-9
('A' <= *s && *s <= 'Z') || //ABC...XYZ
('a' <= *s && *s <= 'z') || //abc...xyz
*s == '~' || *s == '-' || *s == '_' || *s == '.' || *s == ' ')
outputLen++;
else
outputLen += 3;
char *szOutput = (char*)mir_alloc(outputLen + 1);
if (szOutput == nullptr)
return nullptr;
char *d = szOutput;
for (s = (const uint8_t*)szUrl; *s; s++)
if ((*s & 0x80 && !strict) || // UTF-8 multibyte
('0' <= *s && *s <= '9') || //0-9
('A' <= *s && *s <= 'Z') || //ABC...XYZ
('a' <= *s && *s <= 'z') || //abc...xyz
*s == '~' || *s == '-' || *s == '_' || *s == '.')
*d++ = *s;
else if (*s == ' ')
*d++ = '+';
else {
*d++ = '%';
*d++ = szHexDigits[*s >> 4];
*d++ = szHexDigits[*s & 0xF];
}
*d = '\0';
return szOutput;
}
/////////////////////////////////////////////////////////////////////////////////////////
void CVkProto::CheckUpdate()
{
if (getByte("Compatibility") < 1) {
for (auto &cc : AccContacts()) {
VKUserID_t iUserId = getDword(cc, "vk_chat_id", VK_INVALID_USER);
if (iUserId != VK_INVALID_USER) {
WriteVKUserID(cc, iUserId);
delSetting(cc, "vk_chat_id");
delSetting(cc, "ChatRoomID");
}
}
setByte("Compatibility", 1);
}
if (getByte("Iint64IDCompatibility") < 1) {
for (auto& cc : AccContacts()) {
char szID[40];
_ltoa(ReadVKUserID(cc), szID, 10);
db_unset(cc, m_szModuleName, "ID");
setString(cc, "ID", szID);
}
setByte("Iint64IDCompatibility", 1);
bIint64IDCompatibility = true;
}
}
//////////////////////// bIint64IDCompatibility /////////////////////////////////////////
void CVkProto::WriteQSWord(MCONTACT hContact, const char *szParam, uint64_t uValue)
{
if (!bIint64IDCompatibility)
db_unset(hContact, m_szModuleName, szParam);
char szValue[40];
_ltoa(uValue, szValue, 10);
setString(hContact, szParam, szValue);
}
uint64_t CVkProto::ReadQSWord(MCONTACT hContact, const char* szParam, uint64_t uDefaultValue)
{
if (!bIint64IDCompatibility) {
uint64_t uValue = getDword(hContact, szParam, uDefaultValue);
if (uValue != uDefaultValue) {
WriteQSWord(hContact, szParam, uValue);
return uValue;
}
}
ptrA szValue(getStringA(hContact, szParam));
return szValue ? strtol(szValue, nullptr, 10) : uDefaultValue;
}
VKUserID_t CVkProto::ReadVKUserIDFromString(MCONTACT hContact)
{
ptrA szID(getStringA(hContact, "ID"));
return szID ? strtol(szID, nullptr, 10) : VK_INVALID_USER;
}
VKUserID_t CVkProto::ReadVKUserID(MCONTACT hContact)
{
if (bIint64IDCompatibility)
return ReadVKUserIDFromString(hContact);
VKUserID_t iUserId = getDword(hContact, "ID", VK_INVALID_USER);
return iUserId ? iUserId : ReadVKUserIDFromString(hContact);
}
void CVkProto::WriteVKUserID(MCONTACT hContact, VKUserID_t iUserId)
{
if (bIint64IDCompatibility || iUserId > 0xFFFFFFFF) {
char szID[40];
_ltoa(iUserId, szID, 10);
setString(hContact, "ID", szID);
}
else
setDword(hContact, "ID", iUserId);
}
VKPeerType CVkProto::GetVKPeerType(VKUserID_t iPeerId)
{
if (VK_INVALID_USER == iPeerId)
return VKPeerType::vkPeerError;
if (iPeerId == VK_FEED_USER)
return VKPeerType::vkPeerFeed;
if (iPeerId < VK_INVALID_USER)
return VKPeerType::vkPeerGroup;
if ((iPeerId < VK_USERID_MAX1) || (iPeerId >= VK_USERID_MIN2 && iPeerId < VK_USERID_MAX2))
return VKPeerType::vkPeerUser;
if (iPeerId > VK_CHAT_MIN && iPeerId < VK_CHAT_MAX)
return VKPeerType::vkPeerMUC;
return VKPeerType::vkPeerError;
}
/////////////////////////////////////////////////////////////////////////////////////////
void CVkProto::ClearAccessToken()
{
debugLogA("CVkProto::ClearAccessToken");
setDword("LastAccessTokenTime", (uint32_t)time(0));
m_szAccessToken = nullptr;
delSetting("AccessToken");
ShutdownSession();
}
wchar_t* CVkProto::GetUserStoredPassword()
{
debugLogA("CVkProto::GetUserStoredPassword");
ptrA szRawPass(getStringA("Password"));
return (szRawPass != nullptr) ? mir_utf8decodeW(szRawPass) : nullptr;
}
void CVkProto::SetAllContactStatuses(int iStatus)
{
debugLogA("CVkProto::SetAllContactStatuses (%d)", iStatus);
for (auto &hContact : AccContacts()) {
if (isChatRoom(hContact))
SetChatStatus(hContact, iStatus);
else if (getWord(hContact, "Status") != iStatus)
setWord(hContact, "Status", iStatus);
if (iStatus == ID_STATUS_OFFLINE) {
SetMirVer(hContact, -1);
db_unset(hContact, m_szModuleName, "ListeningTo");
}
}
}
/////////////////////////////////////////////////////////////////////////////////////////
MCONTACT CVkProto::FindUser(VKUserID_t dwUserid, bool bCreate)
{
if (!dwUserid)
return 0;
for (auto &hContact : AccContacts()) {
if (isChatRoom(hContact))
continue;
VKUserID_t dbUserid = ReadVKUserID(hContact);
if (dbUserid == VK_INVALID_USER)
continue;
if (dbUserid == dwUserid)
return hContact;
}
if (!bCreate)
return 0;
MCONTACT hNewContact = db_add_contact();
Proto_AddToContact(hNewContact, m_szModuleName);
WriteVKUserID(hNewContact, dwUserid);
Clist_SetGroup(hNewContact, m_vkOptions.pwszDefaultGroup);
if (GetVKPeerType(dwUserid) == VKPeerType::vkPeerGroup)
setByte(hNewContact, "IsGroup", 1);
return hNewContact;
}
MCONTACT CVkProto::FindChat(VKUserID_t iUserId)
{
if (!iUserId)
return 0;
for (auto &hContact : AccContacts()) {
if (!isChatRoom(hContact))
continue;
VKUserID_t dbUserid = ReadVKUserID(hContact);
if (dbUserid == VK_INVALID_USER)
continue;
if (dbUserid == iUserId)
return hContact;
}
return 0;
}
bool CVkProto::IsGroupUser(MCONTACT hContact)
{
if (getBool(hContact, "IsGroup", false))
return true;
VKUserID_t iUserId = ReadVKUserID(hContact);
return GetVKPeerType(iUserId) == VKPeerType::vkPeerGroup;
}
/////////////////////////////////////////////////////////////////////////////////////////
JSONNode& CVkProto::CheckJsonResponse(AsyncHttpRequest *pReq, NETLIBHTTPREQUEST *reply, JSONNode &root)
{
debugLogA("CVkProto::CheckJsonResponse");
if (!reply || !reply->pData)
return nullNode;
root = JSONNode::parse(reply->pData);
if (!CheckJsonResult(pReq, root))
return nullNode;
return root["response"];
}
bool CVkProto::CheckJsonResult(AsyncHttpRequest *pReq, const JSONNode &jnNode)
{
debugLogA("CVkProto::CheckJsonResult");
if (!jnNode) {
if (pReq)
pReq->m_iErrorCode = VKERR_NO_JSONNODE;
return false;
}
const JSONNode &jnError = jnNode["error"];
const JSONNode &jnErrorCode = jnError["error_code"];
const JSONNode &jnRedirectUri = jnError["redirect_uri"];
if (!jnError || !jnErrorCode)
return true;
int iErrorCode = jnErrorCode.as_int();
debugLogA("CVkProto::CheckJsonResult %d", iErrorCode);
if (!pReq)
return (iErrorCode == 0);
pReq->m_iErrorCode = iErrorCode;
switch (iErrorCode) {
case VKERR_AUTHORIZATION_FAILED:
ConnectionFailed(LOGINERR_WRONGPASSWORD);
break;
case VKERR_ACCESS_DENIED:
if ((jnError["error_msg"] && jnError["error_msg"].as_mstring() == L"Access denied: can't set typing activity for this peer")
|| (pReq->m_szUrl.Find("messages.setActivity.json") > -1)
) {
debugLogA("CVkProto::CheckJsonResult VKERR_ACCESS_DENIED (can't set typing activity) - ignore");
break;
}
if (time(0) - getDword("LastAccessTokenTime", 0) > 60 * 60 * 24) {
debugLogA("CVkProto::CheckJsonResult VKERR_ACCESS_DENIED (AccessToken fail?)");
ClearAccessToken();
return false;
}
debugLogA("CVkProto::CheckJsonResult VKERR_ACCESS_DENIED");
MsgPopup(TranslateT("Access denied! Data will not be sent or received."), TranslateT("Error"), true);
break;
case VKERR_CAPTCHA_NEEDED:
ApplyCaptcha(pReq, jnError);
break;
case VKERR_VALIDATION_REQUIRED: // Validation Required
MsgPopup(TranslateT("You have to validate your account before you can use VK in Miranda NG"), TranslateT("Error"), true);
if (jnRedirectUri) {
T2Utf szRedirectUri(jnRedirectUri.as_mstring());
AsyncHttpRequest *pRedirectReq = new AsyncHttpRequest(this, REQUEST_GET, szRedirectUri, false, &CVkProto::OnOAuthAuthorize);
pRedirectReq->m_bApiReq = false;
pRedirectReq->bIsMainConn = true;
Push(pRedirectReq);
}
break;
case VKERR_FLOOD_CONTROL:
pReq->m_iRetry = 0;
__fallthrough;
case VKERR_UNKNOWN:
case VKERR_TOO_MANY_REQ_PER_SEC:
case VKERR_INTERNAL_SERVER_ERR:
if (pReq->m_iRetry > 0) {
pReq->bNeedsRestart = true;
Sleep(1000); //Pause for fix err
debugLogA("CVkProto::CheckJsonResult Retry = %d", pReq->m_iRetry);
pReq->m_iRetry--;
}
else {
CMStringW wszMsg(FORMAT, TranslateT("Error %d. Data will not be sent or received."), iErrorCode);
MsgPopup(wszMsg, TranslateT("Error"), true);
debugLogA("CVkProto::CheckJsonResult SendError");
}
break;
case VKERR_INVALID_PARAMETERS:
MsgPopup(TranslateT("One of the parameters specified was missing or invalid"), TranslateT("Error"), true);
break;
case VKERR_ACC_WALL_POST_DENIED:
MsgPopup(TranslateT("Access to adding post denied"), TranslateT("Error"), true);
break;
case VKERR_CANT_SEND_USER_ON_BLACKLIST:
MsgPopup(TranslateT("Can't send messages for users from blacklist"), TranslateT("Error"), true);
break;
case VKERR_CANT_SEND_USER_WITHOUT_DIALOGS:
MsgPopup(TranslateT("Can't send messages for users without dialogs"), TranslateT("Error"), true);
break;
case VKERR_CANT_SEND_YOU_ON_BLACKLIST:
MsgPopup(TranslateT("Can't send messages to this user due to their privacy settings"), TranslateT("Error"), true);
break;
case VKERR_MESSAGE_IS_TOO_LONG:
MsgPopup(TranslateT("Message is too long"), TranslateT("Error"), true);
break;
case VKERR_COULD_NOT_SAVE_FILE:
case VKERR_INVALID_ALBUM_ID:
case VKERR_INVALID_SERVER:
case VKERR_INVALID_HASH:
case VKERR_INVALID_AUDIO:
case VKERR_AUDIO_DEL_COPYRIGHT:
case VKERR_INVALID_FILENAME:
case VKERR_INVALID_FILESIZE:
case VKERR_HIMSELF_AS_FRIEND:
case VKERR_YOU_ON_BLACKLIST:
case VKERR_USER_ON_BLACKLIST:
break;
// See also CVkProto::SendFileFiled
}
return (iErrorCode == 0);
}
void CVkProto::OnReceiveSmth(NETLIBHTTPREQUEST *reply, AsyncHttpRequest *pReq)
{
JSONNode jnRoot;
const JSONNode &jnResponse = CheckJsonResponse(pReq, reply, jnRoot);
debugLogA("CVkProto::OnReceiveSmth %d", jnResponse.as_int());
}
/////////////////////////////////////////////////////////////////////////////////////////
// Quick & dirty form parser
static CMStringA getAttr(char *szSrc, LPCSTR szAttrName)
{
char *pEnd = strchr(szSrc, '>');
if (pEnd == nullptr)
return "";
*pEnd = 0;
char *p1 = strstr(szSrc, szAttrName);
if (p1 == nullptr) {
*pEnd = '>';
return "";
}
p1 += mir_strlen(szAttrName);
if (p1[0] != '=' || p1[1] != '\"') {
*pEnd = '>';
return "";
}
p1 += 2;
char *p2 = strchr(p1, '\"');
*pEnd = '>';
if (p2 == nullptr)
return "";
return CMStringA(p1, (int)(p2 - p1));
}
bool CVkProto::AutoFillForm(char *pBody, CMStringA &szAction, CMStringA& szResult)
{
debugLogA("CVkProto::AutoFillForm");
szResult.Empty();
char *pFormBeg = strstr(pBody, "