Suggest adding bots to channels as admins.

This commit is contained in:
John Preston 2018-12-28 15:42:58 +04:00
parent c259921269
commit 4002739682
10 changed files with 203 additions and 172 deletions

View file

@ -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...";

View file

@ -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<PeerData*> peer,
const std::vector<not_null<UserData*>> &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<UserData*> user) {
return user->botInfo != nullptr;
});
if (!peer->isMegagroup() && bot != end(users)) {
ShowAddParticipantsError("USER_BOT", peer, users);
return;
}
auto list = QVector<MTPInputUser>();
list.reserve(qMin(int(users.size()), int(kMaxUsersPerInvite)));
const auto send = [&] {
request(MTPchannels_InviteToChannel(
channel->inputChannel,
MTP_vector<MTPInputUser>(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<ChatData*> chat) {
_chatAdminsEnabledRequests.take(
chat
@ -4556,26 +4620,36 @@ void ApiWrap::sendMessage(MessageToSend &&message) {
}
}
void ApiWrap::sendBotStart(not_null<UserData*> bot) {
void ApiWrap::sendBotStart(not_null<UserData*> 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<uint64>();
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<uint64>();
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(

View file

@ -260,6 +260,9 @@ public:
int availableCount,
const QVector<MTPChannelParticipant> &list)> callbackList,
Fn<void()> callbackNotModified = nullptr);
void addChatParticipants(
not_null<PeerData*> peer,
const std::vector<not_null<UserData*>> &users);
struct SendOptions {
SendOptions(not_null<History*> history);
@ -329,7 +332,7 @@ public:
bool handleSupportSwitch = false;
};
void sendMessage(MessageToSend &&message);
void sendBotStart(not_null<UserData*> bot);
void sendBotStart(not_null<UserData*> bot, PeerData *chat = nullptr);
void sendInlineResult(
not_null<UserData*> bot,
not_null<InlineBots::Result*> data,

View file

@ -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<PeerData*> chat,
const std::vector<not_null<UserData*>> &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<QPointer<EditAdminBox>>();
const auto close = [=] {
if (*weak) {
(*weak)->closeBox();
}
};
const auto saveCallback = Profile::SaveAdminCallback(
channel,
user,
[=](auto&&...) { close(); },
close);
auto box = Box<EditAdminBox>(
channel,
user,
MTP_channelAdminRights(MTP_flags(0)));
box->setSaveCallback(saveCallback);
*weak = Ui::show(std::move(box));
};
Ui::show(
Box<ConfirmBox>(
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<UserData*> 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<InformBox>(text), LayerOption::KeepOther);
}
class RevokePublicLinkBox::Inner : public TWidget, private MTP::Sender {
public:
Inner(QWidget *parent, Fn<void()> revokeCallback);

View file

@ -38,6 +38,10 @@ enum class PeerFloodType {
InviteChannel,
};
QString PeerFloodErrorText(PeerFloodType type);
void ShowAddParticipantsError(
const QString &error,
not_null<PeerData*> chat,
const std::vector<not_null<UserData*>> &users);
class AddContactBox : public BoxContent, public RPCSender {
public:

View file

@ -71,32 +71,34 @@ void ShareBotGame(not_null<UserData*> bot, not_null<PeerData*> chat) {
}
void AddBotToGroup(not_null<UserData*> bot, not_null<PeerData*> chat) {
if (auto &info = bot->botInfo) {
if (!info->startGroupToken.isEmpty()) {
MTP::send(
MTPmessages_StartBot(
bot->inputUser,
chat->input,
MTP_long(rand_value<uint64>()),
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<PeerListBox*> box,
not_null<PeerData*> chat) {
const auto rows = box->peerListCollectSelectedRows();
const auto users = ranges::view::all(
rows
) | ranges::view::transform([](not_null<PeerData*> peer) {
Expects(peer->isUser());
Expects(!peer->isSelf());
return not_null<UserData*>(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<ChatData*> chat) {
auto initBox = [chat](not_null<PeerListBox*> box) {
box->addButton(langFactory(lng_participant_invite), [box, chat] {
auto rows = box->peerListCollectSelectedRows();
if (!rows.empty()) {
auto users = std::vector<not_null<UserData*>>();
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<PeerListBox*> 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<PeerListBox*> box) {
auto subscription = std::make_shared<rpl::lifetime>();
box->addButton(langFactory(lng_participant_invite), [box, channel, subscription] {
auto rows = box->peerListCollectSelectedRows();
if (!rows.empty()) {
auto users = std::vector<not_null<UserData*>>();
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<PeerData*> chat) {
}
void AddBotToGroupBoxController::addBotToGroup(not_null<PeerData*> chat) {
if (auto megagroup = chat->asMegagroup()) {
if (const auto megagroup = chat->asMegagroup()) {
if (!megagroup->canAddMembers()) {
Ui::show(
Box<InformBox>(lang(lng_error_cant_add_member)),

View file

@ -192,6 +192,7 @@ private:
bool isAlreadyIn(not_null<UserData*> user) const;
int fullCount() const;
void updateTitle();
bool inviteSelectedUsers(not_null<PeerData*> chat) const;
PeerData *_peer = nullptr;
base::flat_set<not_null<UserData*>> _alreadyIn;

View file

@ -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() {

View file

@ -908,91 +908,6 @@ void MainWidget::deleteAndExit(ChatData *chat) {
rpcFail(&MainWidget::leaveChatFailed, peer));
}
void MainWidget::addParticipants(
not_null<PeerData*> chatOrChannel,
const std::vector<not_null<UserData*>> &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<MTPInputUser> 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<MTPInputUser>(inputUsers)),
rpcDone(&MainWidget::inviteToChannelDone, { channel }),
rpcFail(&MainWidget::addParticipantsFail, { channel }),
0,
5);
inputUsers.clear();
}
}
if (!inputUsers.isEmpty()) {
MTP::send(
MTPchannels_InviteToChannel(
channel->inputChannel,
MTP_vector<MTPInputUser>(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<InformBox>(text));
return false;
}
bool MainWidget::addParticipantsFail(
not_null<ChannelData*> 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<InformBox>(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<ChannelData*> channel,
const MTPUpdates &updates) {
sentUpdatesReceived(updates);
Auth().api().requestParticipantsCountDelayed(channel);
}
void MainWidget::historyToDown(History *history) {
_history->historyToDown(history);
}

View file

@ -125,9 +125,6 @@ public:
return sentUpdatesReceived(0, updates);
}
bool deleteChannelFailed(const RPCError &error);
void inviteToChannelDone(
not_null<ChannelData*> 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<PeerData*> chatOrChannel,
const std::vector<not_null<UserData*>> &users);
struct UserAndPeer {
UserData *user;
PeerData *peer;
};
bool addParticipantFail(UserAndPeer data, const RPCError &e);
bool addParticipantsFail(
not_null<ChannelData*> channel,
const RPCError &e); // for multi invite in channels
bool sendMessageFail(const RPCError &error);
Dialogs::IndexedList *contactsList();