/*
Copyright (C) 2012-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"
struct
{
const wchar_t *begin, *end;
unsigned len1, len2;
}
static bbCodes[] =
{
{ L"[b]", L"[/b]", 3, 4 },
{ L"[i]", L"[/i]", 3, 4 },
{ L"[s]", L"[/s]", 3, 4 },
{ L"[u]", L"[/u]", 3, 4 },
};
TD::object_ptr formatBbcodes(const char *pszText)
{
auto res = TD::make_object();
if (mir_strlen(pszText)) {
std::wstring str = Utf2T(pszText).get();
for (auto &it : bbCodes) {
while (true) {
int i1 = (int)str.find(it.begin);
if (i1 == str.npos)
break;
int i2 = (int)str.find(it.end, i1);
if (i2 == str.npos)
break;
for (auto &jt : res->entities_) {
if (jt->offset_ > i1)
jt->offset_ -= it.len1;
if (jt->offset_ > i2)
jt->offset_ -= it.len2;
}
str.erase(i2, it.len2); i2 -= it.len1;
str.erase(i1, it.len1);
TD::object_ptr pNew;
switch (it.begin[1]) {
case 'b': pNew = TD::make_object(); break;
case 'i': pNew = TD::make_object(); break;
case 's': pNew = TD::make_object(); break;
case 'u': pNew = TD::make_object(); break;
}
res->entities_.push_back(TD::make_object(TD::int32(i1), TD::int32(i2 - i1), std::move(pNew)));
}
}
res->text_ = T2Utf(str.c_str()).get();
}
return res;
}
static CMStringA getFormattedText(TD::object_ptr &pText)
{
if (pText->get_id() == TD::formattedText::ID) {
CMStringW ret(Utf2T(pText->text_.c_str()));
unsigned offset = 0;
for (auto &it : pText->entities_) {
int iCode;
switch (it->type_->get_id()) {
case TD::textEntityTypeBold::ID: iCode = 0; break;
case TD::textEntityTypeItalic::ID: iCode = 1; break;
case TD::textEntityTypeStrikethrough::ID: iCode = 2; break;
case TD::textEntityTypeUnderline::ID: iCode = 3; break;
default:
continue;
}
auto &bb = bbCodes[iCode];
ret.Insert(offset + it->offset_ + it->length_, bb.end);
ret.Insert(offset + it->offset_, bb.begin);
offset += bb.len1 + bb.len2;
}
return T2Utf(ret).get();
}
return "";
}
/////////////////////////////////////////////////////////////////////////////////////////
CMStringA msg2id(TD::int53 chatId, TD::int53 msgId)
{
return CMStringA(FORMAT, "%lld_%lld", chatId, msgId);
}
CMStringA msg2id(const TD::message *pMsg)
{
return CMStringA(FORMAT, "%lld_%lld", pMsg->chat_id_, pMsg->id_);
}
TD::int53 dbei2id(const DBEVENTINFO &dbei)
{
if (dbei.szId == nullptr)
return -1;
auto *p = strchr(dbei.szId, '_');
return _atoi64(p ? p + 1 : dbei.szId);
}
/////////////////////////////////////////////////////////////////////////////////////////
const char *getName(const TD::usernames *pName)
{
return (pName == nullptr) ? TranslateU("none") : pName->editable_username_.c_str();
}
TD::object_ptr makeFile(const wchar_t *pwszFilename)
{
std::string szPath = T2Utf(pwszFilename);
return TD::make_object(std::move(szPath));
}
TG_FILE_REQUEST::Type AutoDetectType(const wchar_t *pwszFilename)
{
if (ProtoGetAvatarFileFormat(pwszFilename) != PA_FORMAT_UNKNOWN)
return TG_FILE_REQUEST::PICTURE;
CMStringW path(pwszFilename);
int idx = path.ReverseFind('.');
if (idx == -1 || path.Find('\\', idx) != -1)
return TG_FILE_REQUEST::FILE;
auto wszExt = path.Right(path.GetLength() - idx);
wszExt.MakeLower();
if (wszExt == L"mp4" || wszExt == L"webm")
return TG_FILE_REQUEST::VIDEO;
else if (wszExt == L"mp3" || wszExt == "ogg" || wszExt == "oga" || wszExt == "wav")
return TG_FILE_REQUEST::VOICE;
return TG_FILE_REQUEST::FILE;
}
CMStringW TG_USER::getDisplayName() const
{
if (hContact != INVALID_CONTACT_ID)
return Clist_GetContactDisplayName(hContact, 0);
if (!wszFirstName.IsEmpty())
return (wszLastName.IsEmpty()) ? wszFirstName : wszFirstName + L" " + wszLastName;
return wszNick;
}
MCONTACT CTelegramProto::GetRealContact(const TG_USER *pUser)
{
return (pUser->hContact != 0) ? pUser->hContact : m_iSavedMessages;
}
TG_USER* CTelegramProto::GetSender(const TD::MessageSender *pSender)
{
switch (pSender->get_id()) {
case TD::messageSenderChat::ID:
return FindChat(((TD::messageSenderChat *)pSender)->chat_id_);
case TD::messageSenderUser::ID:
return FindUser(((TD::messageSenderUser *)pSender)->user_id_);
}
return nullptr;
}
/////////////////////////////////////////////////////////////////////////////////////////
bool CTelegramProto::CheckSearchUser(TG_USER *pUser)
{
auto pSearchId = std::find(m_searchIds.begin(), m_searchIds.end(), pUser->chatId);
if (pSearchId == m_searchIds.end())
return false;
ReportSearchUser(pUser);
m_searchIds.erase(pSearchId);
if (m_searchIds.empty())
ProtoBroadcastAck(0, ACKTYPE_SEARCH, ACKRESULT_SUCCESS, this);
return true;
}
void CTelegramProto::ReportSearchUser(TG_USER *pUser)
{
CMStringW wszId(FORMAT, L"%lld", pUser->id), wszNick, wszLastName, wszFirstName;
PROTOSEARCHRESULT psr = {};
psr.cbSize = sizeof(psr);
psr.flags = PSR_UNICODE;
psr.id.w = wszId.GetBuffer();
if (pUser->hContact != INVALID_CONTACT_ID) {
wszNick = getMStringW(pUser->hContact, "Nick");
wszLastName = getMStringW(pUser->hContact, "LastName");
wszFirstName = getMStringW(pUser->hContact, "FirstName");
psr.nick.w = wszNick.GetBuffer();
psr.lastName.w = wszLastName.GetBuffer();
psr.firstName.w = wszFirstName.GetBuffer();
}
else {
psr.firstName.w = pUser->wszFirstName.GetBuffer();
psr.lastName.w = pUser->wszLastName.GetBuffer();
psr.nick.w = pUser->wszNick.GetBuffer();
}
ProtoBroadcastAck(0, ACKTYPE_SEARCH, ACKRESULT_DATA, this, (LPARAM)&psr);
}
/////////////////////////////////////////////////////////////////////////////////////////
int64_t CTelegramProto::GetId(MCONTACT hContact)
{
return _atoi64(getMStringA(hContact, DBKEY_ID));
}
void CTelegramProto::SetId(MCONTACT hContact, int64_t id)
{
char szId[100];
_i64toa(id, szId, 10);
setString(hContact, DBKEY_ID, szId);
}
/////////////////////////////////////////////////////////////////////////////////////////
void CTelegramProto::UpdateString(MCONTACT hContact, const char *pszSetting, const std::string &str)
{
if (str.empty())
delSetting(hContact, pszSetting);
else
setUString(hContact, pszSetting, str.c_str());
}
/////////////////////////////////////////////////////////////////////////////////////////
// Users
TG_USER* CTelegramProto::FindChat(int64_t id)
{
auto *tmp = (TG_USER *)_alloca(sizeof(TG_USER));
tmp->chatId = id;
return m_arChats.find(tmp);
}
TG_USER* CTelegramProto::FindUser(int64_t id)
{
return m_arUsers.find((TG_USER *)&id);
}
TG_USER* CTelegramProto::AddFakeUser(int64_t id, bool bIsChat)
{
auto *pu = FindUser(id);
if (pu == nullptr) {
pu = new TG_USER(id, INVALID_CONTACT_ID, bIsChat);
m_arUsers.insert(pu);
}
return pu;
}
TG_USER* CTelegramProto::AddUser(int64_t id, bool bIsChat)
{
auto *pUser = FindUser(id);
if (pUser != nullptr)
if (pUser->hContact != INVALID_CONTACT_ID)
return pUser;
MCONTACT hContact = db_add_contact();
Proto_AddToContact(hContact, m_szModuleName);
SetId(hContact, id);
if (bIsChat) {
Clist_SetGroup(hContact, TranslateT("Chat rooms"));
setByte(hContact, "ChatRoom", 1);
}
else if (mir_wstrlen(m_wszDefaultGroup))
Clist_SetGroup(hContact, m_wszDefaultGroup);
if (pUser == nullptr) {
pUser = new TG_USER(id, hContact, bIsChat);
m_arUsers.insert(pUser);
}
else {
pUser->hContact = hContact;
setWString(hContact, "Nick", pUser->wszNick);
if (!pUser->isGroupChat) {
setWString(hContact, "FirstName", pUser->wszFirstName);
setWString(hContact, "LastName", pUser->wszLastName);
}
}
return pUser;
}
/////////////////////////////////////////////////////////////////////////////////////////
// Popups
void CTelegramProto::InitPopups(void)
{
g_plugin.addPopupOption(CMStringW(FORMAT, TranslateT("%s error notifications"), m_tszUserName), m_bUsePopups);
char name[256];
mir_snprintf(name, "%s_%s", m_szModuleName, "Error");
wchar_t desc[256];
mir_snwprintf(desc, L"%s/%s", m_tszUserName, TranslateT("Errors"));
POPUPCLASS ppc = {};
ppc.flags = PCF_UNICODE;
ppc.pszName = name;
ppc.pszDescription.w = desc;
ppc.hIcon = IcoLib_GetIconByHandle(m_hProtoIcon);
ppc.colorBack = RGB(191, 0, 0); //Red
ppc.colorText = RGB(255, 245, 225); //Yellow
ppc.iSeconds = 60;
m_hPopupClass = Popup_RegisterClass(&ppc);
IcoLib_ReleaseIcon(ppc.hIcon);
}
void CTelegramProto::Popup(MCONTACT hContact, const wchar_t *szMsg, const wchar_t *szTitle)
{
if (!m_bUsePopups)
return;
char name[256];
mir_snprintf(name, "%s_%s", m_szModuleName, "Error");
CMStringW wszTitle(szTitle);
if (hContact == 0) {
wszTitle.Insert(0, L": ");
wszTitle.Insert(0, m_tszUserName);
}
POPUPDATACLASS ppd = {};
ppd.szTitle.w = wszTitle;
ppd.szText.w = szMsg;
ppd.pszClassName = name;
ppd.hContact = hContact;
Popup_AddClass(&ppd);
}
/////////////////////////////////////////////////////////////////////////////////////////
bool CTelegramProto::GetGcUserId(TG_USER *pUser, const TD::message *pMsg, char *dest)
{
if (pUser->isGroupChat) {
if (auto *pSender = GetSender(pMsg->sender_id_.get())) {
_i64toa(pSender->id, dest, 10);
if (pUser->m_si && !pSender->wszFirstName.IsEmpty())
g_chatApi.UM_AddUser(pUser->m_si, Utf2T(dest), pSender->getDisplayName(), ID_STATUS_ONLINE);
return true;
}
}
*dest = 0;
return false;
}
/////////////////////////////////////////////////////////////////////////////////////////
bool CTelegramProto::GetMessageFile(
TG_FILE_REQUEST::Type fileType,
TG_USER *pUser,
const TD::file *pFile,
const char *pszFileName,
const std::string &caption,
const char *pszId,
const char *pszUserId,
const TD::message *pMsg)
{
if (pFile->get_id() != TD::file::ID) {
debugLogA("Document contains unsupported type %d, exiting", pFile->get_id());
return false;
}
auto *pRequest = new TG_FILE_REQUEST(fileType, pFile->id_, pFile->remote_->id_.c_str());
pRequest->m_fileName = Utf2T(pszFileName);
pRequest->m_fileSize = pFile->size_;
pRequest->m_bRecv = true;
{
mir_cslock lck(m_csFiles);
m_arFiles.insert(pRequest);
}
char szReplyId[100];
MCONTACT hContact = GetRealContact(pUser);
PROTORECVFILE pre = {};
pre.dwFlags = PRFF_UTF | PRFF_SILENT;
pre.fileCount = 1;
pre.timestamp = pMsg->date_;
pre.files.a = &pszFileName;
pre.lParam = (LPARAM)pRequest;
pre.szId = pszId;
pre.szUserId = pszUserId;
if (!caption.empty())
pre.descr.a = caption.c_str();
if (pMsg->is_outgoing_)
pre.dwFlags |= PRFF_SENT;
if (Contact::IsGroupChat(hContact))
pre.dwFlags |= PRFF_READ;
if (pMsg->reply_to_message_id_) {
_i64toa(pMsg->reply_to_message_id_, szReplyId, 10);
pre.szReplyId = szReplyId;
}
ProtoChainRecvFile(hContact, &pre);
return true;
}
CMStringA CTelegramProto::GetMessageSticker(const TD::file *pFile, const char *pwszExtension)
{
auto *pFileId = pFile->remote_->unique_id_.c_str();
auto *pRequest = new TG_FILE_REQUEST(TG_FILE_REQUEST::AVATAR, pFile->id_, pFileId);
pRequest->m_destPath = GetAvatarPath() + L"\\Stickers";
CreateDirectoryW(pRequest->m_destPath, 0);
pRequest->m_fileName.Format(L"STK{%S}.%S", pFileId, pwszExtension);
{
mir_cslock lck(m_csFiles);
m_arFiles.insert(pRequest);
}
SendQuery(new TD::downloadFile(pFile->id_, 10, 0, 0, true));
return CMStringA(FORMAT, "STK{%s}", pFileId);
}
/////////////////////////////////////////////////////////////////////////////////////////
static const TD::photoSize* GetBiggestPhoto(const TD::photo *pPhoto)
{
const char *types[] = { "y", "x", "m", "s" };
for (auto *pType : types)
for (auto &it : pPhoto->sizes_)
if (it->type_ == pType)
return it.get();
return nullptr;
}
static bool checkStickerType(uint32_t ID)
{
switch (ID) {
case TD::stickerTypeRegular::ID:
case TD::stickerFullTypeRegular::ID:
return true;
default:
return false;
}
}
CMStringA CTelegramProto::GetMessageText(TG_USER *pUser, const TD::message *pMsg)
{
const TD::MessageContent *pBody = pMsg->content_.get();
char szUserId[100], *pszUserId = nullptr;
auto szMsgId(msg2id(pMsg));
if (GetGcUserId(pUser, pMsg, szUserId))
pszUserId = szUserId;
switch (pBody->get_id()) {
case TD::messageChatUpgradeTo::ID:
if (auto *pUgrade = (TD::messageChatUpgradeTo *)pBody) {
MCONTACT hContact = pUser->hContact;
m_arChats.remove(pUser);
m_arUsers.remove(pUser);
SetId(hContact, pUgrade->supergroup_id_);
pUser = new TG_USER(pUgrade->supergroup_id_, hContact, true);
m_arUsers.insert(pUser);
}
break;
case TD::messageChatAddMembers::ID:
if (auto *pDoc = (TD::messageChatAddMembers *)pBody)
for (auto &it : pDoc->member_user_ids_)
GcChangeMember(pUser, pszUserId, it, true);
break;
case TD::messageChatDeleteMember::ID:
if (auto *pDoc = (TD::messageChatDeleteMember *)pBody)
GcChangeMember(pUser, pszUserId, pDoc->user_id_, false);
break;
case TD::messageChatChangeTitle::ID:
if (auto *pDoc = (TD::messageChatChangeTitle *)pBody)
GcChangeTopic(pUser, Utf2T(pDoc->title_.c_str()));
break;
case TD::messagePhoto::ID:
if (auto *pDoc = (TD::messagePhoto *)pBody) {
auto *pPhoto = GetBiggestPhoto(pDoc->photo_.get());
if (pPhoto == nullptr) {
debugLogA("cannot find photo, exiting");
break;
}
CMStringA fileName(FORMAT, "%s (%d x %d)", TranslateU("Picture"), pPhoto->width_, pPhoto->height_);
GetMessageFile(TG_FILE_REQUEST::PICTURE, pUser, pPhoto->photo_.get(), fileName, pDoc->caption_->text_, szMsgId, pszUserId, pMsg);
}
break;
case TD::messageAudio::ID:
if (auto *pDoc = (TD::messageAudio *)pBody) {
auto *pAudio = pDoc->audio_.get();
CMStringA fileName(FORMAT, "%s (%d %s)", TranslateU("Audio"), pAudio->duration_, TranslateU("seconds"));
std::string caption = fileName.c_str();
if (!pDoc->caption_->text_.empty()) {
caption += " ";
caption += pDoc->caption_->text_;
}
GetMessageFile(TG_FILE_REQUEST::VIDEO, pUser, pAudio->audio_.get(), pAudio->file_name_.c_str(), caption, szMsgId, pszUserId, pMsg);
}
break;
case TD::messageVideo::ID:
if (auto *pDoc = (TD::messageVideo *)pBody) {
auto *pVideo = pDoc->video_.get();
CMStringA fileName(FORMAT, "%s (%d x %d, %d %s)", TranslateU("Video"), pVideo->width_, pVideo->height_, pVideo->duration_, TranslateU("seconds"));
std::string caption = fileName.c_str();
if (!pDoc->caption_->text_.empty()) {
caption += " ";
caption += pDoc->caption_->text_;
}
GetMessageFile(TG_FILE_REQUEST::VIDEO, pUser, pVideo->video_.get(), pVideo->file_name_.c_str(), caption, szMsgId, pszUserId, pMsg);
}
break;
case TD::messageAnimation::ID:
if (auto *pDoc = (TD::messageAnimation *)pBody) {
auto *pVideo = pDoc->animation_.get();
CMStringA fileName(FORMAT, "%s (%d x %d, %d %s)", TranslateU("Video"), pVideo->width_, pVideo->height_, pVideo->duration_, TranslateU("seconds"));
std::string caption = fileName.c_str();
if (!pDoc->caption_->text_.empty()) {
caption += " ";
caption += pDoc->caption_->text_;
}
GetMessageFile(TG_FILE_REQUEST::VIDEO, pUser, pVideo->animation_.get(), pVideo->file_name_.c_str(), caption, szMsgId, pszUserId, pMsg);
}
break;
case TD::messageVoiceNote::ID:
if (auto *pDoc = (TD::messageVoiceNote *)pBody) {
CMStringA fileName(FORMAT, "%s (%d %s)", TranslateU("Voice message"), pDoc->voice_note_->duration_, TranslateU("seconds"));
GetMessageFile(TG_FILE_REQUEST::VOICE, pUser, pDoc->voice_note_->voice_.get(), fileName, pDoc->caption_->text_, szMsgId, pszUserId, pMsg);
}
break;
case TD::messageDocument::ID:
if (auto *pDoc = (TD::messageDocument *)pBody)
GetMessageFile(TG_FILE_REQUEST::FILE, pUser, pDoc->document_->document_.get(), pDoc->document_->file_name_.c_str(), pDoc->caption_->text_, szMsgId, pszUserId, pMsg);
break;
case TD::messageAnimatedEmoji::ID:
if (auto *pObj = (TD::messageAnimatedEmoji *)pBody) {
if (m_bSmileyAdd) {
if (auto *pAnimated = pObj->animated_emoji_.get()) {
if (auto *pSticker = pAnimated->sticker_.get()) {
if (!checkStickerType(pSticker->full_type_->get_id())) {
debugLogA("You received a sticker of unsupported type %d, ignored", pSticker->full_type_->get_id());
break;
}
const char *pwszFileExt;
switch (pSticker->thumbnail_->format_->get_id()) {
case TD::thumbnailFormatGif::ID: pwszFileExt = "gif"; break;
case TD::thumbnailFormatPng::ID: pwszFileExt = "png"; break;
case TD::thumbnailFormatTgs::ID: pwszFileExt = "tga"; break;
case TD::thumbnailFormatJpeg::ID: pwszFileExt = "jpg"; break;
case TD::thumbnailFormatWebm::ID: pwszFileExt = "webm"; break;
case TD::thumbnailFormatWebp::ID: pwszFileExt = "webp"; break;
default:pwszFileExt = "jpeg"; break;
}
return GetMessageSticker(pSticker->thumbnail_->file_.get(), pwszFileExt);
}
}
}
return pObj->emoji_.c_str();
}
case TD::messageSticker::ID:
if (auto *pSticker = ((TD::messageSticker *)pBody)->sticker_.get()) {
if (!checkStickerType(pSticker->full_type_->get_id())) {
debugLogA("You received a sticker of unsupported type %d, ignored", pSticker->full_type_->get_id());
break;
}
if (!m_bSmileyAdd)
return CMStringA(FORMAT, "%s: %s", TranslateU("SmileyAdd plugin required to support stickers"), pSticker->emoji_.c_str());
const char *pwszFileExt;
switch (pSticker->format_->get_id()) {
case TD::stickerFormatTgs::ID: pwszFileExt = "tga"; break;
case TD::stickerFormatWebm::ID: pwszFileExt = "webm"; break;
case TD::stickerFormatWebp::ID: pwszFileExt = "webp"; break;
default:pwszFileExt = "jpeg"; break;
}
return GetMessageSticker(pSticker->thumbnail_->file_.get(), pwszFileExt);
}
case TD::messageInvoice::ID:
{
auto *pInvoice = ((TD::messageInvoice *)pBody);
CMStringA ret(FORMAT, "%s: %.2lf %s", TranslateU("You received an invoice"), double(pInvoice->total_amount_)/100.0, pInvoice->currency_.c_str());
if (!pInvoice->title_.empty())
ret.AppendFormat("\r\n%s: %s", TranslateU("Title"), pInvoice->title_.c_str());
if (auto pszText = getFormattedText(pInvoice->description_))
ret.AppendFormat("\r\n%s", pszText.c_str());
return ret;
}
case TD::messageText::ID:
if (auto pszText = getFormattedText(((TD::messageText *)pBody)->text_))
return pszText;
break;
}
return CMStringA();
}