diff --git a/.gitea/workflows/release-app.yaml b/.gitea/workflows/release-app.yaml index 13c683b..84888d6 100644 --- a/.gitea/workflows/release-app.yaml +++ b/.gitea/workflows/release-app.yaml @@ -55,12 +55,19 @@ jobs: test -f tdlib/lib/libtdjson.so - name: Build release bundle + env: + TELEGRAM_TUI_BUILD_API_ID: ${{ secrets.TELEGRAM_API_ID }} + TELEGRAM_TUI_BUILD_API_HASH: ${{ secrets.TELEGRAM_API_HASH }} run: | set -euo pipefail rm -rf build dist + test -n "${TELEGRAM_TUI_BUILD_API_ID}" + test -n "${TELEGRAM_TUI_BUILD_API_HASH}" + cmake -S . -B build \ -DCMAKE_BUILD_TYPE=Release \ + -DTELEGRAM_TUI_REQUIRE_BUILD_CREDENTIALS=ON \ -DTELEGRAM_TUI_TDLIB_ROOT="$PWD/tdlib" cmake --build build -j"$(nproc)" diff --git a/CMakeLists.txt b/CMakeLists.txt index 0799e4e..259c2e4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -12,6 +12,8 @@ find_package(Git QUIET) include(FetchContent) option(TELEGRAM_TUI_USE_SYSTEM_TDLIB "Use an installed TDLib package instead of fetching it." OFF) +option(TELEGRAM_TUI_REQUIRE_BUILD_CREDENTIALS + "Fail configure if build credentials are not provided." OFF) set(TELEGRAM_TUI_TDLIB_ROOT "" CACHE PATH "Path to a prebuilt TDLib root containing include/ and lib/ directories.") @@ -60,22 +62,46 @@ if(GIT_FOUND AND EXISTS "${CMAKE_SOURCE_DIR}/.git") set(TELEGRAM_TUI_BUILD_VERSION "${PROJECT_VERSION}+${TELEGRAM_TUI_GIT_DESCRIBE}") endif() endif() -if(TELEGRAM_TUI_APP_CONFIG_PATH AND EXISTS "${TELEGRAM_TUI_APP_CONFIG_PATH}") +if(DEFINED ENV{TELEGRAM_TUI_BUILD_API_ID} AND NOT "$ENV{TELEGRAM_TUI_BUILD_API_ID}" STREQUAL "") + set(TELEGRAM_TUI_BUILD_API_ID "$ENV{TELEGRAM_TUI_BUILD_API_ID}") +elseif(DEFINED ENV{TELEGRAM_API_ID} AND NOT "$ENV{TELEGRAM_API_ID}" STREQUAL "") + set(TELEGRAM_TUI_BUILD_API_ID "$ENV{TELEGRAM_API_ID}") +endif() + +if(DEFINED ENV{TELEGRAM_TUI_BUILD_API_HASH} AND NOT "$ENV{TELEGRAM_TUI_BUILD_API_HASH}" STREQUAL "") + set(TELEGRAM_TUI_BUILD_API_HASH "$ENV{TELEGRAM_TUI_BUILD_API_HASH}") +elseif(DEFINED ENV{TELEGRAM_API_HASH} AND NOT "$ENV{TELEGRAM_API_HASH}" STREQUAL "") + set(TELEGRAM_TUI_BUILD_API_HASH "$ENV{TELEGRAM_API_HASH}") +endif() + +if((TELEGRAM_TUI_BUILD_API_ID STREQUAL "" OR TELEGRAM_TUI_BUILD_API_HASH STREQUAL "") AND + TELEGRAM_TUI_APP_CONFIG_PATH AND EXISTS "${TELEGRAM_TUI_APP_CONFIG_PATH}") file(READ "${TELEGRAM_TUI_APP_CONFIG_PATH}" TELEGRAM_TUI_APP_CONFIG_JSON) - string(REGEX MATCH "\"api_id\"[ \t\r\n]*:[ \t\r\n]*\"?([0-9]+)\"?" - TELEGRAM_TUI_APP_CONFIG_API_ID_MATCH "${TELEGRAM_TUI_APP_CONFIG_JSON}") - if(CMAKE_MATCH_1) - set(TELEGRAM_TUI_BUILD_API_ID "${CMAKE_MATCH_1}") + if(TELEGRAM_TUI_BUILD_API_ID STREQUAL "") + string(REGEX MATCH "\"api_id\"[ \t\r\n]*:[ \t\r\n]*\"?([0-9]+)\"?" + TELEGRAM_TUI_APP_CONFIG_API_ID_MATCH "${TELEGRAM_TUI_APP_CONFIG_JSON}") + if(CMAKE_MATCH_1) + set(TELEGRAM_TUI_BUILD_API_ID "${CMAKE_MATCH_1}") + endif() endif() - string(REGEX MATCH "\"api_hash\"[ \t\r\n]*:[ \t\r\n]*\"([^\"]+)\"" - TELEGRAM_TUI_APP_CONFIG_API_HASH_MATCH "${TELEGRAM_TUI_APP_CONFIG_JSON}") - if(CMAKE_MATCH_1) - set(TELEGRAM_TUI_BUILD_API_HASH "${CMAKE_MATCH_1}") + if(TELEGRAM_TUI_BUILD_API_HASH STREQUAL "") + string(REGEX MATCH "\"api_hash\"[ \t\r\n]*:[ \t\r\n]*\"([^\"]+)\"" + TELEGRAM_TUI_APP_CONFIG_API_HASH_MATCH "${TELEGRAM_TUI_APP_CONFIG_JSON}") + if(CMAKE_MATCH_1) + set(TELEGRAM_TUI_BUILD_API_HASH "${CMAKE_MATCH_1}") + endif() endif() endif() +if(TELEGRAM_TUI_REQUIRE_BUILD_CREDENTIALS AND + (TELEGRAM_TUI_BUILD_API_ID STREQUAL "" OR TELEGRAM_TUI_BUILD_API_HASH STREQUAL "")) + message(FATAL_ERROR + "Build credentials are required. Set TELEGRAM_TUI_BUILD_API_ID and " + "TELEGRAM_TUI_BUILD_API_HASH (or TELEGRAM_API_ID/TELEGRAM_API_HASH).") +endif() + configure_file( ${CMAKE_SOURCE_DIR}/src/build_config.h.in ${CMAKE_CURRENT_BINARY_DIR}/build_config.h diff --git a/README.md b/README.md index fd91a3e..6ee85d6 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,9 @@ cmake --build build -j During configure, CMake also checks the app config at `$XDG_DATA_HOME/telegram-tui/config.json` or `~/.local/share/telegram-tui/config.json`. If that file contains `api_id` and `api_hash`, they are embedded into the build. +For CI or release builds, prefer setting `TELEGRAM_TUI_BUILD_API_ID` and +`TELEGRAM_TUI_BUILD_API_HASH` in the environment so credentials come from secrets instead of +local config. ## Run @@ -68,7 +71,8 @@ which downloads a prebuilt TDLib bundle from the `shinoa-tdlib` repository using version tag such as `v1.8.63`, builds a rolling `latest` app release, and publishes an archive containing `usr/bin/shinoa` plus the bundled `usr/lib/libtdjson.so*`. The root `PKGBUILD` installs that prebuilt release as -`shinoa-bin`. +`shinoa-bin`. That workflow expects Gitea secrets named `TELEGRAM_API_ID` and +`TELEGRAM_API_HASH`. Release builds are configured to fail if those secrets are missing. To prepare the TDLib bundle on your own machine: diff --git a/src/app.cpp b/src/app.cpp index 33bba9f..a0d3140 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -1,5 +1,9 @@ #include "app.h" +#include +#include +#include + #include #include "build_config.h" @@ -14,6 +18,66 @@ bool is_truthy_env_value(const std::string &value) { value == "YES" || value == "on" || value == "ON"; } +std::string shell_quote(const std::string &value) { + std::string quoted = "'"; + for (char ch : value) { + if (ch == '\'') { + quoted += "'\\''"; + } else { + quoted.push_back(ch); + } + } + quoted.push_back('\''); + return quoted; +} + +std::string run_command_capture(const std::string &command) { + FILE *pipe = popen(command.c_str(), "r"); + if (pipe == nullptr) { + return {}; + } + + std::string output; + std::array buffer{}; + while (std::fgets(buffer.data(), static_cast(buffer.size()), pipe) != nullptr) { + output += buffer.data(); + } + if (pclose(pipe) != 0) { + return {}; + } + return output; +} + +std::optional fetch_update_notice() { + static constexpr const char *kLatestReleaseApiUrl = + "https://git.mshq.dev/api/v1/repos/AxiFisk/shinoa/releases/tags/latest"; + + if (std::string(TELEGRAM_TUI_BUILD_COMMIT).empty()) { + return std::nullopt; + } + + const std::string response = + run_command_capture("curl -fsSL " + shell_quote(kLatestReleaseApiUrl) + " 2>/dev/null"); + if (response.empty()) { + return std::nullopt; + } + + try { + const json release = json::parse(response, nullptr, true, true); + const std::string target_commit = safe_string(release, "target_commitish"); + if (target_commit.empty()) { + return std::nullopt; + } + const std::string current_commit = TELEGRAM_TUI_BUILD_COMMIT; + if (target_commit == current_commit || target_commit.rfind(current_commit, 0) == 0) { + return std::nullopt; + } + return std::string("Update available"); + } catch (const json::exception &) { + return std::nullopt; + } +} + } // namespace App::App() { @@ -49,6 +113,8 @@ App::App() { if (use_test_dc_) { status_line_ = "Starting TDLib in test DC mode..."; } + + start_update_check(); } int App::run() { @@ -80,6 +146,27 @@ int App::run() { return 0; } +void App::start_update_check() { + update_check_future_ = + std::async(std::launch::async, []() { return fetch_update_notice(); }); +} + +bool App::refresh_update_check_result() { + if (!update_check_future_.valid()) { + return false; + } + if (update_check_future_.wait_for(std::chrono::seconds(0)) != std::future_status::ready) { + return false; + } + + const auto notice = update_check_future_.get(); + if (notice == update_notice_) { + return false; + } + update_notice_ = notice.value_or(std::string()); + return true; +} + std::optional App::highlighted_chat_id() const { if (sorted_chat_ids_.empty()) { return std::nullopt; diff --git a/src/app.h b/src/app.h index b650366..b00f2c9 100644 --- a/src/app.h +++ b/src/app.h @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -38,6 +39,8 @@ class App { void init_curses(); void shutdown_curses(); + void start_update_check(); + bool refresh_update_check_result(); bool process_updates(); void handle_td_object(const json &object); void handle_authorization_state(); @@ -197,6 +200,7 @@ class App { std::string input_prompt_; std::string input_buffer_; std::string status_line_ = "Starting TDLib..."; + std::string update_notice_; std::string attachment_preview_graphics_data_; std::string attachment_viewer_title_; std::string attachment_preview_signature_; @@ -217,6 +221,7 @@ class App { std::size_t attachment_viewer_frame_index_ = 0; std::string attachment_viewer_send_caption_; std::vector forward_message_ids_; + std::future> update_check_future_; }; } // namespace telegram_tui diff --git a/src/app_shell.cpp b/src/app_shell.cpp index 4682d96..3311186 100644 --- a/src/app_shell.cpp +++ b/src/app_shell.cpp @@ -96,6 +96,18 @@ void App::draw() { mvprintw(header_y, 1, "%s", header_label.c_str()); const std::string auth_label = authorized_ ? "ready" : current_auth_label(); const int auth_x = std::max(1, width - static_cast(auth_label.size()) - 2); + if (!update_notice_.empty()) { + const std::string centered_notice = + truncate_to_width(update_notice_, std::max(1, width - 4)); + const int notice_x = + std::max(1, (width - static_cast(centered_notice.size())) / 2); + if (notice_x > 1 + static_cast(header_label.size()) && + notice_x + static_cast(centered_notice.size()) < auth_x - 1) { + attron(A_BOLD); + mvprintw(header_y, notice_x, "%s", centered_notice.c_str()); + attroff(A_BOLD); + } + } mvprintw(header_y, auth_x, "%s", auth_label.c_str()); attroff(A_REVERSE); diff --git a/src/app_state.cpp b/src/app_state.cpp index 57cb0e3..b93c7d4 100644 --- a/src/app_state.cpp +++ b/src/app_state.cpp @@ -25,7 +25,7 @@ std::string format_download_progress(std::int64_t downloaded_size, std::int64_t } // namespace bool App::process_updates() { - bool changed = false; + bool changed = refresh_update_check_result(); while (true) { auto update = td_.receive(0.0); if (!update.has_value()) { diff --git a/src/build_config.h.in b/src/build_config.h.in index 0ee5fb3..2730165 100644 --- a/src/build_config.h.in +++ b/src/build_config.h.in @@ -1,5 +1,6 @@ #pragma once +#define TELEGRAM_TUI_BUILD_COMMIT "@TELEGRAM_TUI_BUILD_COMMIT@" #define TELEGRAM_TUI_BUILD_VERSION "@TELEGRAM_TUI_BUILD_VERSION@" #define TELEGRAM_TUI_BUILD_API_ID "@TELEGRAM_TUI_BUILD_API_ID@" #define TELEGRAM_TUI_BUILD_API_HASH "@TELEGRAM_TUI_BUILD_API_HASH@"