#include #include #include #include #include #include #include #include #include #include #include #include #include #include "common.h" #include "commands.h" #include "telegram_client.h" #include "util.h" #include "discord_client.h" #include "curl/multi.h" #include "td/telegram/td_api.h" #include "td/telegram/td_api.hpp" const char *APP_GREETING = "--------------------\n"\ "| shortener-bot v0 |\n"\ "--------------------\n\n"; void async_startup(uv_idle_t *h); void configure_blocked_signals(); void configure_logging(); std::array, 3> configure_shutdown_signals(uv_loop_t *loop, context *ctx); CommandLineParams parse_command_line(int argc, char **argv); void print_greeting(); void print_usage(const char *programName); void shutdown_signal_handler(uv_signal_t *h, int signum); int curl_socket_cb(CURL*, curl_socket_t, int, context*, void*); int curl_timeout_cb(CURLM*, long, context*); void uv_socket_cb(uv_poll_t *handle, int status, int events); void uv_curl_timeout_cb(uv_timer_t *handle); void check_curl_multi_info(context *ctx); void gc_pending_queries(uv_timer_t *handle); int main(int argc, char **argv) { CommandLineParams cmdLineParams; try { cmdLineParams = parse_command_line(argc, argv); } catch (CommandLineParamError e) { switch (e) { case CommandLineParamError::NoApiCreds: print_usage(argv[0]); return 1; } } configure_logging(); configure_blocked_signals(); print_greeting(); uv_loop_t *defaultLoop = uv_default_loop(); context ctx; ctx.apiCreds = cmdLineParams.apiCreds; spdlog::info("Initializing CURL"); curl_global_init(CURL_GLOBAL_DEFAULT); CURLM *curl = curl_multi_init(); ctx.curl = curl; curl_multi_setopt(curl, CURLMOPT_SOCKETFUNCTION, &curl_socket_cb); curl_multi_setopt(curl, CURLMOPT_SOCKETDATA, &ctx); curl_multi_setopt(curl, CURLMOPT_TIMERFUNCTION, &curl_timeout_cb); curl_multi_setopt(curl, CURLMOPT_TIMERDATA, &ctx); uv_timer_init(defaultLoop, &ctx.curlTimer); ctx.curlTimer.data = &ctx; spdlog::info("Creating Telegram client"); TelegramClient tg(defaultLoop); ctx.tg = &tg; spdlog::info("Starting Discord bot"); DiscordClient discord(defaultLoop, cmdLineParams.discordBotToken); ctx.discord = &discord; uv_idle_t startupHandle; startupHandle.data = (void*)&ctx; uv_idle_init(defaultLoop, &startupHandle); uv_idle_start(&startupHandle, async_startup); uv_timer_t pendingQueryGcTimer; uv_timer_init(defaultLoop, &pendingQueryGcTimer); pendingQueryGcTimer.data = &ctx; uv_timer_start(&pendingQueryGcTimer, gc_pending_queries, 60000, 60000); auto signalHandles = configure_shutdown_signals(defaultLoop, &ctx); termios termSettings; tcgetattr(1, &termSettings); termSettings.c_lflag &= ~(ECHO | ICANON); tcsetattr(1, 0, &termSettings); spdlog::info("Entering event loop"); uv_run(defaultLoop, UV_RUN_DEFAULT); spdlog::info("Leaving event loop"); termSettings.c_lflag |= ECHO | ICANON; tcsetattr(1, 0, &termSettings); spdlog::info("Stopping Discord bot"); discord.stop(); spdlog::info("Cleaning up"); curl_multi_cleanup(curl); curl_global_cleanup(); return 0; } void async_startup(uv_idle_t *h) { context *ctx = (context*)h->data; spdlog::info("Starting telegram client (using api id {}, hash {})", ctx->apiCreds.id, ctx->apiCreds.hash); ctx->tg->add_update_handler([ctx](void*, td_api::Object &update){ td_api::downcast_call(update, overloaded( [ctx](td_api::updateAuthorizationState &upd) { td_api::downcast_call(*upd.authorization_state_, overloaded( [ctx](td_api::authorizationStateWaitTdlibParameters &state) { auto request = td_api::make_object(); request->database_directory_ = "tdata"; request->use_message_database_ = false; request->use_secret_chats_ = false; request->api_id_ = ctx->apiCreds.id; request->api_hash_ = ctx->apiCreds.hash; request->system_language_code_ = "en"; request->device_model_ = "server"; request->application_version_ = "1.0.0"; ctx->tg->send_query(std::move(request), {}); }, [ctx](td_api::authorizationStateWaitPhoneNumber &state) { ctx->tg->send_query(td_api::make_object(ctx->apiCreds.botToken), {}); // TODO add error handler }, [ctx](td_api::authorizationStateReady &state) { spdlog::info("Telegram ready"); }, [ctx,&upd](td_api::Object &obj){ spdlog::debug("unknown authorization state ID {}", upd.authorization_state_->get_id()); } )); }, [ctx](td_api::updateNewMessage &upd) { if (upd.message_->is_outgoing_) return; cmd::handle_regular_message(ctx, *upd.message_); }, [ctx](td_api::updateNewInlineQuery &upd) { cmd::handle_inline_query(ctx, upd); }, [ctx](td_api::updateNewInlineCallbackQuery &upd) { cmd::handle_callback_query(ctx, upd); }, [ctx](td_api::updateNewChosenInlineResult &upd) { cmd::handle_chosen_inline_result(ctx, upd); }, [](td_api::Object &obj){} )); }, nullptr); ctx->tg->start(); ctx->discord->on_shorten_command = [ctx](std::string url, dpp::slashcommand_t event){ if (url::is_url_valid(url)) { spdlog::debug("shortening URL from Discord: {}", url); cmd::shorten_link(url, ctx, [event = std::move(event)](std::string shorturl){ spdlog::debug("shortened URL for Discord: {}", shorturl); event.edit_response(shorturl); }); } else { spdlog::debug("invalid URL from Discord user: {}", url); event.edit_response("Invalid URL"); } }; ctx->discord->start(); uv_close((uv_handle_t*)h, nullptr); } void configure_logging() { auto loggingSink = std::make_shared>(stdout, spdlog::color_mode::automatic); loggingSink->set_color(spdlog::level::debug, "\033[38;5;61m"); auto logger = std::make_shared("main", loggingSink); spdlog::set_default_logger(logger); spdlog::set_level(spdlog::level::debug); } void configure_blocked_signals() { sigset_t signalSet; sigemptyset(&signalSet); sigaddset(&signalSet, SIGUSR1); sigaddset(&signalSet, SIGUSR2); sigaddset(&signalSet, SIGHUP); sigprocmask(SIG_BLOCK, &signalSet, nullptr); } std::unique_ptr register_signal_handle(uv_loop_t *loop, context *ctx, int signum, void(*signal_handler)(uv_signal_t*, int)) { spdlog::debug("register signal handler for {}", signum); auto deleter = [](uv_signal_t *handle) { uv_close((uv_handle_t*)handle, [](uv_handle_t *h){ delete h; }); }; std::unique_ptr handle(new uv_signal_t, deleter); uv_signal_init(loop, handle.get()); handle.get()->data = ctx; uv_signal_start(handle.get(), signal_handler, signum); return handle; } std::array, 3> configure_shutdown_signals(uv_loop_t *loop, context *ctx) { return {register_signal_handle(loop, ctx, SIGINT, shutdown_signal_handler), register_signal_handle(loop, ctx, SIGQUIT, shutdown_signal_handler), register_signal_handle(loop, ctx, SIGTERM, shutdown_signal_handler)}; } void shutdown_signal_handler(uv_signal_t *h, int signum) { spdlog::info("Stopping"); uv_stop(uv_default_loop()); } CommandLineParams parse_command_line(int argc, char **argv) { if (argc < 5) throw CommandLineParamError::NoApiCreds; char *apiIdEnd = nullptr; int apiId = strtol(argv[1], &apiIdEnd, 10); if (apiIdEnd == argv[1]) throw CommandLineParamError::NoApiCreds; return CommandLineParams {{apiId, std::string(argv[2]), std::string(argv[3])}, std::string(argv[4])}; } void print_greeting() { std::cout << APP_GREETING << std::flush; } void print_usage(const char *programName) { std::cout << "usage: " << programName << " \n"; } struct curl_context { uv_poll_t *pollHandle; curl_socket_t socketFd; context *ctx; curl_context(uv_loop_t *loop, context *ctx, int fd) { spdlog::debug("creating new curl context for socket {}", fd); pollHandle = new uv_poll_t; uv_poll_init(loop, pollHandle, fd); pollHandle->data = this; socketFd = fd; this->ctx = ctx; } ~curl_context() { spdlog::debug("destroying curl context for socket {}", socketFd); uv_close((uv_handle_t*)pollHandle, [](uv_handle_t *h){ delete h; }); } }; int curl_socket_cb(CURL *curl, curl_socket_t s, int action, context* ctx, void* socketPtr) { (void)curl; spdlog::debug("curl socket callback: {}", action); curl_context *curlCtx; switch (action) { case CURL_POLL_IN: case CURL_POLL_INOUT: case CURL_POLL_OUT: { curlCtx = socketPtr ? (curl_context*)socketPtr : new curl_context(uv_default_loop(), ctx, s); curl_multi_assign(ctx->curl, s, curlCtx); int events = 0; if (action != CURL_POLL_IN) events |= UV_WRITABLE; if (action != CURL_POLL_OUT) events |= UV_READABLE; uv_poll_start(curlCtx->pollHandle, events, uv_socket_cb); break; } case CURL_POLL_REMOVE: if (socketPtr) { uv_poll_stop(((curl_context*)socketPtr)->pollHandle); curl_multi_assign(ctx->curl, s, NULL); delete (curl_context*)socketPtr; } } return 0; } int curl_timeout_cb(CURLM* curl, long timeout, context* ctx) { (void)curl; spdlog::debug("curl timeout change: {} ms", timeout); if (timeout < 0) uv_timer_stop(&ctx->curlTimer); else { if (timeout == 0) timeout = 100; spdlog::debug("uv_timer_start: {}", uv_timer_start(&ctx->curlTimer, uv_curl_timeout_cb, timeout, 0)); } return 0; } void uv_socket_cb(uv_poll_t *handle, int status, int events) { spdlog::debug("libuv: socket {}", events & UV_READABLE ? (events & UV_WRITABLE ? "readable+writable" : "readable") : "writable"); (void)status; auto ctx = reinterpret_cast(handle->data); int flags = 0; if (events & UV_READABLE) flags |= CURL_CSELECT_IN; if (events & UV_WRITABLE) flags |= CURL_CSELECT_OUT; int runningHandles; curl_multi_socket_action(ctx->ctx->curl, ctx->socketFd, flags, &runningHandles); check_curl_multi_info(ctx->ctx); } void uv_curl_timeout_cb(uv_timer_t *handle) { spdlog::debug("libuv: curl timeout ended"); auto ctx = reinterpret_cast(handle->data); if (ctx) { int runningHandles; curl_multi_socket_action(ctx->curl, CURL_SOCKET_TIMEOUT, 0, &runningHandles); check_curl_multi_info(ctx); } } void check_curl_multi_info(context *ctx) { CURLMsg *message; int pending; while ((message = curl_multi_info_read(ctx->curl, &pending))) { switch (message->msg) { case CURLMSG_DONE: { spdlog::debug("curl transfer done"); curl_multi_remove_handle(ctx->curl, message->easy_handle); curl_easy_cleanup(message->easy_handle); try { active_request &req = ctx->requests.at(message->easy_handle); req.doneCallback(req); } catch (std::out_of_range) {} } default: break; } } } size_t curl_receive_cb(char *ptr, size_t size, size_t nmemb, CURL *curl) { spdlog::debug("received {} bytes from server: '{}'", nmemb, std::string(ptr, nmemb)); context *ctx = nullptr; curl_easy_getinfo(curl, CURLINFO_PRIVATE, &ctx); spdlog::debug("ctx pointer: {}", (size_t)ctx); try { active_request &req = ctx->requests.at(curl); req.receivedData.insert(req.receivedData.end(), ptr, ptr + nmemb); } catch (std::out_of_range) { spdlog::error("No associated request data for handle {}", (size_t)curl); } return nmemb; } void gc_pending_queries(uv_timer_t *handle) { context *ctx = reinterpret_cast(handle->data); std::map oldQueries(ctx->inlineQueries); std::map newQueries; auto now = std::chrono::steady_clock::now(); for (auto i = oldQueries.begin(); i != oldQueries.end(); i++) { if (i->second.ttl > now) { newQueries.insert(*i); } else { spdlog::debug("deleting a pending query {} that expired {} seconds ago", i->first, std::chrono::duration_cast(now - i->second.ttl).count()); } } ctx->inlineQueries = newQueries; }