From 100a7cc8469131b595c14d6c629d573a7d4a6791 Mon Sep 17 00:00:00 2001 From: Jannik <32801117+code1mountain@users.noreply.github.com> Date: Sat, 12 Dec 2020 00:45:36 +0100 Subject: [PATCH] Allow Users to use the bot api (#19) Co-authored-by: Jannik --- README.md | 76 +++++++++++ docker-entrypoint.sh | 6 + td | 2 +- telegram-bot-api/Client.cpp | 183 +++++++++++++++++++++++++- telegram-bot-api/Client.h | 21 ++- telegram-bot-api/ClientManager.cpp | 151 +++++++++++++++------ telegram-bot-api/ClientManager.h | 5 +- telegram-bot-api/ClientParameters.h | 3 + telegram-bot-api/HttpConnection.cpp | 41 ++++-- telegram-bot-api/Query.cpp | 3 +- telegram-bot-api/Query.h | 6 +- telegram-bot-api/WebhookActor.cpp | 2 +- telegram-bot-api/telegram-bot-api.cpp | 3 + 13 files changed, 437 insertions(+), 65 deletions(-) diff --git a/README.md b/README.md index 6c561ba..5711dd1 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,7 @@ Please note that only TDLight-specific issues are suitable for this repository. - [TDLight features](#tdlight-features) - [Added features](#added-features) - [Modified features](#modified-features) + - [User Mode](#user-mode) - [Installation](#installation) - [Dependencies](#dependencies) - [Usage](#usage) @@ -126,6 +127,81 @@ In addition, the member list now shows the full bot list (previously only the bo The bot will now receive Updates for all received media, even if a destruction timer is set. + +### User Mode + +You can allow user accounts to access the bot api with the command-line option `--allow-users` or set the env variable +`TELEGRAM_ALLOW_USERS` to `1` when using docker. User Mode is disabled by default, so only bots can access the api. + +You can now log into the bot api with user accounts to create userbots running on your account. + +Note: Never send your 2fa password over a plain http connection. Make sure https is enabled or use this api locally. + +#### User Authorization Process +1. Send a request to `{api_url}/userlogin` + + Parameters: + - `phone_number`: `string`. The phone number of your Telegram Account. + + Returns your `user_token` as `string`. You can use this just like a normal bot token on the `/user` endpoint + +2. Send the received code to `{api_url}/user{user_token}/authcode` + + Parameters: + - `code`: `int`. The code send to you by Telegram In-App or by SMS + + Will send `{"ok": true, "result": true}` on success. + +3. Optional: Send your 2fa password to `{api_url}/user{user_token}/2fapassword` + + Parameters: + - `password`: `string`. Password for 2fa authentication + + Will send `{"ok": true, "result": true}` on success. + +4. Optional: Register the user by calling `{api_url}/user{user_token}/registerUser`. + + User registration is disabled by default. You can enable it with the `--allow-users-registration` command line + option or the env variable `TELEGRAM_ALLOW_USERS_REGISTRATION` set to `1` when using docker. + + Parameters: + - `first_name`: `string`. First name for the new account. + - `last_name`: `string`, optional. Last name for the new account. + + Will send `{"ok": true, "result": true}` on success. + +You are now logged in and can use all methods like in the bot api, just replace the +`/bot{bot_token}/` in your urls with `/user{token}/`. + +You only need to authenticate once, the account will stay logged in. You can use the `logOut` method to log out +or simply close the session in your account settings. + +Some methods are (obviously) not available as a user. This includes: +- `answerCallbackQuery` +- `setMyCommands` +- `editMessageReplyMarkup` +- `uploadStickerFile` +- `createNewStickerSet` +- `addStickerToSet` +- `setStickerPositionInSet` +- `deleteStickerFromSet` +- `setStickerSetThumb` +- `sendInvoice` +- `answerShippingQuery` +- `answerPreCheckoutQuery` +- `setPassportDataErrors` +- `sendGame` +- `setGameScore` +- `getGameHighscores` + +It is also not possible to attach a `reply_markup` to any message. + +Your api wrapper may behave different in +some cases, for examples command message-entities are not created in chats that don't contain any +bots, so your Command Handler may not detect it. + +It is possible to have multiple user-tokens to multiple client instances on the same bot api server. + ## Installation diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh index bd49d8d..6470b67 100755 --- a/docker-entrypoint.sh +++ b/docker-entrypoint.sh @@ -40,6 +40,12 @@ fi if [ -n "$TELEGRAM_NO_FILE_LIMIT" ]; then CUSTOM_ARGS="${CUSTOM_ARGS} --no-file-limit" fi +if [ -n "$TELEGRAM_ALLOW_USERS" ]; then + CUSTOM_ARGS="${CUSTOM_ARGS} --allow-users" +fi +if [ -n "$TELEGRAM_ALLOW_USERS_REGISTRATION" ]; then + CUSTOM_ARGS="${CUSTOM_ARGS} --allow-users-registration" +fi if [ -n "$TELEGRAM_INSECURE" ]; then CUSTOM_ARGS="${CUSTOM_ARGS} --insecure" fi diff --git a/td b/td index b47fab1..2b92c16 160000 --- a/td +++ b/td @@ -1 +1 @@ -Subproject commit b47fab11cd7dddbb2829ef1830007632332b717b +Subproject commit 2b92c16998d39e7bfee580defcb0109151e4fb81 diff --git a/telegram-bot-api/Client.cpp b/telegram-bot-api/Client.cpp index 91dccd7..6ac7b2b 100644 --- a/telegram-bot-api/Client.cpp +++ b/telegram-bot-api/Client.cpp @@ -34,6 +34,21 @@ #include +#define CHECK_IS_BOT() \ + if (is_user_) { \ + return Status::Error(BOT_ONLY_ERROR_CODE, BOT_ONLY_ERROR_DESCRIPTION); \ + } + +#define CHECK_IS_USER() \ + if (!is_user_) { \ + return Status::Error(USER_ONLY_ERROR_CODE, USER_ONLY_ERROR_DESCRIPTION); \ + } + +#define CHECK_USER_REPLY_MARKUP() \ + if (reply_markup != nullptr && is_user_) { \ + return Status::Error(BOT_ONLY_ERROR_CODE, BOT_ONLY_ERROR_DESCRIPTION); \ + } + namespace telegram_bot_api { using td::Jsonable; @@ -124,6 +139,9 @@ void Client::fail_query_with_error(PromisedQueryPtr query, int32 error_code, Sli case 403: prefix = Slice("Forbidden"); break; + case 405: + prefix = Slice("Method Not Allowed"); + break; case 500: prefix = Slice("Internal Server Error"); break; @@ -156,11 +174,31 @@ void Client::fail_query_with_error(PromisedQueryPtr &&query, object_ptrcode_, error->message_, default_message); } -Client::Client(td::ActorShared<> parent, const td::string &bot_token, bool is_test_dc, int64 tqueue_id, +Client::Client(td::ActorShared<> parent, const td::string &bot_token, bool is_user, bool is_test_dc, int64 tqueue_id, std::shared_ptr parameters, td::ActorId stat_actor) : parent_(std::move(parent)) , bot_token_(bot_token) , bot_token_id_("") + , is_user_(is_user) + , is_test_dc_(is_test_dc) + , tqueue_id_(tqueue_id) + , parameters_(std::move(parameters)) + , stat_actor_(std::move(stat_actor)) { + messages_lru_root_.lru_next = &messages_lru_root_; + messages_lru_root_.lru_prev = &messages_lru_root_; + + static auto is_inited = init_methods(); + CHECK(is_inited); +} + +Client::Client(td::ActorShared<> parent, const td::string &bot_token, const td::string &phone_number, bool is_user, + bool is_test_dc, int64 tqueue_id, std::shared_ptr parameters, + td::ActorId stat_actor) + : parent_(std::move(parent)) + , bot_token_(bot_token) + , bot_token_id_("") + , phone_number_(phone_number) + , is_user_(is_user) , is_test_dc_(is_test_dc) , tqueue_id_(tqueue_id) , parameters_(std::move(parameters)) @@ -2295,7 +2333,10 @@ class Client::TdOnAuthorizationCallback : public TdQueryCallback { } void on_result(object_ptr result) override { - bool was_ready = client_->authorization_state_->get_id() != td_api::authorizationStateWaitPhoneNumber::ID; + bool was_ready = client_->authorization_state_->get_id() != td_api::authorizationStateWaitPhoneNumber::ID && + client_->authorization_state_->get_id() != td_api::authorizationStateWaitCode::ID && + client_->authorization_state_->get_id() != td_api::authorizationStateWaitPassword::ID && + client_->authorization_state_->get_id() != td_api::authorizationStateWaitRegistration::ID; if (result->get_id() == td_api::error::ID) { auto error = move_object_as(result); if (error->code_ == 429 || error->code_ >= 500 || (error->code_ != 401 && was_ready)) { @@ -2314,6 +2355,38 @@ class Client::TdOnAuthorizationCallback : public TdQueryCallback { Client *client_; }; +class Client::TdOnAuthorizationQueryCallback : public TdQueryCallback { + public: + TdOnAuthorizationQueryCallback(Client *client, PromisedQueryPtr query) : + client_(client), query_(std::move(query)) { + } + + void on_result(object_ptr result) override { + bool was_ready = client_->authorization_state_->get_id() != td_api::authorizationStateWaitPhoneNumber::ID && + client_->authorization_state_->get_id() != td_api::authorizationStateWaitCode::ID && + client_->authorization_state_->get_id() != td_api::authorizationStateWaitPassword::ID && + client_->authorization_state_->get_id() != td_api::authorizationStateWaitRegistration::ID; + if (result->get_id() == td_api::error::ID) { + auto error = move_object_as(result); + if (error->code_ == 429 || error->code_ >= 500 || (error->code_ != 401 && was_ready)) { + // try again + return client_->on_update_authorization_state(); + } + fail_query(401, "Unauthorized: Log in failed, logging out due to " + td::oneline(to_string(error)), + std::move(query_)); + LOG(WARNING) << "Logging out due to " << td::oneline(to_string(error)); + client_->log_out(); + } else { + answer_query(td::JsonTrue(), std::move(query_)); + client_->on_update_authorization_state(); + } + } + + private: + Client *client_; + PromisedQueryPtr query_; +}; + class Client::TdOnInitCallback : public TdQueryCallback { public: explicit TdOnInitCallback(Client *client) : client_(client) { @@ -3237,7 +3310,7 @@ class Client::TdOnSendCustomRequestCallback : public TdQueryCallback { class Client::TdOnPingCallback : public TdQueryCallback { public: - TdOnPingCallback(PromisedQueryPtr query) + explicit TdOnPingCallback(PromisedQueryPtr query) : query_(std::move(query)) { } @@ -3245,7 +3318,7 @@ class Client::TdOnPingCallback : public TdQueryCallback { if (result->get_id() == td_api::error::ID) { return fail_query_with_error(std::move(query_), move_object_as(result), "Server not available"); } - CHECK(result->get_id() == 959899022); // id for return type `seconds` + CHECK(result->get_id() == td_api::seconds::ID); auto seconds_ = move_object_as(result); answer_query(td::VirtuallyJsonableString(std::to_string(seconds_->seconds_)), std::move(query_)); @@ -3386,7 +3459,7 @@ void Client::raw_event(const td::Event::Raw &event) { } void Client::loop() { - if (logging_out_ || closing_ || was_authorized_) { + if (logging_out_ || closing_ || was_authorized_ || waiting_for_auth_input_) { while (!cmd_queue_.empty()) { auto query = std::move(cmd_queue_.front()); cmd_queue_.pop(); @@ -3947,9 +4020,20 @@ void Client::on_update_authorization_state() { case td_api::authorizationStateWaitEncryptionKey::ID: return send_request(make_object(), std::make_unique(this)); case td_api::authorizationStateWaitPhoneNumber::ID: - return send_request(make_object(bot_token_), - std::make_unique(this)); + if (is_user_) { + return send_request(make_object(phone_number_, nullptr), + std::make_unique(this)); + } else { + return send_request(make_object(bot_token_), + std::make_unique(this)); + } + case td_api::authorizationStateWaitCode::ID: + case td_api::authorizationStateWaitPassword::ID: + case td_api::authorizationStateWaitRegistration::ID: + waiting_for_auth_input_ = true; + return loop(); case td_api::authorizationStateReady::ID: { + waiting_for_auth_input_ = false; auto user_info = get_user_info(my_id_); if (my_id_ <= 0 || user_info == nullptr) { return send_request(make_object(), std::make_unique(this)); @@ -3970,12 +4054,14 @@ void Client::on_update_authorization_state() { return loop(); } case td_api::authorizationStateLoggingOut::ID: + waiting_for_auth_input_ = false; if (!logging_out_) { LOG(WARNING) << "Logging out"; logging_out_ = true; } break; case td_api::authorizationStateClosing::ID: + waiting_for_auth_input_ = false; if (!closing_) { LOG(WARNING) << "Closing"; closing_ = true; @@ -4334,6 +4420,7 @@ void Client::on_closed() { if (logging_out_) { parameters_->shared_data_->webhook_db_->erase(bot_token_with_dc_); + parameters_->shared_data_->user_db_->erase(bot_token_with_dc_); class RmWorker : public td::Actor { public: @@ -6029,6 +6116,17 @@ void Client::on_cmd(PromisedQueryPtr query) { return do_send_request(make_object(), std::make_unique(std::move(query))); } } + if (waiting_for_auth_input_) { + if (query->method() == "authcode") { + return process_authcode_query(query); + } else if (query->method() == "2fapassword") { + return process_2fapassword_query(query); + } else if (query->method() == "registeruser" && parameters_->allow_users_registration_) { + return process_register_user_query(query); + } else { + return fail_query(404, "Not Found: method not found", std::move(query)); + } + } if (logging_out_) { return fail_query(LOGGING_OUT_ERROR_CODE, LOGGING_OUT_ERROR_DESCRIPTION, std::move(query)); @@ -6063,6 +6161,7 @@ td::Status Client::process_get_my_commands_query(PromisedQueryPtr &query) { } td::Status Client::process_set_my_commands_query(PromisedQueryPtr &query) { + CHECK_IS_BOT(); TRY_RESULT(bot_commands, get_bot_commands(query.get())); send_request(make_object(std::move(bot_commands)), std::make_unique(std::move(query))); @@ -6208,12 +6307,14 @@ td::Status Client::process_send_voice_query(PromisedQueryPtr &query) { } td::Status Client::process_send_game_query(PromisedQueryPtr &query) { + CHECK_IS_BOT(); TRY_RESULT(game_short_name, get_required_string_arg(query.get(), "game_short_name")); do_send_message(make_object(my_id_, game_short_name.str()), std::move(query)); return Status::OK(); } td::Status Client::process_send_invoice_query(PromisedQueryPtr &query) { + CHECK_IS_BOT(); TRY_RESULT(title, get_required_string_arg(query.get(), "title")); TRY_RESULT(description, get_required_string_arg(query.get(), "description")); TRY_RESULT(payload, get_required_string_arg(query.get(), "payload")); @@ -6351,6 +6452,7 @@ td::Status Client::process_stop_poll_query(PromisedQueryPtr &query) { auto chat_id = query->arg("chat_id"); auto message_id = get_message_id(query.get()); TRY_RESULT(reply_markup, get_reply_markup(query.get())); + CHECK_USER_REPLY_MARKUP(); resolve_reply_markup_bot_usernames( std::move(reply_markup), std::move(query), @@ -6446,6 +6548,7 @@ td::Status Client::process_edit_message_text_query(PromisedQueryPtr &query) { auto chat_id = query->arg("chat_id"); auto message_id = get_message_id(query.get()); TRY_RESULT(reply_markup, get_reply_markup(query.get())); + CHECK_USER_REPLY_MARKUP(); if (chat_id.empty() && message_id == 0) { TRY_RESULT(inline_message_id, get_inline_message_id(query.get())); @@ -6485,6 +6588,7 @@ td::Status Client::process_edit_message_live_location_query(PromisedQueryPtr &qu auto chat_id = query->arg("chat_id"); auto message_id = get_message_id(query.get()); TRY_RESULT(reply_markup, get_reply_markup(query.get())); + CHECK_USER_REPLY_MARKUP(); if (chat_id.empty() && message_id == 0) { TRY_RESULT(inline_message_id, get_inline_message_id(query.get())); @@ -6520,6 +6624,7 @@ td::Status Client::process_edit_message_media_query(PromisedQueryPtr &query) { auto chat_id = query->arg("chat_id"); auto message_id = get_message_id(query.get()); TRY_RESULT(reply_markup, get_reply_markup(query.get())); + CHECK_USER_REPLY_MARKUP(); TRY_RESULT(input_media, get_input_media(query.get(), "media", false)); if (chat_id.empty() && message_id == 0) { @@ -6554,6 +6659,7 @@ td::Status Client::process_edit_message_caption_query(PromisedQueryPtr &query) { auto chat_id = query->arg("chat_id"); auto message_id = get_message_id(query.get()); TRY_RESULT(reply_markup, get_reply_markup(query.get())); + CHECK_USER_REPLY_MARKUP(); TRY_RESULT(caption, get_caption(query.get())); if (chat_id.empty() && message_id == 0) { @@ -6584,9 +6690,11 @@ td::Status Client::process_edit_message_caption_query(PromisedQueryPtr &query) { } td::Status Client::process_edit_message_reply_markup_query(PromisedQueryPtr &query) { + CHECK_IS_BOT(); auto chat_id = query->arg("chat_id"); auto message_id = get_message_id(query.get()); TRY_RESULT(reply_markup, get_reply_markup(query.get())); + CHECK_USER_REPLY_MARKUP(); if (chat_id.empty() && message_id == 0) { TRY_RESULT(inline_message_id, get_inline_message_id(query.get())); @@ -6636,6 +6744,7 @@ td::Status Client::process_delete_message_query(PromisedQueryPtr &query) { } td::Status Client::process_set_game_score_query(PromisedQueryPtr &query) { + CHECK_IS_BOT(); auto chat_id = query->arg("chat_id"); auto message_id = get_message_id(query.get()); TRY_RESULT(user_id, get_user_id(query.get())); @@ -6673,6 +6782,7 @@ td::Status Client::process_set_game_score_query(PromisedQueryPtr &query) { } td::Status Client::process_get_game_high_scores_query(PromisedQueryPtr &query) { + CHECK_IS_BOT(); auto chat_id = query->arg("chat_id"); auto message_id = get_message_id(query.get()); TRY_RESULT(user_id, get_user_id(query.get())); @@ -6698,6 +6808,7 @@ td::Status Client::process_get_game_high_scores_query(PromisedQueryPtr &query) { } td::Status Client::process_answer_inline_query_query(PromisedQueryPtr &query) { + CHECK_IS_BOT(); auto inline_query_id = td::to_integer(query->arg("inline_query_id")); auto is_personal = to_bool(query->arg("is_personal")); int32 cache_time = get_integer_arg(query.get(), "cache_time", 300, 0, 24 * 60 * 60); @@ -6721,6 +6832,7 @@ td::Status Client::process_answer_inline_query_query(PromisedQueryPtr &query) { } td::Status Client::process_answer_callback_query_query(PromisedQueryPtr &query) { + CHECK_IS_BOT(); auto callback_query_id = td::to_integer(query->arg("callback_query_id")); td::string text = query->arg("text").str(); bool show_alert = to_bool(query->arg("show_alert")); @@ -6733,6 +6845,7 @@ td::Status Client::process_answer_callback_query_query(PromisedQueryPtr &query) } td::Status Client::process_answer_shipping_query_query(PromisedQueryPtr &query) { + CHECK_IS_BOT(); auto shipping_query_id = td::to_integer(query->arg("shipping_query_id")); auto ok = to_bool(query->arg("ok")); td::vector> shipping_options; @@ -6749,6 +6862,7 @@ td::Status Client::process_answer_shipping_query_query(PromisedQueryPtr &query) } td::Status Client::process_answer_pre_checkout_query_query(PromisedQueryPtr &query) { + CHECK_IS_BOT(); auto pre_checkout_query_id = td::to_integer(query->arg("pre_checkout_query_id")); auto ok = to_bool(query->arg("ok")); td::MutableSlice error_message; @@ -7226,6 +7340,7 @@ td::Status Client::process_get_sticker_set_query(PromisedQueryPtr &query) { } td::Status Client::process_upload_sticker_file_query(PromisedQueryPtr &query) { + CHECK_IS_BOT(); TRY_RESULT(user_id, get_user_id(query.get())); auto png_sticker = get_input_file(query.get(), "png_sticker"); @@ -7238,6 +7353,7 @@ td::Status Client::process_upload_sticker_file_query(PromisedQueryPtr &query) { } td::Status Client::process_create_new_sticker_set_query(PromisedQueryPtr &query) { + CHECK_IS_BOT(); TRY_RESULT(user_id, get_user_id(query.get())); auto name = query->arg("name"); auto title = query->arg("title"); @@ -7254,6 +7370,7 @@ td::Status Client::process_create_new_sticker_set_query(PromisedQueryPtr &query) } td::Status Client::process_add_sticker_to_set_query(PromisedQueryPtr &query) { + CHECK_IS_BOT(); TRY_RESULT(user_id, get_user_id(query.get())); auto name = query->arg("name"); TRY_RESULT(stickers, get_input_stickers(query.get())); @@ -7268,6 +7385,7 @@ td::Status Client::process_add_sticker_to_set_query(PromisedQueryPtr &query) { } td::Status Client::process_set_sticker_set_thumb_query(PromisedQueryPtr &query) { + CHECK_IS_BOT(); TRY_RESULT(user_id, get_user_id(query.get())); auto name = query->arg("name"); auto thumbnail = get_input_file(query.get(), "thumb"); @@ -7280,6 +7398,7 @@ td::Status Client::process_set_sticker_set_thumb_query(PromisedQueryPtr &query) } td::Status Client::process_set_sticker_position_in_set_query(PromisedQueryPtr &query) { + CHECK_IS_BOT(); auto file_id = trim(query->arg("sticker")); if (file_id.empty()) { return Status::Error(400, "Sticker is not specified"); @@ -7293,6 +7412,7 @@ td::Status Client::process_set_sticker_position_in_set_query(PromisedQueryPtr &q } td::Status Client::process_delete_sticker_from_set_query(PromisedQueryPtr &query) { + CHECK_IS_BOT(); auto file_id = trim(query->arg("sticker")); if (file_id.empty()) { return Status::Error(400, "Sticker is not specified"); @@ -7304,6 +7424,7 @@ td::Status Client::process_delete_sticker_from_set_query(PromisedQueryPtr &query } td::Status Client::process_set_passport_data_errors_query(PromisedQueryPtr &query) { + CHECK_IS_BOT(); TRY_RESULT(user_id, get_user_id(query.get())); TRY_RESULT(passport_element_errors, get_passport_element_errors(query.get())); @@ -7557,6 +7678,45 @@ td::Status Client::process_ping_query(PromisedQueryPtr &query) { } //end custom methods impl +//start costom auth methods impl + +void Client::process_authcode_query(PromisedQueryPtr &query) { + auto code = query->arg("code"); + if (code.empty()) { + return fail_query(400, "Bad Request: code not found", std::move(query)); + } + if (authorization_state_->get_id() != td_api::authorizationStateWaitCode::ID) { + return fail_query(400, "Bad Request: currently not waiting for a code", std::move(query)); + } + send_request(make_object(code.str()), + std::make_unique(this, std::move(query))); +} + +void Client::process_2fapassword_query(PromisedQueryPtr &query) { + auto password = query->arg("password"); + if (password.empty()) { + return fail_query(400, "Bad Request: password not found", std::move(query)); + } + if (authorization_state_->get_id() != td_api::authorizationStateWaitPassword::ID) { + return fail_query(400, "Bad Request: currently not waiting for a password", std::move(query)); + } + send_request(make_object(password.str()), + std::make_unique(this, std::move(query))); +} + +void Client::process_register_user_query(PromisedQueryPtr &query) { + auto first_name = query->arg("first_name"); + if (first_name.empty()) { + return fail_query(400, "Bad Request: first_name not found", std::move(query)); + } + auto last_name = query->arg("last_name"); + if (authorization_state_->get_id() != td_api::authorizationStateWaitRegistration::ID) { + return fail_query(400, "Bad Request: currently not waiting for registration", std::move(query)); + } + send_request(make_object(first_name.str(), last_name.str()), + std::make_unique(this, std::move(query))); +} +//end custom auth methods impl void Client::do_get_file(object_ptr file, PromisedQueryPtr query) { if ((!parameters_->local_mode_ || !parameters_->no_file_limit_) && @@ -7776,6 +7936,9 @@ void Client::do_send_message(object_ptr input_messa return fail_query_with_error(std::move(query), 400, r_reply_markup.error().message()); } auto reply_markup = r_reply_markup.move_as_ok(); + if (reply_markup != nullptr && is_user_) { + return fail_query_with_error(std::move(query), 405, "Method Not Allowed: reply markup not available as user."); + } resolve_reply_markup_bot_usernames( std::move(reply_markup), std::move(query), @@ -9335,6 +9498,12 @@ constexpr Client::Slice Client::LOGGING_OUT_ERROR_DESCRIPTION; constexpr int Client::CLOSING_ERROR_CODE; constexpr Client::Slice Client::CLOSING_ERROR_DESCRIPTION; +constexpr int Client::BOT_ONLY_ERROR_CODE; +constexpr Client::Slice Client::BOT_ONLY_ERROR_DESCRIPTION; + +constexpr int Client::USER_ONLY_ERROR_CODE; +constexpr Client::Slice Client::USER_ONLY_ERROR_DESCRIPTION; + std::unordered_map Client::methods_; } // namespace telegram_bot_api diff --git a/telegram-bot-api/Client.h b/telegram-bot-api/Client.h index 4d2dfcf..a0760bf 100644 --- a/telegram-bot-api/Client.h +++ b/telegram-bot-api/Client.h @@ -38,9 +38,13 @@ namespace td_api = td::td_api; class Client : public WebhookActor::Callback { public: - Client(td::ActorShared<> parent, const td::string &bot_token, bool is_test_dc, td::int64 tqueue_id, + Client(td::ActorShared<> parent, const td::string &bot_token, bool is_user, bool is_test_dc, td::int64 tqueue_id, std::shared_ptr parameters, td::ActorId stat_actor); + Client(td::ActorShared<> parent, const td::string &bot_token, const td::string &phone_number, bool is_user, + bool is_test_dc, td::int64 tqueue_id, std::shared_ptr parameters, + td::ActorId stat_actor); + void send(PromisedQueryPtr query) override; void close(); @@ -82,6 +86,12 @@ class Client : public WebhookActor::Callback { static constexpr int CLOSING_ERROR_CODE = 500; static constexpr Slice CLOSING_ERROR_DESCRIPTION = "Internal Server Error: restart"; + static constexpr int BOT_ONLY_ERROR_CODE = 405; + static constexpr Slice BOT_ONLY_ERROR_DESCRIPTION = "Method Not Allowed: You can only use this method as a bot"; + + static constexpr int USER_ONLY_ERROR_CODE = 405; + static constexpr Slice USER_ONLY_ERROR_DESCRIPTION = "Method Not Allowed: You can only use this method as a user"; + class JsonFile; class JsonDatedFile; class JsonDatedFiles; @@ -149,6 +159,7 @@ class Client : public WebhookActor::Callback { class TdOnOkCallback; class TdOnAuthorizationCallback; + class TdOnAuthorizationQueryCallback; class TdOnInitCallback; class TdOnGetUserProfilePhotosCallback; class TdOnSendMessageCallback; @@ -498,6 +509,11 @@ class Client : public WebhookActor::Callback { Status process_toggle_group_invites_query(PromisedQueryPtr &query); Status process_ping_query(PromisedQueryPtr &query); + //custom auth methods + void process_authcode_query(PromisedQueryPtr &query); + void process_2fapassword_query(PromisedQueryPtr &query); + void process_register_user_query(PromisedQueryPtr &query); + void webhook_verified(td::string cached_ip_address) override; void webhook_success() override; @@ -823,11 +839,14 @@ class Client : public WebhookActor::Callback { bool logging_out_ = false; bool need_close_ = false; bool clear_tqueue_ = false; + bool waiting_for_auth_input_ = false; td::ActorShared<> parent_; td::string bot_token_; td::string bot_token_with_dc_; td::string bot_token_id_; + td::string phone_number_; + bool is_user_; bool is_test_dc_; int64 tqueue_id_; double start_time_ = 0; diff --git a/telegram-bot-api/ClientManager.cpp b/telegram-bot-api/ClientManager.cpp index f275535..ee2e3d0 100644 --- a/telegram-bot-api/ClientManager.cpp +++ b/telegram-bot-api/ClientManager.cpp @@ -33,6 +33,8 @@ #include "td/utils/StackAllocator.h" #include "td/utils/StringBuilder.h" #include "td/utils/Time.h" +#include "td/utils/Random.h" +#include "td/utils/base64.h" #include #include @@ -62,7 +64,9 @@ void ClientManager::send(PromisedQueryPtr query) { // automatically send 429 return; } - + if (!parameters_->allow_users_ && query->is_user()) { + return fail_query(405, "Method Not Allowed: Users are not allowed to use the api", std::move(query)); + } td::string token = query->token().str(); if (token[0] == '0' || token.size() > 80u || token.find('/') != td::string::npos || token.find(':') == td::string::npos) { @@ -77,55 +81,29 @@ void ClientManager::send(PromisedQueryPtr query) { token += "/test"; } + auto bot_token_with_dc = PSTRING() << query->token() << (query->is_test_dc() ? ":T" : ""); + if (parameters_->shared_data_->user_db_->isset(bot_token_with_dc) != query->is_user()) { + return fail_query(400, "Bad Request: Please use the correct api endpoint for bots or users", std::move(query)); + } + auto id_it = token_to_id_.find(token); if (id_it == token_to_id_.end()) { - std::string ip_address; - if (query->peer_address().is_valid() && !query->peer_address().is_reserved()) { // external connection - ip_address = query->peer_address().get_ip_str().str(); - } else { - // invalid peer address or connection from the local network - ip_address = query->get_header("x-real-ip").str(); + if (!check_flood_limits(query)) { + return; } - if (!ip_address.empty()) { - td::IPAddress tmp; - tmp.init_host_port(ip_address, 0).ignore(); - tmp.clear_ipv6_interface(); - if (tmp.is_valid()) { - ip_address = tmp.get_ip_str().str(); - } - } - LOG(DEBUG) << "Receive incoming query for new bot " << token << " from " << query->peer_address(); - if (!ip_address.empty()) { - LOG(DEBUG) << "Check Client creation flood control for IP address " << ip_address; - auto res = flood_controls_.emplace(std::move(ip_address), td::FloodControlFast()); - auto &flood_control = res.first->second; - if (res.second) { - flood_control.add_limit(60, 20); // 20 in a minute - flood_control.add_limit(60 * 60, 600); // 600 in an hour - } - td::uint32 now = static_cast(td::Time::now()); - td::uint32 wakeup_at = flood_control.get_wakeup_at(); - if (wakeup_at > now) { - LOG(INFO) << "Failed to create Client from IP address " << ip_address; - return query->set_retry_after_error(static_cast(wakeup_at - now) + 1); - } - flood_control.add_event(static_cast(now)); - } - auto id = clients_.create(ClientInfo{BotStatActor(stat_.actor_id(&stat_)), token, td::ActorOwn()}); auto *client_info = clients_.get(id); auto stat_actor = client_info->stat_.actor_id(&client_info->stat_); auto client_id = td::create_actor( - PSLICE() << "Client/" << token, actor_shared(this, id), query->token().str(), query->is_test_dc(), - get_tqueue_id(r_user_id.ok(), query->is_test_dc()), parameters_, std::move(stat_actor)); + PSLICE() << "Client/" << token, actor_shared(this, id), query->token().str(), query->is_user(), + query->is_test_dc(), get_tqueue_id(r_user_id.ok(), query->is_test_dc()), parameters_, std::move(stat_actor)); auto method = query->method(); if (method != "deletewebhook" && method != "setwebhook") { - auto bot_token_with_dc = PSTRING() << query->token() << (query->is_test_dc() ? ":T" : ""); auto webhook_info = parameters_->shared_data_->webhook_db_->get(bot_token_with_dc); if (!webhook_info.empty()) { send_closure(client_id, &Client::send, - get_webhook_restore_query(bot_token_with_dc, webhook_info, parameters_->shared_data_)); + get_webhook_restore_query(bot_token_with_dc, query->is_user(), webhook_info, parameters_->shared_data_)); } } @@ -140,6 +118,89 @@ void ClientManager::send(PromisedQueryPtr query) { send_closure(client_info->client_, &Client::send, std::move(query)); // will send 429 if the client is already closed } +void ClientManager::user_login(PromisedQueryPtr query) { + if (!check_flood_limits(query, true)) { + return; + } + if (!parameters_->allow_users_) { + return fail_query(405, "Method Not Allowed: Users are not allowed to use the api", std::move(query)); + } + td::MutableSlice r_phone_number = query->arg("phone_number"); + if (r_phone_number.size() < 5 || r_phone_number.size() > 15) { + return fail_query(401, "Unauthorized: invalid phone number specified", std::move(query)); + } + td::int64 phone_number = 0; + for (char const &c: r_phone_number) { + if (isdigit(c)) { + phone_number = phone_number * 10 + (c - 48); + } + } + td::UInt256 token_data; + td::Random::secure_bytes(token_data.raw, sizeof(token_data)); + td::string user_token = td::to_string(phone_number) + ":" + td::base64url_encode(token_data.as_slice()); + auto user_token_with_dc = PSTRING() << user_token << (query->is_test_dc() ? ":T" : ""); + + long token_hash = std::hash{}(user_token); + + auto id = clients_.create(ClientInfo{BotStatActor(stat_.actor_id(&stat_)), user_token, td::ActorOwn()}); + auto *client_info = clients_.get(id); + auto stat_actor = client_info->stat_.actor_id(&client_info->stat_); + auto client_id = td::create_actor( + PSLICE() << "Client/" << user_token, actor_shared(this, id), user_token, td::to_string(phone_number), + true, query->is_test_dc(), get_tqueue_id(token_hash, query->is_test_dc()), parameters_, std::move(stat_actor)); + + clients_.get(id)->client_ = std::move(client_id); + auto id_it = token_to_id_.end(); + std::tie(id_it, std::ignore) = token_to_id_.emplace(user_token, id); + parameters_->shared_data_->user_db_->set(user_token_with_dc, "1"); + answer_query(td::VirtuallyJsonableString(user_token), std::move(query)); +} + +bool ClientManager::check_flood_limits(PromisedQueryPtr &query, bool is_user_login) { + td::string ip_address; + if (query->peer_address().is_valid() && !query->peer_address().is_reserved()) { // external connection + ip_address = query->peer_address().get_ip_str().str(); + } else { + // invalid peer address or connection from the local network + ip_address = query->get_header("x-real-ip").str(); + } + if (!ip_address.empty()) { + td::IPAddress tmp; + tmp.init_host_port(ip_address, 0).ignore(); + tmp.clear_ipv6_interface(); + if (tmp.is_valid()) { + ip_address = tmp.get_ip_str().str(); + } + } + LOG(DEBUG) << "Receive incoming query for new bot " << query->token() << " from " << query->peer_address(); + if (!ip_address.empty()) { + LOG(DEBUG) << "Check Client creation flood control for IP address " << ip_address; + if (is_user_login) { + ip_address += "/user"; + } + auto res = flood_controls_.emplace(std::move(ip_address), td::FloodControlFast()); + auto &flood_control = res.first->second; + if (res.second) { + if (is_user_login) { + flood_control.add_limit(60, 5); // 5 in a minute + flood_control.add_limit(60 * 60, 15); // 15 in an hour + } else { + flood_control.add_limit(60, 20); // 20 in a minute + flood_control.add_limit(60 * 60, 600); // 600 in an hour + } + } + auto now = static_cast(td::Time::now()); + td::uint32 wakeup_at = flood_control.get_wakeup_at(); + if (wakeup_at > now) { + LOG(INFO) << "Failed to create Client from IP address " << ip_address; + query->set_retry_after_error(static_cast(wakeup_at - now) + 1); + return false; + } + flood_control.add_event(static_cast(now)); + } + return true; +} + void ClientManager::get_stats(td::PromiseActor promise, td::vector> args) { if (close_flag_) { @@ -297,13 +358,19 @@ void ClientManager::start_up() { parameters_->shared_data_->tqueue_ = std::move(tqueue); } - // init webhook_db + // init webhook_db and user_db auto concurrent_webhook_db = td::make_unique>(); auto status = concurrent_webhook_db->init("webhooks_db.binlog", td::DbKey::empty(), scheduler_id); LOG_IF(FATAL, status.is_error()) << "Can't open webhooks_db.binlog " << status.error(); parameters_->shared_data_->webhook_db_ = std::move(concurrent_webhook_db); + auto concurrent_user_db = td::make_unique>(); + status = concurrent_user_db->init("user_db.binlog", td::DbKey::empty(), scheduler_id); + LOG_IF(FATAL, status.is_error()) << "Can't open user_db.binlog " << status.error(); + parameters_->shared_data_->user_db_ = std::move(concurrent_user_db); + auto &webhook_db = *parameters_->shared_data_->webhook_db_; + auto &user_db = *parameters_->shared_data_->user_db_; for (auto key_value : webhook_db.get_all()) { if (!token_range_(td::to_integer(key_value.first))) { LOG(WARNING) << "DROP WEBHOOK: " << key_value.first << " ---> " << key_value.second; @@ -311,12 +378,13 @@ void ClientManager::start_up() { continue; } - auto query = get_webhook_restore_query(key_value.first, key_value.second, parameters_->shared_data_); + auto query = get_webhook_restore_query(key_value.first, user_db.isset(key_value.first), key_value.second, parameters_->shared_data_); send_closure_later(actor_id(this), &ClientManager::send, std::move(query)); } + } -PromisedQueryPtr ClientManager::get_webhook_restore_query(td::Slice token, td::Slice webhook_info, +PromisedQueryPtr ClientManager::get_webhook_restore_query(td::Slice token, bool is_user, td::Slice webhook_info, std::shared_ptr shared_data) { // create Query with empty promise td::vector containers; @@ -364,7 +432,7 @@ PromisedQueryPtr ClientManager::get_webhook_restore_query(td::Slice token, td::S args.emplace_back(add_string("url"), add_string(parser.read_all())); const auto method = add_string("setwebhook"); - auto query = std::make_unique(std::move(containers), token, is_test_dc, method, std::move(args), + auto query = std::make_unique(std::move(containers), token, is_user, is_test_dc, method, std::move(args), td::vector>(), td::vector(), std::move(shared_data), td::IPAddress()); query->set_internal(true); @@ -392,6 +460,7 @@ void ClientManager::close_db() { parameters_->shared_data_->tqueue_->close(mpromise.get_promise()); parameters_->shared_data_->webhook_db_->close(mpromise.get_promise()); + parameters_->shared_data_->user_db_->close(mpromise.get_promise()); } void ClientManager::finish_close() { diff --git a/telegram-bot-api/ClientManager.h b/telegram-bot-api/ClientManager.h index c76dc7a..7ca02b2 100644 --- a/telegram-bot-api/ClientManager.h +++ b/telegram-bot-api/ClientManager.h @@ -42,6 +42,9 @@ class ClientManager final : public td::Actor { } void send(PromisedQueryPtr query); + void user_login(PromisedQueryPtr query); + + bool check_flood_limits(PromisedQueryPtr &query, bool is_user_login=false); void get_stats(td::PromiseActor promise, td::vector> args); @@ -68,7 +71,7 @@ class ClientManager final : public td::Actor { static td::int64 get_tqueue_id(td::int64 user_id, bool is_test_dc); - static PromisedQueryPtr get_webhook_restore_query(td::Slice token, td::Slice webhook_info, + static PromisedQueryPtr get_webhook_restore_query(td::Slice token, bool is_user, td::Slice webhook_info, std::shared_ptr shared_data); void start_up() override; diff --git a/telegram-bot-api/ClientParameters.h b/telegram-bot-api/ClientParameters.h index 103059a..72b2305 100644 --- a/telegram-bot-api/ClientParameters.h +++ b/telegram-bot-api/ClientParameters.h @@ -34,6 +34,7 @@ struct SharedData { // not thread-safe td::ListNode query_list_; td::unique_ptr webhook_db_; + td::unique_ptr user_db_; td::unique_ptr tqueue_; double unix_time_difference_{-1e100}; @@ -58,6 +59,8 @@ struct ClientParameters { bool allow_http_ = false; bool use_relative_path_ = false; bool no_file_limit_ = true; + bool allow_users_ = false; + bool allow_users_registration_ = false; td::int32 api_id_ = 0; td::string api_hash_; diff --git a/telegram-bot-api/HttpConnection.cpp b/telegram-bot-api/HttpConnection.cpp index 2b87707..8a3aac1 100644 --- a/telegram-bot-api/HttpConnection.cpp +++ b/telegram-bot-api/HttpConnection.cpp @@ -28,22 +28,37 @@ void HttpConnection::handle(td::unique_ptr http_query, return send_http_error(404, "Not Found: absolute URI is specified in the Request-Line"); } - if (!url_path_parser.try_skip("/bot")) { + bool is_login = false; + bool is_user = false; + if (url_path_parser.try_skip("/bot")) { + } else if (url_path_parser.try_skip("/userlogin")) { + is_user = true; + is_login = true; + } else if (url_path_parser.try_skip("/user")) { + is_user = true; + } else { return send_http_error(404, "Not Found"); } - auto token = url_path_parser.read_till('/'); + td::MutableSlice token; bool is_test_dc = false; - if (url_path_parser.try_skip("/test")) { - is_test_dc = true; - } - url_path_parser.skip('/'); - if (url_path_parser.status().is_error()) { - return send_http_error(404, "Not Found"); + td::MutableSlice method; + if (!is_login) { + token = url_path_parser.read_till('/'); + is_test_dc = false; + if (url_path_parser.try_skip("/test")) { + is_test_dc = true; + } + url_path_parser.skip('/'); + if (url_path_parser.status().is_error()) { + return send_http_error(404, "Not Found"); + } + + method = url_path_parser.data(); } - auto method = url_path_parser.data(); - auto query = std::make_unique(std::move(http_query->container_), token, is_test_dc, method, + + auto query = std::make_unique(std::move(http_query->container_), token, is_user, is_test_dc, method, std::move(http_query->args_), std::move(http_query->headers_), std::move(http_query->files_), shared_data_, http_query->peer_address_); @@ -52,7 +67,11 @@ void HttpConnection::handle(td::unique_ptr http_query, td::init_promise_future(&promise, &future); future.set_event(td::EventCreator::yield(actor_id())); auto promised_query = PromisedQueryPtr(query.release(), PromiseDeleter(std::move(promise))); - send_closure(client_manager_, &ClientManager::send, std::move(promised_query)); + if (is_login) { + send_closure(client_manager_, &ClientManager::user_login, std::move(promised_query)); + } else { + send_closure(client_manager_, &ClientManager::send, std::move(promised_query)); + } result_ = std::move(future); } diff --git a/telegram-bot-api/Query.cpp b/telegram-bot-api/Query.cpp index 41cb1ec..adc6a32 100644 --- a/telegram-bot-api/Query.cpp +++ b/telegram-bot-api/Query.cpp @@ -22,7 +22,7 @@ namespace telegram_bot_api { std::unordered_map> empty_parameters; -Query::Query(td::vector &&container, td::Slice token, bool is_test_dc, td::MutableSlice method, +Query::Query(td::vector &&container, td::Slice token, bool is_user, bool is_test_dc, td::MutableSlice method, td::vector> &&args, td::vector> &&headers, td::vector &&files, std::shared_ptr shared_data, const td::IPAddress &peer_address) @@ -31,6 +31,7 @@ Query::Query(td::vector &&container, td::Slice token, bool is_t , peer_address_(peer_address) , container_(std::move(container)) , token_(token) + , is_user_(is_user) , is_test_dc_(is_test_dc) , method_(method) , args_(std::move(args)) diff --git a/telegram-bot-api/Query.h b/telegram-bot-api/Query.h index 24bdac3..f1aca57 100644 --- a/telegram-bot-api/Query.h +++ b/telegram-bot-api/Query.h @@ -37,6 +37,9 @@ class Query : public td::ListNode { td::Slice token() const { return token_; } + bool is_user() const { + return is_user_; + } bool is_test_dc() const { return is_test_dc_; } @@ -122,7 +125,7 @@ class Query : public td::ListNode { is_internal_ = is_internal; } - Query(td::vector &&container, td::Slice token, bool is_test_dc, td::MutableSlice method, + Query(td::vector &&container, td::Slice token, bool is_user, bool is_test_dc, td::MutableSlice method, td::vector> &&args, td::vector> &&headers, td::vector &&files, std::shared_ptr shared_data, const td::IPAddress &peer_address); @@ -155,6 +158,7 @@ class Query : public td::ListNode { // request td::vector container_; td::Slice token_; + bool is_user_; bool is_test_dc_; td::MutableSlice method_; td::vector> args_; diff --git a/telegram-bot-api/WebhookActor.cpp b/telegram-bot-api/WebhookActor.cpp index 64e07c4..a4282d6 100644 --- a/telegram-bot-api/WebhookActor.cpp +++ b/telegram-bot-api/WebhookActor.cpp @@ -586,7 +586,7 @@ void WebhookActor::handle(td::unique_ptr response) { method != "logout" && !td::begins_with(method, "get")) { VLOG(webhook) << "Receive request " << method << " in response to webhook"; auto query = - std::make_unique(std::move(response->container_), td::MutableSlice(), false, td::MutableSlice(), + std::make_unique(std::move(response->container_), td::MutableSlice(), false, false, td::MutableSlice(), std::move(response->args_), std::move(response->headers_), std::move(response->files_), parameters_->shared_data_, response->peer_address_); auto promised_query = diff --git a/telegram-bot-api/telegram-bot-api.cpp b/telegram-bot-api/telegram-bot-api.cpp index 4b7b8a1..3b158fe 100644 --- a/telegram-bot-api/telegram-bot-api.cpp +++ b/telegram-bot-api/telegram-bot-api.cpp @@ -176,6 +176,9 @@ int main(int argc, char *argv[]) { [&] { parameters->no_file_limit_ = true; }); options.add_option('\0', "insecure", "allow the Bot API to send request via insecure HTTP", [&] { parameters->allow_http_ = true; }); options.add_option('\0', "relative", "use relative file path in local mode", [&] { parameters->use_relative_path_ = true; }); + options.add_option('\0', "allow-users", "allow user accounts to use the API", [&] { parameters->allow_users_ = true; }); + options.add_option('\0', "allow-users-registration", "allow user accounts to be registered on the API", + [&] { parameters->allow_users_registration_ = true; }); options.add_checked_option( '\0', "api-id",