diff --git a/src/tools/edit_message_text.cpp b/src/tools/edit_message_text.cpp index 7aaa356..2a061f2 100644 --- a/src/tools/edit_message_text.cpp +++ b/src/tools/edit_message_text.cpp @@ -9,7 +9,7 @@ OpenAITools::Tool tools::editMessageText(_ telegram, _ chat) { return { .name = "edit_message_text", - .description = "Edits the text of a previously sent message in \"{}\" chat.\n" + .description = "Edits the text (or the caption of a photo/media message) previously sent in \"{}\" chat.\n" "You should use this to fix a typo or correct a mistake in a message you already sent."_format(chat->title_), .parameters = { .properties = { @@ -22,18 +22,27 @@ OpenAITools::Tool tools::editMessageText(_ telegram, _getMessage(chat->id_, messageId))->id_; + // remap client-side messageId (reported to the llm) to the server-side messageId, and + // inspect the message so we edit plain text vs. media captions (photo/video) correctly. + auto message = co_await telegram->getMessage(chat->id_, messageId); + messageId = message->id_; - auto content = td::td_api::make_object(); - content->text_ = [&] { - auto t = td::td_api::make_object(); - t->text_ = text.toStdString(); - return t; - }(); + auto formatted = td::td_api::make_object(); + formatted->text_ = text.toStdString(); - co_await telegram->sendQueryWithResult( - ITelegramClient::toPtr(td::td_api::editMessageText(chat->id_, messageId, nullptr, std::move(content)))); + const bool isPlainText = + message->content_ && message->content_->get_id() == td::td_api::messageText::ID; + + if (isPlainText) { + auto content = td::td_api::make_object(); + content->text_ = std::move(formatted); + co_await telegram->sendQueryWithResult(ITelegramClient::toPtr( + td::td_api::editMessageText(chat->id_, messageId, nullptr, std::move(content)))); + } else { + // Photo/video/other media message: TDLib rejects editMessageText here, edit the caption. + co_await telegram->sendQueryWithResult(ITelegramClient::toPtr( + td::td_api::editMessageCaption(chat->id_, messageId, nullptr, std::move(formatted), false))); + } co_return "Message {} was edited successfully."_format(messageId); }, diff --git a/src/tools/react_with_emoji.cpp b/src/tools/react_with_emoji.cpp index 3b050b2..0c70567 100644 --- a/src/tools/react_with_emoji.cpp +++ b/src/tools/react_with_emoji.cpp @@ -6,6 +6,16 @@ #include "util/json_utils.h" +// Telegram stores reaction emojis with or without the U+FE0F variation selector (e.g. "❤️" vs "❤", +// "⚡️" vs "⚡"). Strip it so the model's emoji matches the chat's allowed-reaction list reliably. +static std::string stripVariationSelector(std::string s) { + static const std::string kVs16 = "\xEF\xB8\x8F"; // U+FE0F in UTF-8 + for (auto pos = s.find(kVs16); pos != std::string::npos; pos = s.find(kVs16)) { + s.erase(pos, kVs16.size()); + } + return s; +} + OpenAITools::Tool tools::reactWithEmoji(_ telegram, _ chat) { return { .name = "react_with_emoji", @@ -27,14 +37,54 @@ OpenAITools::Tool tools::reactWithEmoji(_ telegram, _available_reactions_.get(); + avail && avail->get_id() == td::td_api::chatAvailableReactionsSome::ID) { + const auto* some = static_cast(avail); + const auto wanted = stripVariationSelector(emoji.toStdString()); + AStringVector allowed; + bool ok = false; + for (const auto& r : some->reactions_) { + if (r && r->get_id() == td::td_api::reactionTypeEmoji::ID) { + const auto& e = static_cast(r.get())->emoji_; + allowed << AString::fromUtf8(e); + if (stripVariationSelector(e) == wanted) { + ok = true; + } + } + } + if (!ok) { + if (allowed.empty()) { + co_return "Reactions are disabled for this message - you can't react here. Just skip it, this is not an error."; + } + co_return "The reaction {} isn't available for this message. Allowed reactions here: {}. Pick one of those, or just skip reacting."_format( + emoji, allowed.join(' ')); + } + } + auto reaction = td::td_api::make_object(); reaction->chat_id_ = chat->id_; reaction->message_id_ = messageId; - reaction->reaction_type_ = td::td_api::make_object(emoji.toStdString()); + // Telegram's active emoji reactions are stored WITHOUT the U+FE0F variation selector + // (e.g. the heart reaction is "❤" = U+2764, not "❤️" = U+2764 U+FE0F). Sending the + // FE0F form makes addMessageReaction fail with REACTION_INVALID ("The reaction isn't + // available for the message") - this is exactly why ❤️/⚡️ failed while single-codepoint + // emojis (🔥 👍 🤔 …) worked. Strip it so the byte form matches the server's. + reaction->reaction_type_ = td::td_api::make_object(stripVariationSelector(emoji.toStdString())); reaction->is_big_ = false; reaction->update_recent_reactions_ = true; - co_await telegram->sendQueryWithResult(std::move(reaction)); + try { + co_await telegram->sendQueryWithResult(std::move(reaction)); + } catch (const AException& e) { + // Residual per-message restrictions or transient state. Report gracefully so the model + // treats it as "can't react to this one" instead of "reactions are globally broken". + co_return "Couldn't add the reaction {} to this message ({}). It happens with some messages - just skip reacting here, it's not a real error."_format( + emoji, e.getMessage()); + } co_return "Reaction {} added successfully."_format(emoji); }, };