2
0
mirror of https://github.com/ars3niy/tdlib-purple synced 2025-08-22 09:57:52 +00:00
tdlib-purple/receiving.cpp

1141 lines
54 KiB
C++
Raw Normal View History

#include "receiving.h"
#include "client-utils.h"
#include "format.h"
#include "purple-info.h"
#include "file-transfer.h"
2020-10-12 19:48:44 +02:00
#include "sticker.h"
#include "config.h"
#include "call.h"
#include <algorithm>
enum {
HISTORY_MESSAGES_ABSOLUTE_LIMIT = 10000
};
std::string makeNoticeWithSender(const td::td_api::chat &chat, const TgMessageInfo &message,
const char *noticeText, PurpleAccount *account)
{
std::string prefix = getSenderDisplayName(chat, message, account);
if (!prefix.empty())
prefix += ": ";
return prefix + noticeText;
}
std::string getMessageText(const td::td_api::formattedText &text)
{
char *newText = purple_markup_escape_text(text.text_.c_str(), text.text_.size());
std::string result(newText);
g_free(newText);
return result;
}
std::string makeInlineImageText(int imgstoreId)
{
return "\n<img id=\"" + std::to_string(imgstoreId) + "\">";
}
static PurpleMessageFlags getNotificationFlags(PurpleMessageFlags extraFlags)
{
unsigned flags = (extraFlags & PURPLE_MESSAGE_ERROR) | (extraFlags & PURPLE_MESSAGE_NO_LOG);
if (flags == 0)
flags = PURPLE_MESSAGE_SYSTEM;
return (PurpleMessageFlags)flags;
}
void sendConversationReadReceipts(TdAccountData &account, PurpleConversation *conv)
{
if (!conversationHasFocus(conv))
return;
ChatId chatId;
PurpleConversationType convType = purple_conversation_get_type(conv);
const char *convName = purple_conversation_get_name(conv);
if (convType == PURPLE_CONV_TYPE_IM) {
UserId privateChatUserId = purpleBuddyNameToUserId(convName);
SecretChatId secretChatId = purpleBuddyNameToSecretChatId(convName);
const td::td_api::chat *tdlibChat = nullptr;
if (privateChatUserId.valid())
tdlibChat = account.getPrivateChatByUserId(privateChatUserId);
else if (secretChatId.valid())
tdlibChat = account.getChatBySecretChat(secretChatId);
if (tdlibChat)
chatId = getId(*tdlibChat);
} else if (convType == PURPLE_CONV_TYPE_CHAT)
chatId = getTdlibChatId(convName);
std::vector<ReadReceipt> receipts;
account.extractPendingReadReceipts(chatId, receipts);
if (!receipts.empty()) {
purple_debug_misc(config::pluginId, "Sending %zu read receipts for chat %" G_GINT64_FORMAT "\n",
receipts.size(), chatId.value());
td::td_api::object_ptr<td::td_api::viewMessages> viewMessagesReq = td::td_api::make_object<td::td_api::viewMessages>();
viewMessagesReq->chat_id_ = chatId.value();
viewMessagesReq->force_read_ = true; // no idea what "closed chats" are at this point
viewMessagesReq->message_ids_.resize(receipts.size());
for (size_t i = 0; i < receipts.size(); i++)
viewMessagesReq->message_ids_[i] = receipts[i].messageId.value();
account.transceiver.sendQuery(std::move(viewMessagesReq), nullptr);
}
}
void showMessageTextIm(TdAccountData &account, const char *purpleUserName, const char *text,
const char *notification, time_t timestamp, PurpleMessageFlags flags)
{
PurpleConversation *conv = NULL;
if (text) {
if (flags & PURPLE_MESSAGE_SEND) {
// serv_got_im seems to work for messages sent from another client, but not for
// echoed messages from this client. Therefore, this (code snippet from facebook plugin).
conv = getImConversation(account.purpleAccount, purpleUserName);
purple_conv_im_write(purple_conversation_get_im_data(conv),
purple_account_get_name_for_display(account.purpleAccount),
text, flags, timestamp);
} else {
serv_got_im(purple_account_get_connection(account.purpleAccount), purpleUserName, text,
flags, timestamp);
conv = getImConversation(account.purpleAccount, purpleUserName);
}
}
if (notification) {
if (conv == NULL)
conv = getImConversation(account.purpleAccount, purpleUserName);
purple_conv_im_write(purple_conversation_get_im_data(conv), purpleUserName, notification,
getNotificationFlags(flags), timestamp);
}
// TODO: sending all pending read receipts for the chat is technically not quite right,
// because maybe a message is being shown while others are waiting for some asynchronous
// response before they can be displayed. But who cares.
if (conv != NULL)
sendConversationReadReceipts(account, conv);
}
static void showMessageTextChat(TdAccountData &account, const td::td_api::chat &chat,
const TgMessageInfo &message, const char *text,
const char *notification, PurpleMessageFlags flags)
{
// Again, doing what facebook plugin does
int purpleId = account.getPurpleChatId(getId(chat));
PurpleConvChat *conv = getChatConversation(account, chat, purpleId);
if (text) {
if (flags & PURPLE_MESSAGE_SEND) {
if (conv)
purple_conv_chat_write(conv, purple_account_get_name_for_display(account.purpleAccount),
text, flags, message.timestamp);
} else {
if (purpleId != 0)
serv_got_chat_in(purple_account_get_connection(account.purpleAccount), purpleId,
message.incomingGroupchatSender.empty() ? "someone" : message.incomingGroupchatSender.c_str(),
flags, text, message.timestamp);
}
}
if (notification) {
if (conv)
// Protocol plugins mostly use who="" for such messages, but this currently causes problems
// with Spectrum. Use some non-empty string. Pidgin will ignore the who parameter for
// notification messages.
purple_conv_chat_write(conv, " ", notification, getNotificationFlags(flags), message.timestamp);
}
// TODO: sending all pending read receipts for the chat is technically not quite right,
// because maybe a message is being shown while others are waiting for some asynchronous
// response before they can be displayed. But who cares.
PurpleConversation *baseConv = conv ? purple_conv_chat_get_conversation(conv) : NULL;
if (baseConv != NULL)
sendConversationReadReceipts(account, baseConv);
}
static std::string quoteMessage(const td::td_api::message *message, TdAccountData &account)
{
const td::td_api::user *originalAuthor = nullptr;
if (message)
originalAuthor = account.getUser(getSenderUserId(*message));
std::string originalName;
if (originalAuthor)
originalName = account.getDisplayName(*originalAuthor);
else {
// message == NULL means it could not be fetched, or took too long to fetch
// TRANSLATOR: In-line placeholder if the original author of a quote is unknown. Is at the beginning of the line if and only if you make it so, see "<b>&bt {} wrote:"...
originalName = _("Unknown user");
}
std::string text;
if (!message || !message->content_) {
// TRANSLATOR: In-chat placeholder when something unknown is being replied to.
text = _("[message unavailable]");
} else switch (message->content_->get_id()) {
case td::td_api::messageText::ID: {
const td::td_api::messageText &messageText = static_cast<const td::td_api::messageText &>(*message->content_);
if (messageText.text_)
text = getMessageText(*messageText.text_);
else
text = "";
break;
}
case td::td_api::messagePhoto::ID: {
const td::td_api::messagePhoto &photo = static_cast<const td::td_api::messagePhoto &>(*message->content_);
// TRANSLATOR: In-line placeholder when a photo is being replied to.
text = _("[photo]");
if (photo.caption_)
text += " " + photo.caption_->text_;
break;
}
case td::td_api::messageDocument::ID: {
const td::td_api::messageDocument &document = static_cast<const td::td_api::messageDocument &>(*message->content_);
if (document.document_) {
// TRANSLATOR: In-line placeholder when a file is being replied to. Arguments will be the file name and MIME type (e.g. "application/gzip")
text = formatMessage(_("[file: {0} ({1})]"), {document.document_->file_name_,
document.document_->mime_type_});
} else {
// Not supposed to be possible, but just in case
text = "[file]";
}
if (document.caption_)
text += " " + document.caption_->text_;
break;
}
case td::td_api::messageVideo::ID: {
const td::td_api::messageVideo &video = static_cast<const td::td_api::messageVideo &>(*message->content_);
if (video.video_) {
// TRANSLATOR: In-line placeholder when a video is being replied to. Argument will be the file name.
text = formatMessage(_("[video: {}]"), video.video_->file_name_);
} else {
// Not supposed to be possible, but just in case
text = "[video]";
}
if (video.caption_)
text += " " + video.caption_->text_;
break;
}
case td::td_api::messageSticker::ID:
// TRANSLATOR: In-line placeholder when a sticker is being replied to.
text = _("[sticker]");
break;
default:
text = '[' + getUnsupportedMessageDescription(*message->content_) + ']';
break;
}
for (unsigned i = 0; i < text.size(); i++)
if (text[i] == '\n') text[i] = ' ';
// TRANSLATOR: In-chat notification of a reply. Arguments will be username and the original text or description thereof. Please preserve the HTML.
return formatMessage(_("<b>&gt; {0} wrote:</b>\n&gt; {1}"), {originalName, text});
}
void showMessageText(TdAccountData &account, const td::td_api::chat &chat, const TgMessageInfo &message,
const char *text, const char *notification, uint32_t extraFlags)
{
PurpleMessageFlags directionFlag = message.outgoing ? PURPLE_MESSAGE_SEND : PURPLE_MESSAGE_RECV;
PurpleMessageFlags flags = (PurpleMessageFlags) (extraFlags | directionFlag);
if (message.outgoing && !message.sentLocally)
flags = (PurpleMessageFlags) (flags | PURPLE_MESSAGE_REMOTE_SEND);
std::string newText;
if (text) {
if (message.repliedMessageId.valid())
newText = quoteMessage(message.repliedMessage.get(), account);
if (!message.forwardedFrom.empty()) {
if (!newText.empty())
newText += "\n";
// TRANSLATOR: In-chat notification of forward. Argument will be a username. Please preserve the HTML.
newText += formatMessage(_("<b>Forwarded from {}:</b>"), message.forwardedFrom);
}
if (!newText.empty())
newText += "\n";
newText += text;
}
if (!newText.empty())
text = newText.c_str();
const td::td_api::user *privateUser = account.getUserByPrivateChat(chat);
if (privateUser) {
std::string userName = getPurpleBuddyName(*privateUser);
// If there is no buddy in the buddy list, libpurple won't be able to translate buddy name
// to alias, so use display name instead of idXXXXXXXXX
if (!purple_find_buddy(account.purpleAccount, userName.c_str()))
userName = account.getDisplayName(*privateUser);
showMessageTextIm(account, userName.c_str(), text, notification, message.timestamp, flags);
}
SecretChatId secretChatId = getSecretChatId(chat);
if (secretChatId.valid()) {
std::string userName = getSecretChatBuddyName(secretChatId);
showMessageTextIm(account, userName.c_str(), text, notification, message.timestamp, flags);
}
if (getBasicGroupId(chat).valid() || getSupergroupId(chat).valid())
showMessageTextChat(account, chat, message, text, notification, flags);
}
void showChatNotification(TdAccountData &account, const td::td_api::chat &chat,
const char *notification, time_t timestamp, PurpleMessageFlags extraFlags)
{
TgMessageInfo messageInfo;
messageInfo.type = TgMessageInfo::Type::Other;
messageInfo.timestamp = timestamp;
messageInfo.outgoing = true;
showMessageText(account, chat, messageInfo, NULL, notification, extraFlags);
}
void showChatNotification(TdAccountData &account, const td::td_api::chat &chat,
const char *notification, PurpleMessageFlags extraFlags)
{
showChatNotification(account, chat, notification,
(extraFlags & PURPLE_MESSAGE_NO_LOG) ? 0 : time(NULL), extraFlags);
}
2020-10-12 19:48:44 +02:00
static void showDownloadedImage(const td::td_api::chat &chat, TgMessageInfo &message,
const std::string &filePath, const char *caption,
TdAccountData &account)
{
std::string text;
std::string notice;
gchar *data = NULL;
size_t len = 0;
if (g_file_get_contents (filePath.c_str(), &data, &len, NULL)) {
int id = purple_imgstore_add_with_id (data, len, NULL);
text = makeInlineImageText(id);
} else if (filePath.find('"') == std::string::npos)
text = "<img src=\"file://" + filePath + "\">";
else {
// Unlikely error, not worth translating
notice = makeNoticeWithSender(chat, message, "Cannot show photo: file path contains quotes",
account.purpleAccount);
}
if (caption && *caption) {
if (!text.empty())
text += "\n";
text += caption;
}
showMessageText(account, chat, message, text.empty() ? NULL : text.c_str(),
notice.empty() ? NULL : notice.c_str(), PURPLE_MESSAGE_IMAGES);
}
bool isStickerAnimated(const std::string &filePath)
2020-10-12 19:48:44 +02:00
{
return (filePath.size() >= 4) && !strcmp(filePath.c_str() + filePath.size() - 4, ".tgs");
}
bool shouldConvertAnimatedSticker(const TgMessageInfo &message, const PurpleAccount *purpleAccount)
{
#ifndef NoLottie
return !message.outgoing &&
purple_account_get_bool(purpleAccount, AccountOptions::AnimatedStickers,
AccountOptions::AnimatedStickersDefault);
#else
return false;
#endif
2020-10-12 19:48:44 +02:00
}
static void showDownloadedSticker(const td::td_api::chat &chat, TgMessageInfo &message,
const std::string &filePath,
const std::string &fileDescription,
td::td_api::object_ptr<td::td_api::file> thumbnail,
TdTransceiver &transceiver, TdAccountData &account)
{
if (isStickerAnimated(filePath)) {
if (shouldConvertAnimatedSticker(message, account.purpleAccount)) {
2020-10-12 19:48:44 +02:00
// TRANSLATOR: In-chat status update
std::string notice = makeNoticeWithSender(chat, message, _("Converting sticker"),
account.purpleAccount);
showMessageText(account, chat, message, NULL, notice.c_str());
StickerConversionThread *thread;
thread = new StickerConversionThread(account.purpleAccount, filePath, getId(chat),
std::move(message));
thread->startThread();
} else if (thumbnail) {
// Avoid message like "Downloading sticker thumbnail...
// Also ignore size limits, but only determined testers and crazy people would notice.
if (thumbnail->local_ && thumbnail->local_->is_downloading_completed_)
showDownloadedSticker(chat, message, thumbnail->local_->path_,
fileDescription, nullptr, transceiver, account);
else
downloadFileInline(thumbnail->id_, getId(chat), message, fileDescription, nullptr,
transceiver, account);
} else {
showGenericFileInline(chat, message, filePath, NULL, fileDescription, account);
2020-10-12 19:48:44 +02:00
}
} else {
showWebpSticker(chat, message, filePath, fileDescription, account);
}
}
void showGenericFileInline(const td::td_api::chat &chat, const TgMessageInfo &message,
const std::string &filePath, const char *caption,
const std::string &fileDescription, TdAccountData &account)
{
if (filePath.find('"') != std::string::npos) {
std::string notice = makeNoticeWithSender(chat, message, "Cannot show file: path contains quotes",
account.purpleAccount);
showMessageText(account, chat, message, caption, notice.c_str());
} else {
std::string text = "<a href=\"file://" + filePath + "\">" + fileDescription + "</a>";
if (caption && *caption) {
text += "\n";
text += caption;
}
showMessageText(account, chat, message, text.c_str(), NULL);
}
}
2020-10-12 19:48:44 +02:00
void showDownloadedFileInline(ChatId chatId, TgMessageInfo &message,
const std::string &filePath, const char *caption,
const std::string &fileDescription,
td::td_api::object_ptr<td::td_api::file> thumbnail,
TdTransceiver &transceiver, TdAccountData &account)
{
const td::td_api::chat *chat = account.getChat(chatId);
if (!chat) return;
switch (message.type) {
case TgMessageInfo::Type::Photo:
showDownloadedImage(*chat, message, filePath, caption, account);
break;
case TgMessageInfo::Type::Sticker:
showDownloadedSticker(*chat, message, filePath, fileDescription, std::move(thumbnail),
transceiver, account);
break;
case TgMessageInfo::Type::Other:
showGenericFileInline(*chat, message, filePath, caption, fileDescription, account);
2020-10-12 19:48:44 +02:00
break;
}
}
static void showTextMessage(const td::td_api::chat &chat, const TgMessageInfo &message,
const td::td_api::messageText &text, TdAccountData &account)
{
if (text.text_) {
std::string displayText = getMessageText(*text.text_);
showMessageText(account, chat, message, displayText.c_str(), NULL);
}
}
struct InlineDownloadInfo {
int32_t fileId;
ChatId chatId;
TgMessageInfo message;
std::string fileDescription;
TdTransceiver *transceiver;
TdAccountData *account;
};
static void startInlineDownload(void *user_data)
{
std::unique_ptr<InlineDownloadInfo> info(static_cast<InlineDownloadInfo *>(user_data));
downloadFileInline(info->fileId, info->chatId, info->message, info->fileDescription,
nullptr, *info->transceiver, *info->account);
}
static void ignoreInlineDownload(InlineDownloadInfo *info)
{
delete info;
}
static void requestInlineDownload(const char *sender, const td::td_api::file &file,
const std::string &fileDesc, const td::td_api::chat &chat,
TgMessageInfo &message, TdTransceiver &transceiver, TdAccountData &account)
{
// TRANSLATOR: Download dialog, primary content, argument will be a username.
std::string question = formatMessage(_("Download file from {}?"),
getSenderDisplayName(chat, message, account.purpleAccount));
unsigned size = getFileSize(file);
// This dialog is used for files larger than the limit, so size should be non-zero
char * sizeStr = purple_str_size_to_units(size);
// TRANSLATOR: Download dialog, placeholder chat title, in the sentence "posted in a private chat".
std::string chatName = isPrivateChat(chat) ? _("a private chat") : chat.title_;
// TRANSLATOR: Download dialog, secondary content. Arguments will be file description (text), chat name (text), and a file size (text!)
std::string fileInfo = formatMessage(_("{0} posted in {1}, size: {2}"), {fileDesc,
chatName, std::string(sizeStr)});
g_free(sizeStr);
InlineDownloadInfo *info = new InlineDownloadInfo;
info->fileId = file.id_;
info->chatId = getId(chat);
info->message = std::move(message);
info->fileDescription = fileDesc;
info->transceiver = &transceiver;
info->account = &account;
// TRANSLATOR: Download dialog, title
purple_request_action(purple_account_get_connection(account.purpleAccount), _("Download"), question.c_str(),
fileInfo.c_str(), 0, account.purpleAccount, NULL, NULL,
// TRANSLATOR: Download dialog, alternative is "_No"
info, 2, _("_Yes"), startInlineDownload,
// TRANSLATOR: Download dialog, alternative is "_Yes"
_("_No"), ignoreInlineDownload);
}
static void showFileInline(const td::td_api::chat &chat, IncomingMessage &fullMessage,
const td::td_api::file &file, const char *caption,
const std::string &fileDesc,
TdTransceiver &transceiver, TdAccountData &account)
{
std::string notice;
bool askDownload = false;
bool autoDownload = false;
unsigned fileSize = getFileSizeKb(file);
if (caption && (*caption == '\0'))
caption = NULL;
if (file.local_ && file.local_->is_downloading_completed_) {
autoDownload = true;
notice.clear();
} else if (isSizeWithinLimit(fileSize, fullMessage.inlineFileSizeLimit)) {
// Sticker with a caption is not a thing but just in case it was: don't skip "Downloading..."
// notification for such hypothetical object because it will also include caption
if ( !((fullMessage.messageInfo.type == TgMessageInfo::Type::Sticker) && !caption) &&
!fullMessage.inlineDownloadComplete )
{
// TRANSLATOR: In-chat notification, appears after a colon (':'). Argument is a file *type*, not a filename.
notice = formatMessage(_("Downloading {}"), std::string(fileDesc));
}
autoDownload = true;
} else if (!ignoreBigDownloads(account.purpleAccount)) {
// TRANSLATOR: In-chat notification, appears after a colon (':'). Argument is a file *type*, not a filename.
notice = formatMessage(_("Requesting {} download"), std::string(fileDesc));
askDownload = true;
} else {
char *fileSizeStr = purple_str_size_to_units(fileSize); // File size above limit, so it's non-zero
// TRANSLATOR: In-chat notification, appears after a colon (':'). Arguments are a file *type*, not a filename; second argument is a file size with unit.
notice = formatMessage(_("Ignoring {0} download ({1})"), {std::string(fileDesc), std::string(fileSizeStr)});
g_free(fileSizeStr);
}
if (!notice.empty())
notice = makeNoticeWithSender(chat, fullMessage.messageInfo, notice.c_str(),
account.purpleAccount);
// Notice means file isn't downloaded yet or is ignored. Either way, show caption as well.
if (!notice.empty())
showMessageText(account, chat, fullMessage.messageInfo, caption, notice.c_str());
if (autoDownload || askDownload) {
if (fullMessage.animatedStickerConverted) {
if (fullMessage.animatedStickerConvertSuccess) {
std::string text = makeInlineImageText(fullMessage.animatedStickerImageId);
showMessageText(account, chat, fullMessage.messageInfo, text.c_str(), NULL, PURPLE_MESSAGE_IMAGES);
}
} else if (file.local_ && file.local_->is_downloading_completed_)
showDownloadedFileInline(getId(chat), fullMessage.messageInfo, file.local_->path_,
caption, fileDesc, std::move(fullMessage.thumbnail), transceiver, account);
else if (autoDownload && fullMessage.inlineDownloadComplete)
showDownloadedFileInline(getId(chat), fullMessage.messageInfo, fullMessage.inlineDownloadedFilePath,
caption, fileDesc, std::move(fullMessage.thumbnail), transceiver, account);
else if (autoDownload) {
// When download takes too long, message will leave PendingMessageQueue and be "shown".
// However, nothing more should be done at that point except keep waiting for the download.
if (!fullMessage.inlineDownloadTimeout) {
purple_debug_misc(config::pluginId, "Downloading %s (file id %d)\n", fileDesc.c_str(),
(int)file.id_);
downloadFileInline(file.id_, getId(chat), fullMessage.messageInfo, fileDesc,
std::move(fullMessage.thumbnail), transceiver, account);
}
} else if (askDownload) {
std::string sender = getSenderDisplayName(chat, fullMessage.messageInfo,
account.purpleAccount);
requestInlineDownload(sender.c_str(), file, fileDesc, chat, fullMessage.messageInfo,
transceiver, account);
}
}
}
static void showPhotoMessage(const td::td_api::chat &chat, IncomingMessage &fullMessage,
const td::td_api::file *photoSize, const std::string &caption,
TdTransceiver &transceiver, TdAccountData &account)
{
const char *captionCstr = !caption.empty() ? caption.c_str() : nullptr;
if (photoSize) {
// TRANSLATOR: File-type, used to describe what is being downloaded, in sentences like "Downloading photo" or "Ignoring photo download".
showFileInline(chat, fullMessage, *photoSize, captionCstr, _("photo"),
transceiver, account);
} else {
// Unlikely message not worth translating
std::string notice = makeNoticeWithSender(chat, fullMessage.messageInfo, "Faulty image",
account.purpleAccount);
showMessageText(account, chat, fullMessage.messageInfo, captionCstr, notice.c_str());
}
}
static void showFileMessage(const td::td_api::chat &chat, IncomingMessage &fullMessage,
const td::td_api::file* file,
const std::string &caption,
const std::string &fileDescription,
const std::string &fileName,
TdTransceiver &transceiver, TdAccountData &account)
{
const char *captionStr = !caption.empty() ? caption.c_str() : NULL;
if (!file) {
// Unlikely message not worth translating
std::string notice = formatMessage("Faulty file: {}", fileDescription);
notice = makeNoticeWithSender(chat, fullMessage.messageInfo, notice.c_str(),
account.purpleAccount);
showMessageText(account, chat, fullMessage.messageInfo, captionStr, notice.c_str());
} else {
if ( !fullMessage.standardDownloadConfigured || !chat.type_ ||
((chat.type_->get_id() != td::td_api::chatTypePrivate::ID) &&
(chat.type_->get_id() != td::td_api::chatTypeSecret::ID)) )
{
showFileInline(chat, fullMessage, *file, captionStr, fileDescription,
transceiver, account);
} else
requestStandardDownload(getId(chat), fullMessage.messageInfo, fileName, *file,
transceiver, account);
}
}
static void showStickerMessage(const td::td_api::chat &chat, IncomingMessage &fullMessage,
td::td_api::messageSticker &stickerContent,
TdTransceiver &transceiver, TdAccountData &account)
{
if (!stickerContent.sticker_) return;
td::td_api::sticker &sticker = *stickerContent.sticker_;
if (sticker.sticker_)
// TRANSLATOR: File-type, used to describe what is being downloaded, in sentences like "Downloading photo" or "Ignoring photo download".
showFileInline(chat, fullMessage, *sticker.sticker_, NULL, _("sticker"),
transceiver, account);
}
void showMessage(const td::td_api::chat &chat, IncomingMessage &fullMessage,
TdTransceiver &transceiver, TdAccountData &account)
{
if (!fullMessage.message) return;
td::td_api::message &message = *fullMessage.message;
if (!message.content_)
return;
purple_debug_misc(config::pluginId, "Displaying message %" G_GINT64_FORMAT "\n", message.id_);
TgMessageInfo &messageInfo = fullMessage.messageInfo;
messageInfo.repliedMessage = std::move(fullMessage.repliedMessage);
if (message.ttl_ != 0) {
// TRANSLATOR: In-chat warning message
const char *text = _("Received self-destructing message, not displayed due to lack of support");
std::string notice = makeNoticeWithSender(chat, messageInfo, text, account.purpleAccount);
showMessageText(account, chat, messageInfo, NULL, notice.c_str());
return;
}
FileInfo fileInfo;
getFileFromMessage(fullMessage, fileInfo);
if (fileInfo.secret) {
// TRANSLATOR: In-chat warning message
std::string notice = formatMessage("Ignoring secret file ({})", fileInfo.description);
notice = makeNoticeWithSender(chat, messageInfo, notice.c_str(), account.purpleAccount);
showMessageText(account, chat, messageInfo, !fileInfo.caption.empty() ? fileInfo.caption.c_str() : nullptr,
notice.c_str());
return;
}
switch (message.content_->get_id()) {
case td::td_api::messageText::ID:
showTextMessage(chat, messageInfo, static_cast<const td::td_api::messageText &>(*message.content_),
account);
break;
case td::td_api::messagePhoto::ID:
showPhotoMessage(chat, fullMessage, fileInfo.file, fileInfo.caption, transceiver, account);
break;
case td::td_api::messageDocument::ID:
case td::td_api::messageVideo::ID:
case td::td_api::messageAnimation::ID:
case td::td_api::messageAudio::ID:
case td::td_api::messageVoiceNote::ID:
case td::td_api::messageVideoNote::ID:
showFileMessage(chat, fullMessage, fileInfo.file, fileInfo.caption, fileInfo.description,
fileInfo.name, transceiver, account);
break;
case td::td_api::messageSticker::ID:
showStickerMessage(chat, fullMessage, static_cast<td::td_api::messageSticker &>(*message.content_),
transceiver, account);
break;
case td::td_api::messageChatChangeTitle::ID: {
const auto &titleChange = static_cast<const td::td_api::messageChatChangeTitle &>(*message.content_);
// TRANSLATOR: In-chat status update, arguments are chat names.
std::string notice = formatMessage(_("{0} changed group name to {1}"),
{getSenderDisplayName(chat, messageInfo, account.purpleAccount),
titleChange.title_});
showMessageText(account, chat, messageInfo, NULL, notice.c_str());
break;
}
case td::td_api::messageCall::ID:
showCallMessage(chat, messageInfo, static_cast<td::td_api::messageCall &>(*message.content_), account);
break;
default: {
// TRANSLATOR: In-chat error message, argument will be a Telegram type.
std::string notice = getUnsupportedMessageDescription(*message.content_);
notice = makeNoticeWithSender(chat, messageInfo, notice.c_str(), account.purpleAccount);
showMessageText(account, chat, messageInfo, NULL, notice.c_str());
}
}
// Put it back - may be needed if long download is in progress
fullMessage.repliedMessage = std::move(messageInfo.repliedMessage);
}
void showMessages(std::vector<IncomingMessage>& messages, TdAccountData &account)
{
for (IncomingMessage &readyMessage: messages) {
if (!readyMessage.message) continue;
const td::td_api::chat *chat = account.getChat(getChatId(*readyMessage.message));
if (chat)
showMessage(*chat, readyMessage, account.transceiver, account);
}
}
const td::td_api::file *selectPhotoSize(PurpleAccount *account, const td::td_api::messagePhoto &photo)
{
unsigned sizeLimit = getAutoDownloadLimitKb(account);
const td::td_api::photoSize *selectedSize = nullptr;
bool selectedFileSize = 0;
if (photo.photo_)
for (const auto &newSize: photo.photo_->sizes_)
if (newSize && newSize->photo_) {
unsigned fileSize = getFileSizeKb(*newSize->photo_);
bool isWithinLimit = isSizeWithinLimit(fileSize, sizeLimit);
bool selectedWithinLimit = isSizeWithinLimit(selectedFileSize, sizeLimit);
if (!selectedSize ||
(!selectedWithinLimit && (isWithinLimit || (fileSize < selectedFileSize))) ||
(selectedWithinLimit && isWithinLimit && (newSize->width_ > selectedSize->width_)))
{
selectedSize = newSize.get();
selectedFileSize = fileSize;
}
}
if (selectedSize)
purple_debug_misc(config::pluginId, "Selected size %dx%d for photo\n",
(int)selectedSize->width_, (int)selectedSize->height_);
else
purple_debug_warning(config::pluginId, "No file found for a photo\n");
return selectedSize ? selectedSize->photo_.get() : nullptr;
}
void makeFullMessage(const td::td_api::chat &chat, td::td_api::object_ptr<td::td_api::message> message,
IncomingMessage &fullMessage, const TdAccountData &account)
{
if (!message) {
fullMessage.message = nullptr;
return;
}
fullMessage.repliedMessage = nullptr;
fullMessage.selectedPhotoSizeId = 0;
fullMessage.repliedMessageFetchDoneOrFailed = false;
fullMessage.inlineDownloadComplete = false;
fullMessage.inlineDownloadTimeout = false;
fullMessage.animatedStickerConverted = false;
fullMessage.animatedStickerConvertSuccess = false;
fullMessage.animatedStickerImageId = 0;
const char *option = purple_account_get_string(account.purpleAccount, AccountOptions::DownloadBehaviour,
AccountOptions::DownloadBehaviourDefault());
fullMessage.standardDownloadConfigured = (strcmp(option, AccountOptions::DownloadBehaviourHyperlink) != 0);
fullMessage.inlineFileSizeLimit = getAutoDownloadLimitKb(account.purpleAccount);
TgMessageInfo &messageInfo = fullMessage.messageInfo;
messageInfo.id = getId(*message);
messageInfo.type = TgMessageInfo::Type::Other;
messageInfo.incomingGroupchatSender = getIncomingGroupchatSenderPurpleName(chat, *message, account);
messageInfo.timestamp = message->date_;
messageInfo.outgoing = message->is_outgoing_;
messageInfo.sentLocally = (message->sending_state_ != nullptr);
messageInfo.repliedMessageId = getReplyMessageId(*message);
if (message->forward_info_)
messageInfo.forwardedFrom = getForwardSource(*message->forward_info_, account);
if (message && message->content_) {
if (message->content_->get_id() == td::td_api::messagePhoto::ID) {
messageInfo.type = TgMessageInfo::Type::Photo;
const td::td_api::messagePhoto &photo = static_cast<const td::td_api::messagePhoto &>(*message->content_);
const td::td_api::file *file = selectPhotoSize(account.purpleAccount, photo);
if (file)
fullMessage.selectedPhotoSizeId = file->id_;
} else if (message->content_->get_id() == td::td_api::messageSticker::ID) {
messageInfo.type = TgMessageInfo::Type::Sticker;
td::td_api::messageSticker &sticker = static_cast<td::td_api::messageSticker &>(*message->content_);
if (sticker.sticker_ && sticker.sticker_->thumbnail_) {
fullMessage.thumbnail = std::move(sticker.sticker_->thumbnail_->photo_);
}
}
}
fullMessage.message = std::move(message);
}
static const td::td_api::file *getSelectedPhotoSize(const IncomingMessage &fullMessage,
const td::td_api::messagePhoto &photo)
{
if (photo.photo_)
for (const auto &newSize: photo.photo_->sizes_)
if (newSize && newSize->photo_ && (newSize->photo_->id_ == fullMessage.selectedPhotoSizeId))
return newSize->photo_.get();
return nullptr;
}
static bool isInlineDownload(const IncomingMessage &fullMessage,
const td::td_api::MessageContent &content,
const td::td_api::chat &chat)
{
return (content.get_id() == td::td_api::messagePhoto::ID) ||
(content.get_id() == td::td_api::messageSticker::ID) ||
!fullMessage.standardDownloadConfigured || !chat.type_ ||
((chat.type_->get_id() != td::td_api::chatTypePrivate::ID) &&
(chat.type_->get_id() != td::td_api::chatTypeSecret::ID));
}
static bool inlineDownloadNeedAutoDl(const IncomingMessage &fullMessage,
const td::td_api::file &file)
{
unsigned fileSize = getFileSizeKb(file);
return !((file.local_ && file.local_->is_downloading_completed_)) &&
isSizeWithinLimit(fileSize, fullMessage.inlineFileSizeLimit);
}
static bool isFileMessageReady(const IncomingMessage &fullMessage, ChatId chatId,
const td::td_api::MessageContent &content,
const td::td_api::file &file, const TdAccountData &account)
{
const td::td_api::chat *chat = account.getChat(chatId);
if (chat && isInlineDownload(fullMessage, content, *chat)) {
// File will be shown inline
// Animated stickers are not ready until converted
if (fullMessage.inlineDownloadComplete)
return !((content.get_id() == td::td_api::messageSticker::ID) &&
isStickerAnimated(fullMessage.inlineDownloadedFilePath) &&
shouldConvertAnimatedSticker(fullMessage.messageInfo, account.purpleAccount) &&
!fullMessage.animatedStickerConverted);
else if (file.local_ && file.local_->is_downloading_completed_)
return !((content.get_id() == td::td_api::messageSticker::ID) &&
isStickerAnimated(file.local_->path_) &&
shouldConvertAnimatedSticker(fullMessage.messageInfo, account.purpleAccount) &&
!fullMessage.animatedStickerConverted);
else
// Files above limit will either be ignored (in which case, message is ready)
// or requested (in which case, don't try do display in order)
return fullMessage.inlineDownloadTimeout || !inlineDownloadNeedAutoDl(fullMessage, file);
} else
// Standard libpurple transfer will be used, nothing to postpone
return true;
}
void getFileFromMessage(const IncomingMessage &fullMessage, FileInfo &result)
{
result.file = nullptr;
result.caption = "";
result.secret = false;
if (!fullMessage.message || !fullMessage.message->content_)
return;
const td::td_api::message &message = *fullMessage.message;
switch (message.content_->get_id()) {
case td::td_api::messagePhoto::ID: {
const td::td_api::messagePhoto &photo = static_cast<const td::td_api::messagePhoto &>(*message.content_);
result.file = getSelectedPhotoSize(fullMessage, photo);
result.name = ""; // will not be needed - inline download only
if (photo.caption_) result.caption = photo.caption_->text_;
// TRANSLATOR: File-type, used to describe what is being downloaded, in sentences like "Downloading photo" or "Ignoring photo download".
result.description = _("photo");
result.secret = photo.is_secret_;
break;
}
case td::td_api::messageDocument::ID: {
const td::td_api::messageDocument &document = static_cast<const td::td_api::messageDocument &>(*message.content_);
result.file = document.document_ ? document.document_->document_.get() : nullptr;
if (document.caption_) result.caption = document.caption_->text_;
result.name = getFileName(document.document_.get());
result.description = makeDocumentDescription(document.document_.get());
break;
}
case td::td_api::messageVideo::ID: {
const td::td_api::messageVideo &video = static_cast<const td::td_api::messageVideo &>(*message.content_);
result.file = video.video_ ? video.video_->video_.get() : nullptr;
if (video.caption_) result.caption = video.caption_->text_;
result.name = getFileName(video.video_.get());
result.description = makeDocumentDescription(video.video_.get());
result.secret = video.is_secret_;
break;
}
case td::td_api::messageAnimation::ID: {
const td::td_api::messageAnimation &animation = static_cast<const td::td_api::messageAnimation &>(*message.content_);
result.file = animation.animation_ ? animation.animation_->animation_.get() : nullptr;
if (animation.caption_) result.caption = animation.caption_->text_;
result.name = getFileName(animation.animation_.get());
result.description = makeDocumentDescription(animation.animation_.get());
result.secret = animation.is_secret_;
break;
}
case td::td_api::messageAudio::ID: {
const td::td_api::messageAudio &audio = static_cast<const td::td_api::messageAudio &>(*message.content_);
result.file = audio.audio_ ? audio.audio_->audio_.get() : nullptr;
if (audio.caption_) result.caption = audio.caption_->text_;
result.name = getFileName(audio.audio_.get());
result.description = makeDocumentDescription(audio.audio_.get());
break;
}
case td::td_api::messageVoiceNote::ID: {
const td::td_api::messageVoiceNote &audio = static_cast<const td::td_api::messageVoiceNote &>(*message.content_);
result.file = audio.voice_note_ ? audio.voice_note_->voice_.get() : nullptr;
if (audio.caption_) result.caption = audio.caption_->text_;
result.name = getFileName(audio.voice_note_.get());
result.description = makeDocumentDescription(audio.voice_note_.get());
break;
}
case td::td_api::messageVideoNote::ID: {
const td::td_api::messageVideoNote &video = static_cast<const td::td_api::messageVideoNote &>(*message.content_);
result.file = video.video_note_ ? video.video_note_->video_.get() : nullptr;
result.name = getFileName(video.video_note_.get());
result.description = makeDocumentDescription(video.video_note_.get());
result.secret = video.is_secret_;
break;
}
case td::td_api::messageSticker::ID: {
const td::td_api::messageSticker &sticker = static_cast<const td::td_api::messageSticker &>(*message.content_);
result.file = sticker.sticker_ ? sticker.sticker_->sticker_.get() : nullptr;
result.name = ""; // will not be needed - inline download only
// TRANSLATOR: File-type, used to describe what is being downloaded, in sentences like "Downloading photo" or "Ignoring photo download".
result.description = _("sticker");
break;
}
}
}
bool isMessageReady(const IncomingMessage &fullMessage, const TdAccountData &account)
{
if (!fullMessage.message) return true;
const td::td_api::message &message = *fullMessage.message;
ChatId chatId = getChatId(message);
if (getReplyMessageId(message).valid() && !fullMessage.repliedMessageFetchDoneOrFailed)
{
return false;
}
if (message.content_)
{
FileInfo fileInfo;
getFileFromMessage(fullMessage, fileInfo);
// For stickers, will wait for sticker download to display the message, but not for
// animated sticker conversion
if (fileInfo.file && !isFileMessageReady(fullMessage, chatId, *message.content_,
*fileInfo.file, account))
{
return false;
}
}
return true;
}
void fetchExtras(IncomingMessage &fullMessage, TdTransceiver &transceiver, TdAccountData &account,
TdTransceiver::ResponseCb2 onFetchReply)
{
if (!fullMessage.message) return;
const td::td_api::message &message = *fullMessage.message;
MessageId messageId = getId(message);
MessageId replyMessageId = getReplyMessageId(message);
ChatId chatId = getChatId(message);
const td::td_api::chat *chat = account.getChat(chatId);
if (replyMessageId.valid()) {
purple_debug_misc(config::pluginId, "Fetching message %" G_GINT64_FORMAT " which message %" G_GINT64_FORMAT " replies to\n",
replyMessageId.value(), messageId.value());
auto getMessageReq = td::td_api::make_object<td::td_api::getMessage>();
getMessageReq->chat_id_ = chatId.value();
getMessageReq->message_id_ = replyMessageId.value();
transceiver.sendQueryWithTimeout(std::move(getMessageReq), onFetchReply, 1);
}
FileInfo fileInfo;
getFileFromMessage(fullMessage, fileInfo);
if (fileInfo.file && message.content_ && chat && isInlineDownload(fullMessage, *message.content_, *chat)) {
if (fileInfo.file->local_ && fileInfo.file->local_->is_downloading_completed_ &&
(message.content_->get_id() == td::td_api::messageSticker::ID) &&
isStickerAnimated(fileInfo.file->local_->path_))
{
if (shouldConvertAnimatedSticker(fullMessage.messageInfo, account.purpleAccount)) {
StickerConversionThread *thread;
thread = new StickerConversionThread(account.purpleAccount, fileInfo.file->local_->path_,
chatId, &fullMessage.messageInfo);
thread->startThread();
}
// TODO: if animated stickers are disabled, fetch thumbnail instead
} else if (inlineDownloadNeedAutoDl(fullMessage, *fileInfo.file)) {
// TgMessageInfo on fullMessage has replyMessage=NULL which will be copied onto DownloadRequest.
// If message leaves PendingMessageQueue while download is still active, there's probably
// a replyMessage on IncomingMessage by then, and it needs to be moved over to DownloadRequest.
// Thumbnail also needs moving onto DownloadRequest.
downloadFileInline(fileInfo.file->id_, chatId, fullMessage.messageInfo, fileInfo.description,
nullptr, transceiver, account);
}
}
}
void checkMessageReady(const IncomingMessage *message, TdTransceiver &transceiver,
TdAccountData &account, std::vector<IncomingMessage> *rvReadyMessages)
{
if (!message || !message->message) return;
if (isMessageReady(*message, account)) {
std::vector<IncomingMessage> readyMessages;
account.pendingMessages.setMessageReady(getChatId(*message->message), getId(*message->message),
rvReadyMessages ? *rvReadyMessages : readyMessages);
message = nullptr;
showMessages(rvReadyMessages ? *rvReadyMessages : readyMessages, account);
}
}
static void findMessageResponse(TdAccountData &account, ChatId chatId, MessageId pendingMessageId,
td::td_api::object_ptr<td::td_api::Object> object)
{
IncomingMessage *pendingMessage = account.pendingMessages.findPendingMessage(chatId, pendingMessageId);
if (!pendingMessage) return;
pendingMessage->repliedMessageFetchDoneOrFailed = true;
if (object && (object->get_id() == td::td_api::message::ID))
pendingMessage->repliedMessage = td::move_tl_object_as<td::td_api::message>(object);
else
purple_debug_misc(config::pluginId, "Failed to fetch reply source for message %" G_GINT64_FORMAT "\n",
pendingMessageId.value());
checkMessageReady(pendingMessage, account.transceiver, account);
}
void handleIncomingMessage(TdAccountData &account, const td::td_api::chat &chat,
td::td_api::object_ptr<td::td_api::message> message,
PendingMessageQueue::MessageAction action)
{
if (!message) return;
ChatId chatId = getId(chat);
if (isReadReceiptsEnabled(account.purpleAccount))
account.addPendingReadReceipt(chatId, getId(*message));
IncomingMessage fullMessage;
makeFullMessage(chat, std::move(message), fullMessage, account);
if (isMessageReady(fullMessage, account)) {
2021-01-03 02:01:05 +01:00
IncomingMessage readyMessage = account.pendingMessages.addReadyMessage(std::move(fullMessage), action);
if (readyMessage.message)
showMessage(chat, readyMessage, account.transceiver, account);
} else {
MessageId messageId = getId(*fullMessage.message);
2021-01-03 02:01:05 +01:00
IncomingMessage &addedMessage = account.pendingMessages.addPendingMessage(std::move(fullMessage), action);
fetchExtras(addedMessage, account.transceiver, account,
[&account, chatId, messageId](uint64_t, td::td_api::object_ptr<td::td_api::Object> object) {
findMessageResponse(account, chatId, messageId, std::move(object));
}
);
}
}
static void fetchHistoryRequest(TdAccountData &account, ChatId chatId, unsigned messagesFetched,
MessageId fetchBackFrom, MessageId lastReceivedMessage);
static void fetchHistoryResponse(TdAccountData &account, ChatId chatId, MessageId stopAt,
unsigned messagesFetched,
td::td_api::object_ptr<td::td_api::Object> response)
{
MessageId requestMoreFrom = MessageId::invalid;
const td::td_api::chat *chat = account.getChat(chatId);
if (response && (response->get_id() == td::td_api::messages::ID)) {
td::td_api::messages &messages = static_cast<td::td_api::messages &>(*response);
purple_debug_misc(config::pluginId, "Fetched %zu messages for chat %" G_GINT64_FORMAT "\n",
messages.messages_.size(), chatId.value());
auto stop = messages.messages_.begin();
MessageId lastMessageId = MessageId::invalid;
for (; stop != messages.messages_.end(); ++stop) {
td::td_api::object_ptr<td::td_api::message> message = std::move(*stop);
if (!message) {
purple_debug_warning(config::pluginId, "Erroneous message in history, stopping\n");
break;
}
if (stopAt.valid() && (getId(*message) == stopAt)) {
purple_debug_misc(config::pluginId, "Found message %" G_GINT64_FORMAT ", stopping\n",
stopAt.value());
break;
}
if ((!stopAt.valid() && (messagesFetched == 100)) ||
(messagesFetched == HISTORY_MESSAGES_ABSOLUTE_LIMIT))
{
purple_debug_misc(config::pluginId, "Reached history limit, stopping\n");
break;
}
messagesFetched++;
lastMessageId = getId(*message);
if (chat)
handleIncomingMessage(account, *chat, std::move(message), PendingMessageQueue::Prepend);
}
if (stop == messages.messages_.end())
requestMoreFrom = lastMessageId;
} else {
std::string message = formatMessage(_("Failed to fetch earlier messages: {}"),
getDisplayedError(response));
purple_debug_warning(config::pluginId, "%s\n", message.c_str());
if (chat)
showChatNotification(account, *chat, message.c_str(), PURPLE_MESSAGE_ERROR);
}
if (requestMoreFrom.valid())
fetchHistoryRequest(account, chatId, messagesFetched, requestMoreFrom, stopAt);
else {
purple_debug_misc(config::pluginId, "Done fetching history for chat %" G_GINT64_FORMAT "\n",
chatId.value());
std::vector<IncomingMessage> readyMessages;
account.pendingMessages.setChatReady(chatId, readyMessages);
showMessages(readyMessages, account);
}
}
static void fetchHistoryRequest(TdAccountData &account, ChatId chatId, unsigned messagesFetched,
MessageId fetchBackFrom, MessageId stopAt)
{
auto request = td::td_api::make_object<td::td_api::getChatHistory>();
request->chat_id_ = chatId.value();
request->from_message_id_ = fetchBackFrom.valid() ? fetchBackFrom.value() : 0;
request->limit_ = 30;
request->offset_ = 0;
request->only_local_ = false;
purple_debug_misc(config::pluginId, "Requesting history for chat %" G_GINT64_FORMAT
" starting from %" G_GINT64_FORMAT "\n", chatId.value(), fetchBackFrom.value());
account.transceiver.sendQuery(std::move(request),
[&account, chatId, messagesFetched, stopAt](uint64_t requestId, td::td_api::object_ptr<td::td_api::Object> response) {
fetchHistoryResponse(account, chatId, stopAt, messagesFetched, std::move(response));
});
}
void fetchHistory(TdAccountData &account, ChatId chatId, MessageId fetchFrom, MessageId stopAt)
{
if (!account.pendingMessages.isChatReady(chatId))
return;
account.pendingMessages.setChatNotReady(chatId);
fetchHistoryRequest(account, chatId, 0, fetchFrom, stopAt);
}