diff --git a/Telegram/Resources/langs/lang.strings b/Telegram/Resources/langs/lang.strings index 22c5a07e6..650fcadb6 100644 --- a/Telegram/Resources/langs/lang.strings +++ b/Telegram/Resources/langs/lang.strings @@ -1090,7 +1090,10 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL "lng_cant_invite_banned" = "Sorry, only admin can add this user."; "lng_cant_invite_privacy" = "Sorry, you cannot add this user to groups because of their privacy settings."; "lng_cant_invite_privacy_channel" = "Sorry, you cannot add this user to channels because of their privacy settings."; +"lng_cant_invite_bot_to_channel" = "Sorry, bots can only be added to channels as administrators."; "lng_cant_do_this" = "Sorry, this action is unavailable."; +"lng_cant_invite_offer_admin" = "Bots can only be added as administrators."; +"lng_cant_invite_make_admin" = "Make admin"; "lng_send_button" = "Send"; "lng_message_ph" = "Write a message..."; diff --git a/Telegram/SourceFiles/apiwrap.cpp b/Telegram/SourceFiles/apiwrap.cpp index b9a686fab..b8e75f0a2 100644 --- a/Telegram/SourceFiles/apiwrap.cpp +++ b/Telegram/SourceFiles/apiwrap.cpp @@ -56,9 +56,22 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL namespace { -constexpr auto kReloadChannelMembersTimeout = 1000; // 1 second wait before reload members in channel after adding -constexpr auto kSaveCloudDraftTimeout = 1000; // save draft to the cloud with 1 sec extra delay -constexpr auto kSaveDraftBeforeQuitTimeout = 1500; // give the app 1.5 secs to save drafts to cloud when quitting +// 1 second wait before reload members in channel after adding. +constexpr auto kReloadChannelMembersTimeout = 1000; + +// Save draft to the cloud with 1 sec extra delay. +constexpr auto kSaveCloudDraftTimeout = 1000; + +// Give the app 1.5 secs to save drafts to cloud when quitting. +constexpr auto kSaveDraftBeforeQuitTimeout = 1500; + +// Max users in one super group invite request. +constexpr auto kMaxUsersPerInvite = 100; + +// How many messages from chat history server should forward to user, +// that was added to this chat. +constexpr auto kForwardMessagesOnAdd = 100; + constexpr auto kProxyPromotionInterval = TimeId(60 * 60); constexpr auto kProxyPromotionMinDelay = TimeId(10); constexpr auto kSmallDelayMs = 5; @@ -3565,6 +3578,57 @@ void ApiWrap::checkForUnreadMentions( } } +void ApiWrap::addChatParticipants( + not_null peer, + const std::vector> &users) { + if (const auto chat = peer->asChat()) { + for (const auto user : users) { + request(MTPmessages_AddChatUser( + chat->inputChat, + user->inputUser, + MTP_int(kForwardMessagesOnAdd) + )).done([=](const MTPUpdates &result) { + applyUpdates(result); + }).fail([=](const RPCError &error) { + ShowAddParticipantsError(error.type(), peer, { 1, user }); + }).afterDelay(TimeMs(5)).send(); + } + } else if (const auto channel = peer->asChannel()) { + const auto bot = ranges::find_if(users, [](not_null user) { + return user->botInfo != nullptr; + }); + if (!peer->isMegagroup() && bot != end(users)) { + ShowAddParticipantsError("USER_BOT", peer, users); + return; + } + auto list = QVector(); + list.reserve(qMin(int(users.size()), int(kMaxUsersPerInvite))); + const auto send = [&] { + request(MTPchannels_InviteToChannel( + channel->inputChannel, + MTP_vector(list) + )).done([=](const MTPUpdates &result) { + applyUpdates(result); + requestParticipantsCountDelayed(channel); + }).fail([=](const RPCError &error) { + ShowAddParticipantsError(error.type(), peer, users); + }).afterDelay(TimeMs(5)).send(); + }; + for (const auto user : users) { + list.push_back(user->inputUser); + if (list.size() == kMaxUsersPerInvite) { + send(); + list.clear(); + } + } + if (!list.empty()) { + send(); + } + } else { + Unexpected("User in ApiWrap::addChatParticipants."); + } +} + void ApiWrap::cancelEditChatAdmins(not_null chat) { _chatAdminsEnabledRequests.take( chat @@ -4556,26 +4620,36 @@ void ApiWrap::sendMessage(MessageToSend &&message) { } } -void ApiWrap::sendBotStart(not_null bot) { +void ApiWrap::sendBotStart(not_null bot, PeerData *chat) { Expects(bot->botInfo != nullptr); + Expects(chat == nullptr || !bot->botInfo->startGroupToken.isEmpty()); - const auto token = bot->botInfo->startToken; + if (chat && chat->isChannel() && !chat->isMegagroup()) { + ShowAddParticipantsError("USER_BOT", chat, { 1, bot }); + return; + } + + auto &info = bot->botInfo; + auto &token = chat ? info->startGroupToken : info->startToken; if (token.isEmpty()) { auto message = ApiWrap::MessageToSend(App::history(bot)); message.textWithTags = { qsl("/start"), TextWithTags::Tags() }; sendMessage(std::move(message)); - } else { - bot->botInfo->startToken = QString(); - const auto randomId = rand_value(); - request(MTPmessages_StartBot( - bot->inputUser, - MTP_inputPeerEmpty(), - MTP_long(randomId), - MTP_string(token) - )).done([=](const MTPUpdates &result) { - applyUpdates(result); - }).send(); + return; } + const auto randomId = rand_value(); + request(MTPmessages_StartBot( + bot->inputUser, + chat ? chat->input : MTP_inputPeerEmpty(), + MTP_long(randomId), + MTP_string(base::take(token)) + )).done([=](const MTPUpdates &result) { + applyUpdates(result); + }).fail([=](const RPCError &error) { + if (chat) { + ShowAddParticipantsError(error.type(), chat, { 1, bot }); + } + }).send(); } void ApiWrap::sendInlineResult( diff --git a/Telegram/SourceFiles/apiwrap.h b/Telegram/SourceFiles/apiwrap.h index 2cd351861..1bfcc2af9 100644 --- a/Telegram/SourceFiles/apiwrap.h +++ b/Telegram/SourceFiles/apiwrap.h @@ -260,6 +260,9 @@ public: int availableCount, const QVector &list)> callbackList, Fn callbackNotModified = nullptr); + void addChatParticipants( + not_null peer, + const std::vector> &users); struct SendOptions { SendOptions(not_null history); @@ -329,7 +332,7 @@ public: bool handleSupportSwitch = false; }; void sendMessage(MessageToSend &&message); - void sendBotStart(not_null bot); + void sendBotStart(not_null bot, PeerData *chat = nullptr); void sendInlineResult( not_null bot, not_null data, diff --git a/Telegram/SourceFiles/boxes/add_contact_box.cpp b/Telegram/SourceFiles/boxes/add_contact_box.cpp index c177a6f57..08ba9677b 100644 --- a/Telegram/SourceFiles/boxes/add_contact_box.cpp +++ b/Telegram/SourceFiles/boxes/add_contact_box.cpp @@ -16,7 +16,9 @@ https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL #include "boxes/confirm_box.h" #include "boxes/photo_crop_box.h" #include "boxes/peer_list_controllers.h" +#include "boxes/edit_participant_box.h" #include "core/file_utilities.h" +#include "profile/profile_channel_controllers.h" #include "chat_helpers/emoji_suggestions_widget.h" #include "ui/widgets/checkbox.h" #include "ui/widgets/buttons.h" @@ -67,6 +69,77 @@ QString PeerFloodErrorText(PeerFloodType type) { return lng_cant_send_to_not_contact(lt_more_info, link); } +void ShowAddParticipantsError( + const QString &error, + not_null chat, + const std::vector> &users) { + if (error == qstr("USER_BOT")) { + const auto channel = chat->asChannel(); + if ((users.size() == 1) + && (users.front()->botInfo != nullptr) + && channel + && !channel->isMegagroup() + && channel->canAddAdmins()) { + const auto makeAdmin = [=] { + const auto user = users.front(); + const auto weak = std::make_shared>(); + const auto close = [=] { + if (*weak) { + (*weak)->closeBox(); + } + }; + const auto saveCallback = Profile::SaveAdminCallback( + channel, + user, + [=](auto&&...) { close(); }, + close); + auto box = Box( + channel, + user, + MTP_channelAdminRights(MTP_flags(0))); + box->setSaveCallback(saveCallback); + *weak = Ui::show(std::move(box)); + }; + Ui::show( + Box( + lang(lng_cant_invite_offer_admin), + lang(lng_cant_invite_make_admin), + lang(lng_cancel), + makeAdmin), + LayerOption::KeepOther); + return; + } + } + const auto bot = ranges::find_if(users, [](not_null user) { + return user->botInfo != nullptr; + }); + const auto hasBot = (bot != end(users)); + const auto text = [&] { + if (error == qstr("USER_BOT")) { + return lang(lng_cant_invite_bot_to_channel); + } else if (error == qstr("USER_LEFT_CHAT")) { + // Trying to return a user who has left. + } else if (error == qstr("USER_KICKED")) { + // Trying to return a user who was kicked by admin. + return lang(lng_cant_invite_banned); + } else if (error == qstr("USER_PRIVACY_RESTRICTED")) { + return lang(lng_cant_invite_privacy); + } else if (error == qstr("USER_NOT_MUTUAL_CONTACT")) { + // Trying to return user who does not have me in contacts. + return lang(lng_failed_add_not_mutual); + } else if (error == qstr("USER_ALREADY_PARTICIPANT") && hasBot) { + return lang(lng_bot_already_in_group); + } else if (error == qstr("PEER_FLOOD")) { + const auto isGroup = (chat->isChat() || chat->isMegagroup()); + return PeerFloodErrorText(isGroup + ? PeerFloodType::InviteGroup + : PeerFloodType::InviteChannel); + } + return lang(lng_failed_add_participant); + }(); + Ui::show(Box(text), LayerOption::KeepOther); +} + class RevokePublicLinkBox::Inner : public TWidget, private MTP::Sender { public: Inner(QWidget *parent, Fn revokeCallback); diff --git a/Telegram/SourceFiles/boxes/add_contact_box.h b/Telegram/SourceFiles/boxes/add_contact_box.h index dcbb61421..5f41428a5 100644 --- a/Telegram/SourceFiles/boxes/add_contact_box.h +++ b/Telegram/SourceFiles/boxes/add_contact_box.h @@ -38,6 +38,10 @@ enum class PeerFloodType { InviteChannel, }; QString PeerFloodErrorText(PeerFloodType type); +void ShowAddParticipantsError( + const QString &error, + not_null chat, + const std::vector> &users); class AddContactBox : public BoxContent, public RPCSender { public: diff --git a/Telegram/SourceFiles/boxes/peer_list_controllers.cpp b/Telegram/SourceFiles/boxes/peer_list_controllers.cpp index 4db299b98..a51b185c1 100644 --- a/Telegram/SourceFiles/boxes/peer_list_controllers.cpp +++ b/Telegram/SourceFiles/boxes/peer_list_controllers.cpp @@ -71,32 +71,34 @@ void ShareBotGame(not_null bot, not_null chat) { } void AddBotToGroup(not_null bot, not_null chat) { - if (auto &info = bot->botInfo) { - if (!info->startGroupToken.isEmpty()) { - MTP::send( - MTPmessages_StartBot( - bot->inputUser, - chat->input, - MTP_long(rand_value()), - MTP_string(info->startGroupToken)), - App::main()->rpcDone(&MainWidget::sentUpdatesReceived), - App::main()->rpcFail( - &MainWidget::addParticipantFail, - { bot, chat })); - } else { - App::main()->addParticipants( - chat, - { 1, bot }); - } + if (bot->botInfo && !bot->botInfo->startGroupToken.isEmpty()) { + Auth().api().sendBotStart(bot, chat); } else { - App::main()->addParticipants( - chat, - { 1, bot }); + Auth().api().addChatParticipants(chat, { 1, bot }); } Ui::hideLayer(); Ui::showPeerHistory(chat, ShowAtUnreadMsgId); } +bool InviteSelectedUsers( + not_null box, + not_null chat) { + const auto rows = box->peerListCollectSelectedRows(); + const auto users = ranges::view::all( + rows + ) | ranges::view::transform([](not_null peer) { + Expects(peer->isUser()); + Expects(!peer->isSelf()); + + return not_null(peer->asUser()); + }) | ranges::to_vector; + if (users.empty()) { + return false; + } + Auth().api().addChatParticipants(chat, users); + return true; +} + } // namespace // Not used for now. @@ -544,18 +546,9 @@ void AddParticipantsBoxController::updateTitle() { } void AddParticipantsBoxController::Start(not_null chat) { - auto initBox = [chat](not_null box) { - box->addButton(langFactory(lng_participant_invite), [box, chat] { - auto rows = box->peerListCollectSelectedRows(); - if (!rows.empty()) { - auto users = std::vector>(); - for (auto peer : rows) { - auto user = peer->asUser(); - Assert(user != nullptr); - Assert(!user->isSelf()); - users.push_back(peer->asUser()); - } - App::main()->addParticipants(chat, users); + auto initBox = [=](not_null box) { + box->addButton(langFactory(lng_participant_invite), [=] { + if (InviteSelectedUsers(box, chat)) { Ui::showPeerHistory(chat, ShowAtTheEndMsgId); } }); @@ -570,17 +563,8 @@ void AddParticipantsBoxController::Start( bool justCreated) { auto initBox = [channel, justCreated](not_null box) { auto subscription = std::make_shared(); - box->addButton(langFactory(lng_participant_invite), [box, channel, subscription] { - auto rows = box->peerListCollectSelectedRows(); - if (!rows.empty()) { - auto users = std::vector>(); - for (auto peer : rows) { - auto user = peer->asUser(); - Assert(user != nullptr); - Assert(!user->isSelf()); - users.push_back(peer->asUser()); - } - App::main()->addParticipants(channel, users); + box->addButton(langFactory(lng_participant_invite), [=, subscription] { + if (InviteSelectedUsers(box, channel)) { if (channel->isMegagroup()) { Ui::showPeerHistory(channel, ShowAtTheEndMsgId); } else { @@ -830,7 +814,7 @@ void AddBotToGroupBoxController::shareBotGame(not_null chat) { } void AddBotToGroupBoxController::addBotToGroup(not_null chat) { - if (auto megagroup = chat->asMegagroup()) { + if (const auto megagroup = chat->asMegagroup()) { if (!megagroup->canAddMembers()) { Ui::show( Box(lang(lng_error_cant_add_member)), diff --git a/Telegram/SourceFiles/boxes/peer_list_controllers.h b/Telegram/SourceFiles/boxes/peer_list_controllers.h index 5c66d5686..a463bf043 100644 --- a/Telegram/SourceFiles/boxes/peer_list_controllers.h +++ b/Telegram/SourceFiles/boxes/peer_list_controllers.h @@ -192,6 +192,7 @@ private: bool isAlreadyIn(not_null user) const; int fullCount() const; void updateTitle(); + bool inviteSelectedUsers(not_null chat) const; PeerData *_peer = nullptr; base::flat_set> _alreadyIn; diff --git a/Telegram/SourceFiles/config.h b/Telegram/SourceFiles/config.h index 14d9b28e6..7ff666efe 100644 --- a/Telegram/SourceFiles/config.h +++ b/Telegram/SourceFiles/config.h @@ -26,8 +26,6 @@ enum { MTPKillFileSessionTimeout = 5000, // how much time without upload / download causes additional session kill - MaxUsersPerInvite = 100, // max users in one super group invite request - MTPChannelGetDifferenceLimit = 100, MaxSelectedItems = 100, @@ -273,8 +271,6 @@ enum { IdleMsecs = 60 * 1000, // after 60secs without user input we think we are idle SendViewsTimeout = 1000, // send views each second - - ForwardOnAdd = 100, // how many messages from chat history server should forward to user, that was added to this chat }; inline const QRegularExpression &cRussianLetters() { diff --git a/Telegram/SourceFiles/mainwidget.cpp b/Telegram/SourceFiles/mainwidget.cpp index 867b45ae7..865b7e9b1 100644 --- a/Telegram/SourceFiles/mainwidget.cpp +++ b/Telegram/SourceFiles/mainwidget.cpp @@ -908,91 +908,6 @@ void MainWidget::deleteAndExit(ChatData *chat) { rpcFail(&MainWidget::leaveChatFailed, peer)); } -void MainWidget::addParticipants( - not_null chatOrChannel, - const std::vector> &users) { - if (auto chat = chatOrChannel->asChat()) { - for_const (auto user, users) { - MTP::send( - MTPmessages_AddChatUser( - chat->inputChat, - user->inputUser, - MTP_int(ForwardOnAdd)), - rpcDone(&MainWidget::sentUpdatesReceived), - rpcFail(&MainWidget::addParticipantFail, { user, chat }), - 0, - 5); - } - } else if (auto channel = chatOrChannel->asChannel()) { - QVector inputUsers; - inputUsers.reserve(qMin(int(users.size()), int(MaxUsersPerInvite))); - for (auto i = users.cbegin(), e = users.cend(); i != e; ++i) { - inputUsers.push_back((*i)->inputUser); - if (inputUsers.size() == MaxUsersPerInvite) { - MTP::send( - MTPchannels_InviteToChannel( - channel->inputChannel, - MTP_vector(inputUsers)), - rpcDone(&MainWidget::inviteToChannelDone, { channel }), - rpcFail(&MainWidget::addParticipantsFail, { channel }), - 0, - 5); - inputUsers.clear(); - } - } - if (!inputUsers.isEmpty()) { - MTP::send( - MTPchannels_InviteToChannel( - channel->inputChannel, - MTP_vector(inputUsers)), - rpcDone(&MainWidget::inviteToChannelDone, { channel }), - rpcFail(&MainWidget::addParticipantsFail, { channel }), - 0, - 5); - } - } -} - -bool MainWidget::addParticipantFail(UserAndPeer data, const RPCError &error) { - if (MTP::isDefaultHandledError(error)) return false; - - QString text = lang(lng_failed_add_participant); - if (error.type() == qstr("USER_LEFT_CHAT")) { // trying to return a user who has left - } else if (error.type() == qstr("USER_KICKED")) { // trying to return a user who was kicked by admin - text = lang(lng_cant_invite_banned); - } else if (error.type() == qstr("USER_PRIVACY_RESTRICTED")) { - text = lang(lng_cant_invite_privacy); - } else if (error.type() == qstr("USER_NOT_MUTUAL_CONTACT")) { // trying to return user who does not have me in contacts - text = lang(lng_failed_add_not_mutual); - } else if (error.type() == qstr("USER_ALREADY_PARTICIPANT") && data.user->botInfo) { - text = lang(lng_bot_already_in_group); - } else if (error.type() == qstr("PEER_FLOOD")) { - text = PeerFloodErrorText((data.peer->isChat() || data.peer->isMegagroup()) ? PeerFloodType::InviteGroup : PeerFloodType::InviteChannel); - } - Ui::show(Box(text)); - return false; -} - -bool MainWidget::addParticipantsFail( - not_null channel, - const RPCError &error) { - if (MTP::isDefaultHandledError(error)) return false; - - QString text = lang(lng_failed_add_participant); - if (error.type() == qstr("USER_LEFT_CHAT")) { // trying to return banned user to his group - } else if (error.type() == qstr("USER_KICKED")) { // trying to return a user who was kicked by admin - text = lang(lng_cant_invite_banned); - } else if (error.type() == qstr("USER_PRIVACY_RESTRICTED")) { - text = lang(channel->isMegagroup() ? lng_cant_invite_privacy : lng_cant_invite_privacy_channel); - } else if (error.type() == qstr("USER_NOT_MUTUAL_CONTACT")) { // trying to return user who does not have me in contacts - text = lang(channel->isMegagroup() ? lng_failed_add_not_mutual : lng_failed_add_not_mutual_channel); - } else if (error.type() == qstr("PEER_FLOOD")) { - text = PeerFloodErrorText(PeerFloodType::InviteGroup); - } - Ui::show(Box(text)); - return false; -} - bool MainWidget::sendMessageFail(const RPCError &error) { if (MTP::isDefaultHandledError(error)) return false; @@ -2318,13 +2233,6 @@ bool MainWidget::deleteChannelFailed(const RPCError &error) { return true; } -void MainWidget::inviteToChannelDone( - not_null channel, - const MTPUpdates &updates) { - sentUpdatesReceived(updates); - Auth().api().requestParticipantsCountDelayed(channel); -} - void MainWidget::historyToDown(History *history) { _history->historyToDown(history); } diff --git a/Telegram/SourceFiles/mainwidget.h b/Telegram/SourceFiles/mainwidget.h index 8ca27c4d6..cc5fd59a2 100644 --- a/Telegram/SourceFiles/mainwidget.h +++ b/Telegram/SourceFiles/mainwidget.h @@ -125,9 +125,6 @@ public: return sentUpdatesReceived(0, updates); } bool deleteChannelFailed(const RPCError &error); - void inviteToChannelDone( - not_null channel, - const MTPUpdates &updates); void historyToDown(History *hist); void dialogsToUp(); void newUnreadMsg( @@ -201,18 +198,6 @@ public: bool deleteHistory = true); void deleteAndExit(ChatData *chat); - void addParticipants( - not_null chatOrChannel, - const std::vector> &users); - struct UserAndPeer { - UserData *user; - PeerData *peer; - }; - bool addParticipantFail(UserAndPeer data, const RPCError &e); - bool addParticipantsFail( - not_null channel, - const RPCError &e); // for multi invite in channels - bool sendMessageFail(const RPCError &error); Dialogs::IndexedList *contactsList();