diff --git a/packages/missgram/CMakeLists.txt b/packages/missgram/CMakeLists.txt index 3dbd0cc7..adee6d85 100644 --- a/packages/missgram/CMakeLists.txt +++ b/packages/missgram/CMakeLists.txt @@ -20,6 +20,7 @@ target_compile_features(missgram PRIVATE cxx_std_23) if(DEFINED MISSGRAM_CONFIG_FILE) target_compile_definitions(missgram PRIVATE MISSGRAM_CONFIG_FILE="${MISSGRAM_CONFIG_FILE}") endif() +target_compile_definitions(missgram PRIVATE BIU_LOGGER_SOURCE_ROOT="${CMAKE_CURRENT_SOURCE_DIR}") install(TARGETS missgram RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}) get_property(ImportedTargets DIRECTORY "${CMAKE_SOURCE_DIR}" PROPERTY IMPORTED_TARGETS) diff --git a/packages/missgram/include/missgram.hpp b/packages/missgram/include/missgram.hpp index d5c72ff2..522ebfc1 100644 --- a/packages/missgram/include/missgram.hpp +++ b/packages/missgram/include/missgram.hpp @@ -2,11 +2,6 @@ namespace missgram { - void db_write(std::string misskey_note, std::int32_t telegram_message_id); - std::optional db_read(std::string misskey_note); - - std::optional tg_send(std::string text, std::optional replyId = {}); - struct Config { std::string Secret; @@ -15,4 +10,10 @@ namespace missgram std::int16_t ServerPort; std::string dbPassword; } inline config; + struct File { std::string url; bool is_photo; bool should_hidden; }; + + void db_write(std::string misskey_note, std::int32_t telegram_message_id); + std::optional db_read(std::string misskey_note); + + std::optional tg_send(std::string text, std::optional replyId, std::vector files); } diff --git a/packages/missgram/src/main.cpp b/packages/missgram/src/main.cpp index 5aa2ce19..52c089b8 100644 --- a/packages/missgram/src/main.cpp +++ b/packages/missgram/src/main.cpp @@ -21,47 +21,88 @@ int main() { biu::Logger::try_exec([&] { + log.debug(req.body); + log.debug("{}"_f(req.headers)); + if (req.get_header_value("x-misskey-hook-secret") != config.Secret) throw std::runtime_error("Invalid secret key."); struct Content { std::string type, server; - struct + struct Body { struct Note { - std::string id, text, visibility; - std::optional replyId; + std::string id, visibility; + std::optional text, replyId; 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; } body; }; auto content = YAML::Load(req.body).as(); + log(); + + // 只考虑公开且允许联合的帖子。 if ( - content.type != "note" // 只转发 note 的情况 + content.type != "note" // 只考虑 note 的情况,这里note包括了回复、转发、引用 || !content.body.note // 大概不会发生,但还是判断一下 - || content.body.note->visibility != "public" // 只转发公开的 note + || content.body.note->visibility != "public" || content.body.note->localOnly // 只转发公开的、允许联合的帖子 || content.body.note->replyId // 不转发回复 ) return; - std::string text = content.body.note->text; - if (content.body.note->renote) - text += "\n🔁 Renote: {}/notes/{}"_f(content.server, content.body.note->renote->id); - std::thread([text, note_id = content.body.note->id] + // 接下来准备要转发的文字内容 + std::string text; + std::optional reply_id; + // 如果是转发,则直接写链接 + if (!content.body.note->text) + text = "转发帖子: [在铜锣湾查看]({}/notes/{})"_f(content.server, content.body.note->id); + // 否则(引用或普通帖子) + else { - auto message_id = tg_send(text); + text = *content.body.note->text; + // 如果有引用,则需要查找被引用的帖子是否已经被转发过,若是则直接回复被转发的消息。 + // 如果没有被转发过,则附上链接 + if (content.body.note->renote) + { + reply_id = db_read(content.body.note->renote->id); + if (!reply_id) + text += "\n引用帖子: [在铜锣湾查看]({}/notes/{})"_f(content.server, content.body.note->renote->id); + } + // 最后附上原贴地址 + 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] + { + auto message_id = tg_send(text, reply_id, files); if (message_id) db_write(note_id, *message_id); }).detach(); + // 完成 http 响应 res.status = 200; res.body = "OK"; - - log.debug(req.body); }); }); svr.listen("0.0.0.0", config.ServerPort); diff --git a/packages/missgram/src/tg.cpp b/packages/missgram/src/tg.cpp index fdad9013..f148e5cb 100644 --- a/packages/missgram/src/tg.cpp +++ b/packages/missgram/src/tg.cpp @@ -1,29 +1,89 @@ # include # include -std::optional missgram::tg_send(std::string text, std::optional replyId) +std::optional missgram::tg_send + (std::string text, std::optional replyId, std::vector files) { using namespace biu::literals; - // 准备信息 + // 整理要发送的信息 TgBot::Bot bot(config.TelegramBotToken); std::shared_ptr reply; - if (replyId) reply = std::make_shared(*replyId); - - // 发送信息,带重试机制 - TgBot::Message::Ptr message; - auto retry_delay = 1s; - int attempts = 0; - while (attempts < 5 && !message) - { - biu::Logger::try_exec([&] + if (replyId) reply = std::make_shared(*replyId, config.TelegramChatId); + auto attachs = files + | ranges::views::transform([&](auto&& file) -> TgBot::InputMedia::Ptr { - message = bot.getApi().sendMessage - (config.TelegramChatId, text, nullptr, reply); - }); - if (!message) { std::this_thread::sleep_for(retry_delay); retry_delay *= 2; attempts++; } - } + 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; - // 返回消息 ID - if (message) return message->messageId; else return {}; + // 多次尝试运行函数,直到成功或达到最大尝试次数(5次) + 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::this_thread::sleep_for(retry_delay); + retry_delay *= 2; + attempts++; + } + return std::nullopt; + }; + + // 如果没有附件,使用 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([&] + { + return bot.getApi().sendPhoto + ( + config.TelegramChatId, files[0].url, text, reply, + nullptr, "MarkdownV2", false, {}, 0, false, files[0].should_hidden + ); + }); + // 如果有多个附件,使用 sendMediaGroup 分两条消息发送,返回第一条的 id + else + { + auto message = try_run([&] { return bot.getApi().sendMessage + ( + config.TelegramChatId, text, nullptr, reply, nullptr, + "MarkdownV2" + );}); + if (message) + { + auto message2 = try_run([&] -> TgBot::Message::Ptr + { + 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 {}; + } + return message; + } }