mirror of
https://github.com/AyuGram/AyuGramDesktop.git
synced 2026-07-26 23:42:09 +00:00
987 lines
23 KiB
C++
987 lines
23 KiB
C++
// This is the source code of AyuGram for Desktop.
|
|
//
|
|
// We do not and cannot prevent the use of our code,
|
|
// but be respectful and credit the original author.
|
|
//
|
|
// Copyright @Radolyn, 2025
|
|
#include "telegram_helpers.h"
|
|
|
|
#include <functional>
|
|
#include <latch>
|
|
#include <QTimer>
|
|
|
|
#include "apiwrap.h"
|
|
|
|
#include "lang_auto.h"
|
|
#include "rc_manager.h"
|
|
#include "ayu/ayu_worker.h"
|
|
#include "ayu/data/entities.h"
|
|
#include "core/mime_type.h"
|
|
#include "data/data_channel.h"
|
|
#include "data/data_forum.h"
|
|
#include "data/data_forum_topic.h"
|
|
#include "data/data_histories.h"
|
|
#include "data/data_peer_id.h"
|
|
#include "data/data_photo.h"
|
|
#include "data/data_user.h"
|
|
|
|
#include "data/data_document.h"
|
|
#include "data/data_session.h"
|
|
#include "history/history.h"
|
|
#include "history/history_item.h"
|
|
#include "history/history_item_components.h"
|
|
#include "history/history_unread_things.h"
|
|
#include "main/main_account.h"
|
|
#include "main/main_session.h"
|
|
#include "ui/text/format_values.h"
|
|
|
|
#include "ayu/ayu_settings.h"
|
|
#include "ayu/ayu_state.h"
|
|
#include "ayu/data/messages_storage.h"
|
|
#include "ayu/features/filters/filters_controller.h"
|
|
#include "data/data_chat.h"
|
|
#include "data/data_poll.h"
|
|
#include "data/data_saved_sublist.h"
|
|
#include "lang/lang_text_entity.h"
|
|
#include "main/main_domain.h"
|
|
#include "styles/style_ayu_styles.h"
|
|
#include "ui/text/text_utilities.h"
|
|
#include "ui/toast/toast.h"
|
|
|
|
#include "unicode/regex.h"
|
|
|
|
namespace {
|
|
|
|
constexpr auto usernameResolverBotId = 7424190611L;
|
|
const auto usernameResolverBotUsername = QString("tgdb_search_bot");
|
|
const auto usernameResolverEmpty = QString("Error, username or id invalid/not found.");
|
|
|
|
}
|
|
|
|
Main::Session *getSession(ID userId) {
|
|
for (const auto &[index, account] : Core::App().domain().accounts()) {
|
|
if (const auto session = account->maybeSession()) {
|
|
if (session->userId().bare == userId) {
|
|
return session;
|
|
}
|
|
}
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
void dispatchToMainThread(const std::function<void()> &callback, int delay) {
|
|
auto timer = new QTimer();
|
|
timer->moveToThread(qApp->thread());
|
|
timer->setSingleShot(true);
|
|
QObject::connect(timer,
|
|
&QTimer::timeout,
|
|
[=]()
|
|
{
|
|
callback();
|
|
timer->deleteLater();
|
|
});
|
|
QMetaObject::invokeMethod(timer, "start", Qt::QueuedConnection, Q_ARG(int, delay));
|
|
}
|
|
|
|
ID getDialogIdFromPeer(not_null<PeerData*> peer) {
|
|
ID peerId = peer->id.value & PeerId::kChatTypeMask;
|
|
if (peer->isChannel() || peer->isChat()) {
|
|
peerId = -peerId;
|
|
}
|
|
|
|
return peerId;
|
|
}
|
|
|
|
ID getBareID(not_null<PeerData*> peer) {
|
|
return peer->id.value & PeerId::kChatTypeMask;
|
|
}
|
|
|
|
bool isExteraPeer(ID peerId) {
|
|
return RCManager::getInstance().developers().contains(peerId) || RCManager::getInstance().channels().
|
|
contains(peerId);
|
|
}
|
|
|
|
bool isSupporterPeer(ID peerId) {
|
|
return RCManager::getInstance().supporters().contains(peerId) || RCManager::getInstance().supporterChannels().
|
|
contains(peerId);
|
|
}
|
|
|
|
bool isCustomBadgePeer(ID peerId) {
|
|
return RCManager::getInstance().supporterCustomBadges().contains(peerId);
|
|
}
|
|
|
|
CustomBadge getCustomBadge(ID peerId) {
|
|
const auto &badges = RCManager::getInstance().supporterCustomBadges();
|
|
if (const auto it = badges.find(peerId); it != badges.end()) {
|
|
return it->second;
|
|
}
|
|
return {};
|
|
}
|
|
|
|
rpl::producer<Info::Profile::Badge::Content> ExteraBadgeTypeFromPeer(not_null<PeerData*> peer) {
|
|
if (isCustomBadgePeer(getBareID(peer))) {
|
|
return rpl::single(Info::Profile::Badge::Content{
|
|
.badge = Info::Profile::BadgeType::ExteraCustom,
|
|
.emojiStatusId = getCustomBadge(getBareID(peer)).emojiStatusId
|
|
});
|
|
} else if (isExteraPeer(getBareID(peer))) {
|
|
return rpl::single(Info::Profile::Badge::Content{
|
|
.badge = Info::Profile::BadgeType::Extera
|
|
});
|
|
} else if (isSupporterPeer(getBareID(peer))) {
|
|
return rpl::single(Info::Profile::Badge::Content{
|
|
.badge = Info::Profile::BadgeType::ExteraSupporter
|
|
});
|
|
}
|
|
return rpl::single(Info::Profile::Badge::Content{Info::Profile::BadgeType::None});
|
|
}
|
|
|
|
Fn<void()> badgeClickHandler(not_null<PeerData*> peer) {
|
|
return [=]
|
|
{
|
|
const auto isCustomBadge = isCustomBadgePeer(getBareID(peer));
|
|
const auto isExtera = isExteraPeer(getBareID(peer));
|
|
const auto isSupporter = isSupporterPeer(getBareID(peer));
|
|
|
|
TextWithEntities text;
|
|
if (isCustomBadge) {
|
|
const auto custom = getCustomBadge(getBareID(peer));
|
|
text = custom.text.isEmpty()
|
|
? (isExtera
|
|
? tr::ayu_DeveloperPopup(
|
|
tr::now,
|
|
lt_item,
|
|
TextWithEntities{peer->name()},
|
|
Ui::Text::RichLangValue)
|
|
: tr::ayu_SupporterPopup(
|
|
tr::now,
|
|
lt_item,
|
|
TextWithEntities{peer->name()},
|
|
Ui::Text::RichLangValue))
|
|
: Ui::Text::RichLangValue(custom.text);
|
|
} else if (isExtera) {
|
|
text = peer->isUser()
|
|
? tr::ayu_DeveloperPopup(
|
|
tr::now,
|
|
lt_item,
|
|
TextWithEntities{peer->name()},
|
|
Ui::Text::RichLangValue)
|
|
: tr::ayu_OfficialResourcePopup(
|
|
tr::now,
|
|
lt_item,
|
|
TextWithEntities{peer->name()},
|
|
Ui::Text::RichLangValue);
|
|
} else if (isSupporter) {
|
|
text = tr::ayu_SupporterPopup(
|
|
tr::now,
|
|
lt_item,
|
|
TextWithEntities{peer->name()},
|
|
Ui::Text::RichLangValue);
|
|
} else {
|
|
return;
|
|
}
|
|
|
|
Ui::Toast::Show({
|
|
.text = text,
|
|
.st = &st::exteraBadgeToast,
|
|
.adaptive = true,
|
|
.duration = 3 * crl::time(1000),
|
|
});
|
|
};
|
|
}
|
|
|
|
bool isMessageHidden(const not_null<HistoryItem*> item) {
|
|
if (AyuState::isHidden(item)) {
|
|
return true;
|
|
}
|
|
|
|
return FiltersController::filtered(item);
|
|
}
|
|
|
|
void MarkAsReadChatList(not_null<Dialogs::MainList*> list) {
|
|
auto mark = std::vector<not_null<History*>>();
|
|
for (const auto &row : list->indexed()->all()) {
|
|
if (const auto history = row->history()) {
|
|
mark.push_back(history);
|
|
}
|
|
}
|
|
ranges::for_each(mark, MarkAsReadThread);
|
|
}
|
|
|
|
void readMentions(base::weak_ptr<Data::Thread> weakThread) {
|
|
const auto thread = weakThread.get();
|
|
if (!thread) {
|
|
return;
|
|
}
|
|
const auto peer = thread->peer();
|
|
const auto topic = thread->asTopic();
|
|
const auto rootId = topic ? topic->rootId() : 0;
|
|
using Flag = MTPmessages_ReadMentions::Flag;
|
|
peer->session().api().request(MTPmessages_ReadMentions(
|
|
MTP_flags(rootId ? Flag::f_top_msg_id : Flag()),
|
|
peer->input,
|
|
MTP_int(rootId)
|
|
)).done([=](const MTPmessages_AffectedHistory &result)
|
|
{
|
|
const auto offset = peer->session().api().applyAffectedHistory(
|
|
peer,
|
|
result);
|
|
if (offset > 0) {
|
|
readMentions(weakThread);
|
|
} else {
|
|
peer->owner().history(peer)->clearUnreadMentionsFor(rootId);
|
|
}
|
|
}).send();
|
|
}
|
|
|
|
void readReactions(base::weak_ptr<Data::Thread> weakThread) {
|
|
const auto thread = weakThread.get();
|
|
if (!thread) {
|
|
return;
|
|
}
|
|
const auto topic = thread->asTopic();
|
|
const auto sublist = thread->asSublist();
|
|
const auto peer = thread->peer();
|
|
const auto rootId = topic ? topic->rootId() : 0;
|
|
using Flag = MTPmessages_ReadReactions::Flag;
|
|
peer->session().api().request(MTPmessages_ReadReactions(
|
|
MTP_flags(rootId ? Flag::f_top_msg_id : Flag(0)),
|
|
peer->input,
|
|
MTP_int(rootId),
|
|
sublist ? sublist->sublistPeer()->input : MTPInputPeer()
|
|
)).done([=](const MTPmessages_AffectedHistory &result)
|
|
{
|
|
const auto offset = peer->session().api().applyAffectedHistory(
|
|
peer,
|
|
result);
|
|
if (offset > 0) {
|
|
readReactions(weakThread);
|
|
} else {
|
|
peer->owner().history(peer)->clearUnreadReactionsFor(rootId, sublist);
|
|
}
|
|
}).send();
|
|
}
|
|
|
|
void MarkAsReadThread(not_null<Data::Thread*> thread) {
|
|
const auto readHistoryNative = [&](const not_null<History*> history)
|
|
{
|
|
history->owner().histories().readInbox(history);
|
|
};
|
|
const auto sendReadMentions = [=](
|
|
const not_null<Data::Thread*> threadInner)
|
|
{
|
|
readMentions(base::make_weak(threadInner));
|
|
};
|
|
const auto sendReadReactions = [=](
|
|
const not_null<Data::Thread*> threadInner)
|
|
{
|
|
readReactions(base::make_weak(threadInner));
|
|
};
|
|
|
|
if (thread->chatListBadgesState().unread) {
|
|
if (const auto forum = thread->asForum()) {
|
|
forum->enumerateTopics([](
|
|
not_null<Data::ForumTopic*> topic)
|
|
{
|
|
MarkAsReadThread(topic);
|
|
});
|
|
} else if (const auto topic = thread->asTopic()) {
|
|
topic->readTillEnd();
|
|
} else if (const auto history = thread->asHistory()) {
|
|
readHistoryNative(history);
|
|
if (const auto migrated = history->migrateSibling()) {
|
|
readHistoryNative(migrated);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (thread->unreadMentions().has()) {
|
|
sendReadMentions(thread);
|
|
}
|
|
|
|
if (thread->unreadReactions().has()) {
|
|
sendReadReactions(thread);
|
|
}
|
|
|
|
AyuWorker::markAsOnline(&thread->session());
|
|
}
|
|
|
|
void readHistory(not_null<HistoryItem*> message) {
|
|
const auto history = message->history();
|
|
const auto tillId = message->id;
|
|
|
|
history->session().data().histories()
|
|
.sendRequest(history,
|
|
Data::Histories::RequestType::ReadInbox,
|
|
[=](Fn<void()> finish)
|
|
{
|
|
if (const auto channel = history->peer->asChannel()) {
|
|
return history->session().api().request(MTPchannels_ReadHistory(
|
|
channel->inputChannel,
|
|
MTP_int(tillId)
|
|
)).done([=] { AyuWorker::markAsOnline(&history->session()); }).send();
|
|
}
|
|
|
|
return history->session().api().request(MTPmessages_ReadHistory(
|
|
history->peer->input,
|
|
MTP_int(tillId)
|
|
)).done([=](const MTPmessages_AffectedMessages &result)
|
|
{
|
|
history->session().api().applyAffectedMessages(history->peer, result);
|
|
AyuWorker::markAsOnline(&history->session());
|
|
}).fail([=]
|
|
{
|
|
}).send();
|
|
});
|
|
|
|
if (history->unreadMentions().has()) {
|
|
readMentions(history->asThread());
|
|
}
|
|
|
|
if (history->unreadReactions().has()) {
|
|
readReactions(history->asThread());
|
|
}
|
|
}
|
|
|
|
QString formatTTL(int time) {
|
|
if (time == 0x7FFFFFFF) {
|
|
return QString("👀 %1").arg(tr::ayu_OneViewTTL(tr::now));
|
|
}
|
|
|
|
return QString("🕓 %1s").arg(time);
|
|
}
|
|
|
|
QString getDCName(int dc) {
|
|
const auto getName = [=]
|
|
{
|
|
switch (dc) {
|
|
case 1:
|
|
case 3: return "Miami FL, USA";
|
|
case 2:
|
|
case 4: return "Amsterdam, NL";
|
|
case 5: return "Singapore, SG";
|
|
default: return "UNKNOWN";
|
|
}
|
|
};
|
|
|
|
if (dc < 1) {
|
|
return {"DC_UNKNOWN"};
|
|
}
|
|
|
|
return QString("DC%1, %2").arg(dc).arg(getName());
|
|
}
|
|
|
|
QString getLocalizedAt() {
|
|
static const auto val = tr::lng_mediaview_date_time(
|
|
tr::now,
|
|
lt_date,
|
|
"",
|
|
lt_time,
|
|
"");
|
|
return val;
|
|
}
|
|
|
|
QString formatDateTime(const QDateTime &date) {
|
|
const auto locale = QLocale::system();
|
|
const auto datePart = locale.toString(date.date(), QLocale::ShortFormat);
|
|
const auto timePart = locale.toString(date, "HH:mm:ss");
|
|
|
|
return datePart + getLocalizedAt() + timePart;
|
|
}
|
|
|
|
QString formatMessageTime(const QTime &time) {
|
|
const auto &settings = AyuSettings::getInstance();
|
|
|
|
const auto format =
|
|
settings.showMessageSeconds
|
|
? (QLocale().timeFormat(QLocale::ShortFormat).contains("AP")
|
|
? "h:mm:ss AP"
|
|
: "HH:mm:ss")
|
|
: QLocale().timeFormat(QLocale::ShortFormat);
|
|
|
|
return QLocale().toString(
|
|
time,
|
|
format
|
|
);
|
|
}
|
|
|
|
int getMediaSizeBytes(not_null<HistoryItem*> message) {
|
|
if (!message->media()) {
|
|
return -1;
|
|
}
|
|
|
|
const auto media = message->media();
|
|
|
|
const auto document = media->document();
|
|
const auto photo = media->photo();
|
|
|
|
int64 size = -1;
|
|
if (document) {
|
|
// any file
|
|
size = document->size;
|
|
} else if (photo && photo->hasVideo()) {
|
|
// video
|
|
size = photo->videoByteSize(Data::PhotoSize::Large);
|
|
if (size == 0) {
|
|
size = photo->videoByteSize(Data::PhotoSize::Small);
|
|
}
|
|
if (size == 0) {
|
|
size = photo->videoByteSize(Data::PhotoSize::Thumbnail);
|
|
}
|
|
} else if (photo && !photo->hasVideo()) {
|
|
// photo
|
|
size = photo->imageByteSize(Data::PhotoSize::Large);
|
|
if (size == 0) {
|
|
size = photo->imageByteSize(Data::PhotoSize::Small);
|
|
}
|
|
if (size == 0) {
|
|
size = photo->imageByteSize(Data::PhotoSize::Thumbnail);
|
|
}
|
|
}
|
|
|
|
return size;
|
|
}
|
|
|
|
QString getMediaSize(not_null<HistoryItem*> message) {
|
|
const auto size = getMediaSizeBytes(message);
|
|
|
|
if (size == -1) {
|
|
return {};
|
|
}
|
|
|
|
return Ui::FormatSizeText(size);
|
|
}
|
|
|
|
QString getMediaMime(not_null<HistoryItem*> message) {
|
|
if (!message->media()) {
|
|
return {};
|
|
}
|
|
|
|
const auto media = message->media();
|
|
|
|
const auto document = media->document();
|
|
const auto photo = media->photo();
|
|
|
|
if (document) {
|
|
// any file
|
|
return document->mimeString();
|
|
} else if (photo && photo->hasVideo()) {
|
|
// video
|
|
return "video/mp4";
|
|
} else if (photo && !photo->hasVideo()) {
|
|
// photo
|
|
return "image/jpeg";
|
|
}
|
|
|
|
return {};
|
|
}
|
|
|
|
QString getMediaName(not_null<HistoryItem*> message) {
|
|
if (!message->media()) {
|
|
return {};
|
|
}
|
|
|
|
const auto media = message->media();
|
|
|
|
if (const auto document = media->document()) {
|
|
return document->filename();
|
|
}
|
|
|
|
return {};
|
|
}
|
|
|
|
QString getMediaResolution(not_null<HistoryItem*> message) {
|
|
if (!message->media()) {
|
|
return {};
|
|
}
|
|
|
|
const auto media = message->media();
|
|
|
|
const auto document = media->document();
|
|
const auto photo = media->photo();
|
|
|
|
const auto formatQSize = [=](QSize size)
|
|
{
|
|
if (size.isNull() || size.isEmpty() || !size.isValid()) {
|
|
return QString();
|
|
}
|
|
|
|
return QString("%1x%2").arg(size.width()).arg(size.height());
|
|
};
|
|
|
|
if (document) {
|
|
return formatQSize(document->dimensions);
|
|
} else if (photo) {
|
|
auto result = photo->size(Data::PhotoSize::Large);
|
|
if (!result.has_value()) {
|
|
result = photo->size(Data::PhotoSize::Small);
|
|
}
|
|
if (!result.has_value()) {
|
|
result = photo->size(Data::PhotoSize::Thumbnail);
|
|
}
|
|
return result.has_value() ? formatQSize(result.value()) : QString();
|
|
}
|
|
|
|
return {};
|
|
}
|
|
|
|
QString getMediaDC(not_null<HistoryItem*> message) {
|
|
if (!message->media()) {
|
|
return {};
|
|
}
|
|
|
|
const auto media = message->media();
|
|
|
|
const auto document = media->document();
|
|
const auto photo = media->photo();
|
|
|
|
if (document) {
|
|
return getDCName(document->getDC());
|
|
} else if (photo) {
|
|
return getDCName(photo->getDC());
|
|
}
|
|
|
|
return {};
|
|
}
|
|
|
|
QString getPeerDC(not_null<PeerData*> peer) {
|
|
if (const auto statsDcId = peer->owner().statsDcId(peer)) {
|
|
return getDCName(statsDcId);
|
|
}
|
|
|
|
if (peer->hasUserpic()) {
|
|
const auto dc = v::match(
|
|
peer->userpicLocation().file().data,
|
|
[&](const StorageFileLocation &data)
|
|
{
|
|
return data.dcId();
|
|
},
|
|
[&](const WebFileLocation &)
|
|
{
|
|
// should't happen, but still
|
|
// all webpages are on DC4
|
|
return 4;
|
|
},
|
|
[&](const GeoPointLocation &)
|
|
{
|
|
// shouldn't happen naturally
|
|
return 0;
|
|
},
|
|
[&](const AudioAlbumThumbLocation &)
|
|
{
|
|
// shouldn't happen naturally
|
|
return 0;
|
|
},
|
|
[&](const PlainUrlLocation &)
|
|
{
|
|
// should't happen, but still
|
|
// all webpages are on DC4
|
|
return 4;
|
|
},
|
|
[&](const InMemoryLocation &)
|
|
{
|
|
// shouldn't happen naturally
|
|
return 0;
|
|
});
|
|
|
|
if (dc > 0) {
|
|
return getDCName(dc);
|
|
}
|
|
}
|
|
|
|
return {};
|
|
}
|
|
|
|
int getScheduleTime(int64 sumSize) {
|
|
auto time = 12;
|
|
time += (int) std::ceil(std::max(6.0, std::ceil(sumSize / 1024.0 / 1024.0 * 0.7))) + 1;
|
|
return time;
|
|
}
|
|
|
|
bool isMessageSavable(const not_null<HistoryItem*> item) {
|
|
const auto &settings = AyuSettings::getInstance();
|
|
|
|
if (!settings.saveDeletedMessages) {
|
|
return false;
|
|
}
|
|
|
|
if (const auto possiblyBot = item->history()->peer->asUser()) {
|
|
return !possiblyBot->isBot() || (settings.saveForBots && possiblyBot->isBot());
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void processMessageDelete(not_null<HistoryItem*> item) {
|
|
if (!isMessageSavable(item)) {
|
|
item->destroy();
|
|
} else {
|
|
item->setDeleted();
|
|
AyuMessages::addDeletedMessage(item);
|
|
}
|
|
}
|
|
|
|
void resolvePeer(
|
|
const QString &peerId,
|
|
const QString &username,
|
|
Main::Session *session,
|
|
const UsernameResolverCallback &callback) {
|
|
auto normalized = username.trimmed().toLower();
|
|
if (normalized.isEmpty()) {
|
|
callback(QString(), nullptr);
|
|
return;
|
|
}
|
|
normalized = normalized.startsWith("@") ? normalized.mid(1) : normalized;
|
|
|
|
if (normalized.isEmpty()) {
|
|
callback(QString(), nullptr);
|
|
return;
|
|
}
|
|
|
|
session->api().request(MTPcontacts_ResolveUsername(
|
|
MTP_flags(0),
|
|
MTP_string(normalized),
|
|
MTP_string()
|
|
)).done([=](const MTPcontacts_ResolvedPeer &result)
|
|
{
|
|
Expects(result.type() == mtpc_contacts_resolvedPeer);
|
|
|
|
auto &data = result.c_contacts_resolvedPeer();
|
|
session->data().processUsers(data.vusers());
|
|
session->data().processChats(data.vchats());
|
|
if (const auto peer = session->data().peerLoaded(peerFromMTP(data.vpeer()))) {
|
|
if (QString::number(peer->id.value & PeerId::kChatTypeMask) == peerId) {
|
|
callback(normalized, peer);
|
|
return;
|
|
}
|
|
}
|
|
|
|
callback(normalized, nullptr);
|
|
}).fail([=]
|
|
{
|
|
callback(QString(), nullptr);
|
|
}).send();
|
|
}
|
|
|
|
void searchPeerInner(const QString &peerId, Main::Session *session, const UsernameResolverCallback &callback) {
|
|
if (!session) {
|
|
callback(QString(), nullptr);
|
|
return;
|
|
}
|
|
|
|
const auto bot = session->data().userLoaded(usernameResolverBotId);
|
|
if (!bot) {
|
|
callback(QString(), nullptr);
|
|
return;
|
|
}
|
|
|
|
session->api().request(MTPmessages_GetInlineBotResults(
|
|
MTP_flags(0),
|
|
bot->inputUser,
|
|
MTP_inputPeerEmpty(),
|
|
MTPInputGeoPoint(),
|
|
MTP_string(peerId),
|
|
MTP_string("")
|
|
)).done([=](const MTPmessages_BotResults &result)
|
|
{
|
|
if (result.type() != mtpc_messages_botResults) {
|
|
callback(QString(), nullptr);
|
|
return;
|
|
}
|
|
auto &d = result.c_messages_botResults();
|
|
session->data().processUsers(d.vusers());
|
|
|
|
auto &v = d.vresults().v;
|
|
|
|
for (const auto &res : v) {
|
|
const auto message = res.match(
|
|
[&](const MTPDbotInlineResult &data)
|
|
{
|
|
return &data.vsend_message();
|
|
},
|
|
[&](const MTPDbotInlineMediaResult &data)
|
|
{
|
|
return &data.vsend_message();
|
|
});
|
|
|
|
const auto text = message->match(
|
|
[&](const MTPDbotInlineMessageMediaAuto &data)
|
|
{
|
|
return QString();
|
|
},
|
|
[&](const MTPDbotInlineMessageText &data)
|
|
{
|
|
return qs(data.vmessage());
|
|
},
|
|
[&](const MTPDbotInlineMessageMediaGeo &data)
|
|
{
|
|
return QString();
|
|
},
|
|
[&](const MTPDbotInlineMessageMediaVenue &data)
|
|
{
|
|
return QString();
|
|
},
|
|
[&](const MTPDbotInlineMessageMediaContact &data)
|
|
{
|
|
return QString();
|
|
},
|
|
[&](const MTPDbotInlineMessageMediaInvoice &data)
|
|
{
|
|
return QString();
|
|
},
|
|
[&](const MTPDbotInlineMessageMediaWebPage &data)
|
|
{
|
|
return QString();
|
|
});
|
|
|
|
if (text.isEmpty() || text.contains(usernameResolverEmpty)) {
|
|
continue;
|
|
}
|
|
|
|
QString id; // 🆔
|
|
QString title; // 🏷
|
|
QString username; // 📧
|
|
|
|
for (auto &line : text.split('\n')) {
|
|
if (line.startsWith("🆔")) {
|
|
id = line.mid(line.indexOf(": ") + 2).trimmed();
|
|
} else if (line.startsWith("🏷")) {
|
|
title = line.mid(line.indexOf(": ") + 2);
|
|
} else if (line.startsWith("📧")) {
|
|
username = line.mid(line.indexOf(": ") + 2);
|
|
}
|
|
}
|
|
|
|
if (id.isEmpty() || id != peerId) {
|
|
continue;
|
|
}
|
|
|
|
if (id.startsWith("-100")) {
|
|
id = id.mid(4);
|
|
}
|
|
|
|
if (!username.isEmpty()) {
|
|
resolvePeer(
|
|
id,
|
|
username,
|
|
session,
|
|
[=](const QString &titleInner, PeerData *data)
|
|
{
|
|
if (data) {
|
|
callback(titleInner, data);
|
|
} else {
|
|
callback(title, nullptr);
|
|
}
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (!title.isEmpty()) {
|
|
callback(title, nullptr);
|
|
return;
|
|
}
|
|
}
|
|
|
|
callback(QString(), nullptr);
|
|
}).fail([=]
|
|
{
|
|
callback(QString(), nullptr);
|
|
}).handleAllErrors().send();
|
|
}
|
|
|
|
void searchPeer(const QString &peerId, Main::Session *session, const UsernameResolverCallback &callback) {
|
|
if (!session) {
|
|
callback(QString(), nullptr);
|
|
return;
|
|
}
|
|
|
|
if (session->data().userLoaded(usernameResolverBotId)) {
|
|
searchPeerInner(peerId, session, callback);
|
|
} else {
|
|
resolvePeer(
|
|
QString::number(usernameResolverBotId),
|
|
usernameResolverBotUsername,
|
|
session,
|
|
[=](const QString &title, PeerData *data)
|
|
{
|
|
searchPeerInner(peerId, session, callback);
|
|
});
|
|
}
|
|
}
|
|
|
|
void searchUserById(ID userId, Main::Session *session, const UsernameResolverCallback &callback) {
|
|
if (userId == 0 || !session) {
|
|
callback(QString(), nullptr);
|
|
return;
|
|
}
|
|
|
|
if (const auto userLoaded = session->data().userLoaded(userId)) {
|
|
callback(userLoaded->username(), userLoaded);
|
|
return;
|
|
}
|
|
|
|
searchPeer(
|
|
QString::number(userId),
|
|
session,
|
|
[=](const QString &title, PeerData *data)
|
|
{
|
|
if (data) {
|
|
if (const auto user = data->asUser(); user->accessHash()) {
|
|
callback(title, user);
|
|
return;
|
|
}
|
|
}
|
|
callback(QString(), nullptr);
|
|
});
|
|
}
|
|
|
|
void searchChatById(ID chatId, Main::Session *session, const UsernameResolverCallback &callback) {
|
|
if (chatId == 0 || !session) {
|
|
callback(QString(), nullptr);
|
|
return;
|
|
}
|
|
|
|
if (const auto channelLoaded = session->data().channelLoaded(chatId)) {
|
|
callback(channelLoaded->username(), channelLoaded);
|
|
return;
|
|
}
|
|
|
|
if (const auto chatLoaded = session->data().chatLoaded(chatId)) {
|
|
callback(chatLoaded->username(), chatLoaded);
|
|
return;
|
|
}
|
|
|
|
searchPeer(
|
|
QString("-100") + QString::number(chatId),
|
|
session,
|
|
[=](const QString &title, PeerData *data)
|
|
{
|
|
if (data && (data->isChat() || data->isChannel())) {
|
|
callback(title, data);
|
|
} else {
|
|
callback(QString(), nullptr);
|
|
}
|
|
});
|
|
}
|
|
|
|
ID getUserIdFromPackId(uint64 id) {
|
|
// https://github.com/TDesktop-x64/tdesktop/pull/218/commits/844e5f0ab116e7639cfc79633a68afe8fdcbc463
|
|
auto ownerId = id >> 32;
|
|
if ((id >> 16 & 0xff) == 0x3f) {
|
|
ownerId |= 0x80000000;
|
|
}
|
|
if (id >> 24 & 0xff) {
|
|
ownerId += 0x100000000;
|
|
}
|
|
|
|
return ownerId;
|
|
}
|
|
|
|
TextWithTags extractText(not_null<HistoryItem*> item) {
|
|
TextWithTags result;
|
|
|
|
QString text;
|
|
if (const auto media = item->media()) {
|
|
if (const auto poll = media->poll()) {
|
|
text.append("\xF0\x9F\x93\x8A ") // 📊
|
|
.append(poll->question.text).append("\n");
|
|
for (const auto answer : poll->answers) {
|
|
text.append("• ").append(answer.text.text).append("\n");
|
|
}
|
|
}
|
|
}
|
|
|
|
result.tags = TextUtilities::ConvertEntitiesToTextTags(item->originalText().entities);
|
|
result.text = text.isEmpty() ? item->originalText().text : text;
|
|
return result;
|
|
}
|
|
|
|
bool mediaDownloadable(const Data::Media *media) {
|
|
if (!media
|
|
|| media->webpage() || media->poll() || media->game()
|
|
|| media->invoice() || media->location() || media->paper()
|
|
|| media->giveawayStart() || media->giveawayResults()
|
|
|| media->sharedContact() || media->call()
|
|
) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void resolveAllChats(const std::map<long long, QString> &peers) {
|
|
// not sure is this works
|
|
auto session = currentSession();
|
|
|
|
crl::async([=, &session]
|
|
{
|
|
while (!peers.empty()) {
|
|
for (const auto &[id, username] : peers) {
|
|
std::latch latch(1);
|
|
|
|
auto onSuccess = [=, &latch](const MTPChatInvite &invite)
|
|
{
|
|
invite.match([=](const MTPDchatInvite &data)
|
|
{
|
|
},
|
|
[=](const MTPDchatInviteAlready &data)
|
|
{
|
|
if (const auto chat = session->data().processChat(data.vchat())) {
|
|
if (const auto channel = chat->asChannel()) {
|
|
channel->clearInvitePeek();
|
|
}
|
|
}
|
|
},
|
|
[=](const MTPDchatInvitePeek &data)
|
|
{
|
|
});
|
|
|
|
latch.count_down();
|
|
};
|
|
auto onFail = [=, &latch](const MTP::Error &error)
|
|
{
|
|
if (MTP::IsFloodError(error.type())) {
|
|
std::this_thread::sleep_for(std::chrono::seconds(20));
|
|
}
|
|
latch.count_down();
|
|
};
|
|
|
|
session->api().checkChatInvite(username, onSuccess, onFail);
|
|
latch.wait();
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
not_null<Main::Session*> currentSession() {
|
|
return &Core::App().domain().active().session();
|
|
}
|
|
|
|
template<typename T>
|
|
PeerData *getPeerFromDialogId(T id) {
|
|
for (const auto &[index, account] : Core::App().domain().accounts()) {
|
|
if (const auto session = account->maybeSession()) {
|
|
PeerData *from = session->data().userLoaded(id);
|
|
if (!from) {
|
|
from = session->data().channelLoaded(id);
|
|
}
|
|
if (!from) {
|
|
from = reinterpret_cast<PeerData*>(session->data().chatLoaded(id));
|
|
}
|
|
|
|
if (from) {
|
|
return from;
|
|
}
|
|
}
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
PeerData *getPeerFromDialogId(ID id) {
|
|
return getPeerFromDialogId<ID>(id);
|
|
}
|
|
|
|
PeerData *getPeerFromDialogId(unsigned long long id) {
|
|
return getPeerFromDialogId<unsigned long long>(id);
|
|
}
|