diff --git a/packages/biu/include/biu/common.hpp b/packages/biu/include/biu/common.hpp index 7ae93090..096ac4f5 100644 --- a/packages/biu/include/biu/common.hpp +++ b/packages/biu/include/biu/common.hpp @@ -19,7 +19,7 @@ namespace biu namespace common { std::size_t hash(auto&&... objs); - [[gnu::always_inline]] void unused(auto&&...); + [[gnu::always_inline]] inline void unused(auto&&...); [[noreturn]] void block_forever(); bool is_interactive(); diff --git a/packages/biu/include/biu/logger.hpp b/packages/biu/include/biu/logger.hpp index 5c959cf4..cbb36383 100644 --- a/packages/biu/include/biu/logger.hpp +++ b/packages/biu/include/biu/logger.hpp @@ -41,10 +41,10 @@ namespace biu protected: const std::chrono::time_point CreateTime_; // call log("create {type} at {address}."); - protected: [[gnu::always_inline]] ObjectMonitor(); + protected: [[gnu::always_inline]] inline ObjectMonitor(); // call log("destroy {type} at {address} after {duration} ms."); - protected: [[gnu::always_inline]] virtual ~ObjectMonitor(); + protected: [[gnu::always_inline]] inline virtual ~ObjectMonitor(); }; template friend class ObjectMonitor; @@ -65,7 +65,7 @@ namespace biu // if sizeof...(Param) > 0, call log("begin function with {arguments}."); // else call log("begin function."); - public: template [[gnu::always_inline]] explicit Guard(Param&&... param); + public: template [[gnu::always_inline]] inline explicit Guard(Param&&... param); // call log("end function after {duration} ms.") public: [[gnu::always_inline]] inline virtual ~Guard(); @@ -74,12 +74,12 @@ namespace biu public: [[gnu::always_inline]] inline void operator()() const; // call log("return {return} after {duration} ms.") - public: template [[gnu::always_inline]] T rtn(T&& value) const; + public: template [[gnu::always_inline]] inline T rtn(T&& value) const; // print the following message if LoggerConfig_ is set and the level is higher than the level of the // LoggerConfig_ // [ {time} {thread} {indent} {filename}:{line} {function_name} ] {message} - public: template [[gnu::always_inline]] void log(const std::string& message) const; + public: template [[gnu::always_inline]] inline void log(const std::string& message) const; public: [[gnu::always_inline]] inline void error(const std::string& message) const; public: [[gnu::always_inline]] inline void info(const std::string& message) const; public: [[gnu::always_inline]] inline void debug(const std::string& message) const; diff --git a/packages/missgram/CMakeLists.txt b/packages/missgram/CMakeLists.txt index adee6d85..61011287 100644 --- a/packages/missgram/CMakeLists.txt +++ b/packages/missgram/CMakeLists.txt @@ -12,10 +12,11 @@ endif() find_package(biu REQUIRED) find_package(httplib REQUIRED) find_package(sqlgen REQUIRED) +find_package(nlohmann_json REQUIRED) add_executable(missgram src/main.cpp src/db.cpp src/tg.cpp) target_include_directories(missgram PRIVATE $) -target_link_libraries(missgram PRIVATE biu::biu httplib::httplib sqlgen::sqlgen) +target_link_libraries(missgram PRIVATE biu::biu httplib::httplib sqlgen::sqlgen nlohmann_json::nlohmann_json) target_compile_features(missgram PRIVATE cxx_std_23) if(DEFINED MISSGRAM_CONFIG_FILE) target_compile_definitions(missgram PRIVATE MISSGRAM_CONFIG_FILE="${MISSGRAM_CONFIG_FILE}") @@ -23,6 +24,15 @@ endif() target_compile_definitions(missgram PRIVATE BIU_LOGGER_SOURCE_ROOT="${CMAKE_CURRENT_SOURCE_DIR}") install(TARGETS missgram RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}) +add_executable(tg-test EXCLUDE_FROM_ALL src/tg.cpp src/tg-test.cpp) +target_include_directories(tg-test PRIVATE $) +target_link_libraries(tg-test PRIVATE biu::biu httplib::httplib nlohmann_json::nlohmann_json) +target_compile_features(tg-test PRIVATE cxx_std_23) +if(DEFINED MISSGRAM_CONFIG_FILE) + target_compile_definitions(tg-test PRIVATE MISSGRAM_CONFIG_FILE="${MISSGRAM_CONFIG_FILE}") +endif() +target_compile_definitions(tg-test PRIVATE BIU_LOGGER_SOURCE_ROOT="${CMAKE_CURRENT_SOURCE_DIR}") + get_property(ImportedTargets DIRECTORY "${CMAKE_SOURCE_DIR}" PROPERTY IMPORTED_TARGETS) message("Imported targets: ${ImportedTargets}") message("List of compile features: ${CMAKE_CXX_COMPILE_FEATURES}") diff --git a/packages/missgram/default.nix b/packages/missgram/default.nix index 4bc098da..3382c827 100644 --- a/packages/missgram/default.nix +++ b/packages/missgram/default.nix @@ -1,8 +1,8 @@ -{ lib, stdenv, cmake, pkg-config, biu, configFile ? null, httplib, sqlgen }: stdenv.mkDerivation +{ lib, stdenv, cmake, pkg-config, biu, configFile ? null, httplib, sqlgen, nlohmann_json }: stdenv.mkDerivation { name = "missgram"; src = ./.; - buildInputs = [ biu httplib sqlgen ]; + buildInputs = [ biu httplib sqlgen nlohmann_json ]; nativeBuildInputs = [ cmake pkg-config ]; cmakeFlags = lib.optional (configFile != null) [ "-DMISSGRAM_CONFIG_FILE=${configFile}" ]; } diff --git a/packages/missgram/include/missgram.hpp b/packages/missgram/include/missgram.hpp index 522ebfc1..4fbbd650 100644 --- a/packages/missgram/include/missgram.hpp +++ b/packages/missgram/include/missgram.hpp @@ -10,7 +10,7 @@ namespace missgram std::int16_t ServerPort; std::string dbPassword; } inline config; - struct File { std::string url; bool is_photo; bool should_hidden; }; + struct File { std::string name, url, type; bool isSensitive; }; void db_write(std::string misskey_note, std::int32_t telegram_message_id); std::optional db_read(std::string misskey_note); diff --git a/packages/missgram/src/main.cpp b/packages/missgram/src/main.cpp index f50cc1b0..b68ad720 100644 --- a/packages/missgram/src/main.cpp +++ b/packages/missgram/src/main.cpp @@ -39,7 +39,6 @@ int main() struct Renote { std::string id; }; std::optional renote; bool localOnly; - struct File { bool isSensitive; std::string url; std::string type; }; std::vector files; }; std::optional note; @@ -66,7 +65,7 @@ int main() // 否则(引用或普通帖子) else { - text = *content.body.note->text; + text = content.body.note->text.value_or(""); // 如果有引用,则需要查找被引用的帖子是否已经被转发过,若是则直接回复被转发的消息。 // 如果没有被转发过,则在开头附上链接 if (content.body.note->renote) @@ -82,21 +81,9 @@ int main() text += "\n[在联邦宇宙查看]({}/notes/{})"_f(content.server, content.body.note->id); } - // 接下来整理要转发的文件 - auto files = content.body.note->files | ranges::views::transform([](auto&& file) -> File - { - return File - { - .url = file.url, - .is_photo = file.type.starts_with("image/"), - .should_hidden = file.isSensitive - }; - }) | ranges::to_vector; - - log(); - // 异步发送消息 - std::thread([text, note_id = content.body.note->id, reply_id, files] + std::thread([text, note_id = content.body.note->id, reply_id, + files = content.body.note->files] { auto message_id = tg_send(text, reply_id, files); if (message_id) db_write(note_id, *message_id); diff --git a/packages/missgram/src/tg-test.cpp b/packages/missgram/src/tg-test.cpp new file mode 100644 index 00000000..a1a491f5 --- /dev/null +++ b/packages/missgram/src/tg-test.cpp @@ -0,0 +1,25 @@ +# include + +# ifndef MISSGRAM_CONFIG_FILE +# define MISSGRAM_CONFIG_FILE "./config.yaml" +# endif + +int main() +{ + using namespace biu::literals; + using namespace missgram; + biu::Logger::Guard log; + + config = YAML::LoadFile(MISSGRAM_CONFIG_FILE).as(); + // tg_send("aaaa", std::nullopt, {}); + // tg_send("aaaa", std::nullopt, {{"IMG20241013173523.jpg", "https://xn--s8w913fdga.chn.moe/files/3dd41113-4df5-4f34-a825-e4137d146172", "image/jpeg", false}}); + tg_send("aaaa", std::nullopt, + { + {"2026-01-07 22-02-22 1.png", "https://xn--s8w913fdga.chn.moe/files/a23d13ea-de37-4907-9d54-66417d7e0e36", "image/png", false} + }); + // tg_send("aaaa", std::nullopt, + // { + // {"IMG20241013173523.jpg", "https://xn--s8w913fdga.chn.moe/files/// 3dd41113-4df5-4f34-a825-e4137d146172", "image/jpeg", true}, + // {"2026-01-07 22-02-22 1.png", "https://xn--s8w913fdga.chn.moe/files/// a23d13ea-de37-4907-9d54-66417d7e0e36", "image/png", false} + // }); +} diff --git a/packages/missgram/src/tg.cpp b/packages/missgram/src/tg.cpp index af79471b..84ee0c10 100644 --- a/packages/missgram/src/tg.cpp +++ b/packages/missgram/src/tg.cpp @@ -1,5 +1,6 @@ # include -# include +# include +# include std::optional missgram::tg_send (std::string text, std::optional replyId, std::vector files) @@ -7,84 +8,121 @@ std::optional missgram::tg_send using namespace biu::literals; biu::Logger::Guard log; - // 整理要发送的信息 - TgBot::Bot bot(config.TelegramBotToken); - std::shared_ptr reply; - if (replyId) reply = std::make_shared(*replyId, config.TelegramChatId); - auto attachs = files - | ranges::views::transform([&](auto&& file) -> TgBot::InputMedia::Ptr - { - if (file.is_photo) - { - auto pic = std::make_shared(); - pic->media = file.url; - pic->hasSpoiler = file.should_hidden; - return pic; - } - else - { - auto doc = std::make_shared(); - doc->media = file.url; - return doc; - } - }) - | ranges::to_vector; - // 多次尝试运行函数,直到成功或达到最大尝试次数(5次) - auto try_run = [&](auto&& func) -> std::optional + auto try_run = [&](auto&& func) -> std::optional { auto retry_delay = 1s; int attempts = 0; while (attempts < 5) { - TgBot::Message::Ptr message; - biu::Logger::try_exec([&] { message = func(); }); - if (message) return message->messageId; + std::optional result; + biu::Logger::try_exec([&] { result = func(); }); + if (result) return result; std::this_thread::sleep_for(retry_delay); retry_delay *= 2; attempts++; } - return std::nullopt; + return {}; }; - // 如果没有附件,使用 sendMessage 发送文本消息 - if (attachs.empty()) return try_run([&] { return bot.getApi().sendMessage - ( - config.TelegramChatId, text, nullptr, reply, nullptr, - "MarkdownV2" - );}); - // 如果只有一个附件并且是图片,使用 sendPhoto 发送 - else if (attachs.size() == 1 && files[0].is_photo) return try_run([&] + // 下载一个 https 资源到内存 + auto download = [&](const std::string& url) -> std::string { - return bot.getApi().sendPhoto - ( - config.TelegramChatId, files[0].url, text, reply, - nullptr, "MarkdownV2", false, {}, 0, false, files[0].should_hidden - ); - }); - // 如果有多个附件,使用 sendMediaGroup 分两条消息发送,返回第一条的 id + std::regex https_regex(R"(https://([^/]+)(/.+))"); + std::smatch match; + if (!std::regex_match(url, match, https_regex)) + throw std::runtime_error("Only https URLs are supported"); + httplib::SSLClient cli(match[1].str()); + auto res = cli.Get(match[2].str()); + if (res && res->status == 200) return res->body; + else throw std::runtime_error("Failed to download file from " + url); + }; + + // 下载要发送的文件 + std::vector file_contents; + for (const auto& file : files) + { + auto content = try_run([&] { return download(file.url); }); + if (!content) throw std::runtime_error("Failed to download file from " + file.url); + file_contents.push_back(std::move(*content)); + } + + // 准备要发送的请求 + httplib::UploadFormDataItems items; + std::string method; + if (files.empty()) + { + method = "sendMessage"; + items.push_back({"text", text}); + items.push_back({"parse_mode", "MarkdownV2"}); + items.push_back({"link_preview_options", + []{ nlohmann::json j; j["is_disabled"] = true; return j.dump(); }()}); + } + else if (files.size() == 1) + { + auto is_photo = files[0].type.starts_with("image/"); + method = is_photo ? "sendPhoto" : "sendDocument"; + items.push_back + ({ + is_photo ? "photo" : "document", + file_contents[0], files[0].name, files[0].type + }); + items.push_back({"caption", text}); + items.push_back({"parse_mode", "MarkdownV2"}); + if (is_photo && files[0].isSensitive) items.push_back({"has_spoiler", "True"}); + } else { - auto message = try_run([&] { return bot.getApi().sendMessage - ( - config.TelegramChatId, text, nullptr, reply, nullptr, - "MarkdownV2" - );}); - if (message) + method = "sendMediaGroup"; + auto all_photo = ranges::all_of(files, + [](auto&& file) { return file.type.starts_with("image/"); }); + nlohmann::json media_group = files | ranges::views::enumerate | ranges::views::transform([&](auto&& file) { - auto message2 = try_run([&] -> TgBot::Message::Ptr + nlohmann::json params; + if (all_photo) { - auto msg = bot.getApi().sendMediaGroup - ( - config.TelegramChatId, attachs, false, - std::make_shared(*message, config.TelegramChatId) - ); - if (msg.empty() || !ranges::all_of(msg, [](auto&& m) { return bool(m); })) - return nullptr; - else return msg[0]; - }); - if (!message2) return {}; + params["type"] = "photo"; + if (file.second.isSensitive) params["has_spoiler"] = true; + } + else params["type"] = "document"; + params["media"] = "attach://media-{}"_f(file.first); + log.debug("Prepared media {}: {}"_f(file.first, params.dump())); + if (file.first == 0) { params["caption"] = text; params["parse_mode"] = "MarkdownV2"; } + return params; + }) | ranges::to_vector; + items.push_back({"media", media_group.dump()}); + for (int i = 0; i < files.size(); i++) items.push_back + ({"media-{}"_f(i), file_contents[i], files[i].name, files[i].type}); + } + items.push_back({"chat_id", std::to_string(config.TelegramChatId)}); + if (replyId) items.push_back({"reply_parameters", [&] + { + nlohmann::json j; + j["message_id"] = *replyId; + j["chat_id"] = config.TelegramChatId; + return j.dump(); + }()}); + + items.push_back({"disable_notification", "True"}); + httplib::Client cli("https://api.telegram.org"); + auto result = cli.Post("/bot{}/{}"_f(config.TelegramBotToken, method), items); + log.debug("{} {} {}"_f(result->status, result->body, result->headers)); + if (result && result->status == 200) + { + auto json = nlohmann::json::parse(result->body); + // 测试 js["result"]["message_id"] 是否存在且为整数 + if (json.contains("result") && json["result"].contains("message_id") + && json["result"]["message_id"].is_number_integer()) + return json["result"]["message_id"].get(); + else + { + log.error("Telegram API error: {}"_f(json.dump())); + return {}; } - return message; + } + else + { + log.error("HTTP error: {}"_f(result ? std::to_string(result->status) : "No response")); + return {}; } }