From c6c1498094bc0a1e98340459517a6e04901f4f2e Mon Sep 17 00:00:00 2001 From: Gabriel Monteiro Tomasin Date: Tue, 7 Jul 2026 07:56:21 -0300 Subject: [PATCH 1/2] feat: add messageRichMessage support nchat was showing [UnknownMessage ] for Telegram rich messages (tables, lists, code blocks, collapsible details, etc) because TdMessageContentConvert() did not handle the messageRichMessage type. This adds: - GetRichText(): recursively extracts plain text from all RichText variants (bold, italic, links, mentions, custom emoji, etc) - AppendPageBlock(): renders PageBlock types as terminal-friendly text: - Tables -> ASCII aligned with borders - Lists -> bullets with indentation - Code blocks -> preserved formatting - Block quotes -> '> ' prefix - Collapsible details -> header + indented content - Dividers, headers, subheaders, paragraphs - GetRichMessageText(): assembles all blocks into final text - New else-if clause in TdMessageContentConvert() to dispatch messageRichMessage to the new renderer Co-authored-by: Claude Code --- lib/tgchat/src/tgchat.cpp | 305 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 305 insertions(+) diff --git a/lib/tgchat/src/tgchat.cpp b/lib/tgchat/src/tgchat.cpp index f54c3a62..40b278d5 100644 --- a/lib/tgchat/src/tgchat.cpp +++ b/lib/tgchat/src/tgchat.cpp @@ -192,6 +192,9 @@ class TgChat::Impl void AddMentionEntities(td::td_api::object_ptr& p_FormattedText, const std::vector& p_MentionInfos); std::string GetText(td::td_api::object_ptr&& p_FormattedText); + std::string GetRichText(const td::td_api::RichText* p_RichText); + void AppendPageBlock(const td::td_api::PageBlock& p_Block, std::ostringstream& p_Oss, const std::string& p_Indent); + std::string GetRichMessageText(const td::td_api::richMessage& p_RichMessage); void TdMessageContentConvert(td::td_api::MessageContent& p_TdMessageContent, int64_t p_SenderId, std::string& p_Text, std::string& p_FileInfo); void TdMessageConvert(td::td_api::message& p_TdMessage, ChatMessage& p_ChatMessage); @@ -3239,6 +3242,300 @@ std::string TgChat::Impl::GetText(td::td_api::object_ptrget_id()) + { + case td::td_api::richTextPlain::ID: + return static_cast(p_RichText)->text_; + case td::td_api::richTextCustomEmoji::ID: + return static_cast(p_RichText)->alternative_text_; + case td::td_api::richTextMathematicalExpression::ID: + return static_cast(p_RichText)->expression_; + case td::td_api::richTextIcon::ID: + case td::td_api::richTextAnchor::ID: + return ""; + case td::td_api::richTexts::ID: + { + std::string text; + for (const auto& sub : static_cast(p_RichText)->texts_) + { + text += GetRichText(sub.get()); + } + return text; + } + case td::td_api::richTextBold::ID: + return GetRichText(static_cast(p_RichText)->text_.get()); + case td::td_api::richTextItalic::ID: + return GetRichText(static_cast(p_RichText)->text_.get()); + case td::td_api::richTextUnderline::ID: + return GetRichText(static_cast(p_RichText)->text_.get()); + case td::td_api::richTextStrikethrough::ID: + return GetRichText(static_cast(p_RichText)->text_.get()); + case td::td_api::richTextSpoiler::ID: + return GetRichText(static_cast(p_RichText)->text_.get()); + case td::td_api::richTextSubscript::ID: + return GetRichText(static_cast(p_RichText)->text_.get()); + case td::td_api::richTextSuperscript::ID: + return GetRichText(static_cast(p_RichText)->text_.get()); + case td::td_api::richTextMarked::ID: + return GetRichText(static_cast(p_RichText)->text_.get()); + case td::td_api::richTextFixed::ID: + return GetRichText(static_cast(p_RichText)->text_.get()); + case td::td_api::richTextDateTime::ID: + return GetRichText(static_cast(p_RichText)->text_.get()); + case td::td_api::richTextMention::ID: + return GetRichText(static_cast(p_RichText)->text_.get()); + case td::td_api::richTextHashtag::ID: + return GetRichText(static_cast(p_RichText)->text_.get()); + case td::td_api::richTextCashtag::ID: + return GetRichText(static_cast(p_RichText)->text_.get()); + case td::td_api::richTextBotCommand::ID: + return GetRichText(static_cast(p_RichText)->text_.get()); + case td::td_api::richTextMentionName::ID: + return GetRichText(static_cast(p_RichText)->text_.get()); + case td::td_api::richTextUrl::ID: + return GetRichText(static_cast(p_RichText)->text_.get()); + case td::td_api::richTextEmailAddress::ID: + return GetRichText(static_cast(p_RichText)->text_.get()); + case td::td_api::richTextPhoneNumber::ID: + return GetRichText(static_cast(p_RichText)->text_.get()); + case td::td_api::richTextBankCardNumber::ID: + return GetRichText(static_cast(p_RichText)->text_.get()); + case td::td_api::richTextAnchorLink::ID: + return GetRichText(static_cast(p_RichText)->text_.get()); + case td::td_api::richTextReferenceLink::ID: + return GetRichText(static_cast(p_RichText)->text_.get()); + case td::td_api::richTextReference::ID: + return GetRichText(static_cast(p_RichText)->text_.get()); + default: + return ""; + } +} + +void TgChat::Impl::AppendPageBlock(const td::td_api::PageBlock& p_Block, std::ostringstream& p_Oss, + const std::string& p_Indent) +{ + switch (p_Block.get_id()) + { + case td::td_api::pageBlockTitle::ID: + p_Oss << p_Indent << GetRichText(static_cast(p_Block).title_.get()) << "\n"; + break; + case td::td_api::pageBlockSubtitle::ID: + p_Oss << p_Indent << GetRichText(static_cast(p_Block).subtitle_.get()) << "\n"; + break; + case td::td_api::pageBlockAuthorDate::ID: + p_Oss << p_Indent << GetRichText(static_cast(p_Block).author_.get()) << "\n"; + break; + case td::td_api::pageBlockHeader::ID: + p_Oss << p_Indent << GetRichText(static_cast(p_Block).header_.get()) << "\n"; + break; + case td::td_api::pageBlockSubheader::ID: + p_Oss << p_Indent << GetRichText(static_cast(p_Block).subheader_.get()) << "\n"; + break; + case td::td_api::pageBlockSectionHeading::ID: + p_Oss << p_Indent << GetRichText(static_cast(p_Block).text_.get()) << "\n"; + break; + case td::td_api::pageBlockKicker::ID: + p_Oss << p_Indent << GetRichText(static_cast(p_Block).kicker_.get()) << "\n"; + break; + case td::td_api::pageBlockParagraph::ID: + p_Oss << p_Indent << GetRichText(static_cast(p_Block).text_.get()) << "\n"; + break; + case td::td_api::pageBlockFooter::ID: + p_Oss << p_Indent << GetRichText(static_cast(p_Block).footer_.get()) << "\n"; + break; + case td::td_api::pageBlockPreformatted::ID: + { + const auto& block = static_cast(p_Block); + std::string code = GetRichText(block.text_.get()); + std::string line; + for (char c : code) + { + if (c == '\n') + { + p_Oss << p_Indent << line << "\n"; + line.clear(); + } + else + { + line += c; + } + } + if (!line.empty()) + { + p_Oss << p_Indent << line << "\n"; + } + break; + } + case td::td_api::pageBlockDivider::ID: + p_Oss << p_Indent << "---\n"; + break; + case td::td_api::pageBlockMathematicalExpression::ID: + p_Oss << p_Indent << static_cast(p_Block).expression_ << "\n"; + break; + case td::td_api::pageBlockList::ID: + { + const auto& list = static_cast(p_Block); + for (const auto& item : list.items_) + { + if (!item) continue; + std::string label = item->label_.empty() ? std::string("-") : item->label_; + p_Oss << p_Indent << label << " "; + std::string itemIndent = p_Indent + std::string(label.size() + 1, ' '); + for (const auto& sub : item->blocks_) + { + if (sub) AppendPageBlock(*sub, p_Oss, itemIndent); + } + } + break; + } + case td::td_api::pageBlockBlockQuote::ID: + { + const auto& quote = static_cast(p_Block); + for (const auto& sub : quote.blocks_) + { + if (sub) AppendPageBlock(*sub, p_Oss, p_Indent + "> "); + } + std::string credit = GetRichText(quote.credit_.get()); + if (!credit.empty()) + { + p_Oss << p_Indent << "> - " << credit << "\n"; + } + break; + } + case td::td_api::pageBlockPullQuote::ID: + { + const auto& quote = static_cast(p_Block); + std::string text = GetRichText(quote.text_.get()); + p_Oss << p_Indent << "\"" << text << "\"\n"; + std::string credit = GetRichText(quote.credit_.get()); + if (!credit.empty()) + { + p_Oss << p_Indent << "- " << credit << "\n"; + } + break; + } + case td::td_api::pageBlockTable::ID: + { + const auto& table = static_cast(p_Block); + std::string caption = GetRichText(table.caption_.get()); + if (!caption.empty()) + { + p_Oss << p_Indent << caption << "\n"; + } + + std::vector> rows; + size_t maxCols = 0; + for (const auto& row : table.cells_) + { + std::vector cells; + for (const auto& cell : row) + { + std::string cellText = GetRichText(cell->text_.get()); + for (char& c : cellText) + { + if (c == '\n') c = ' '; + } + cells.push_back(std::move(cellText)); + } + maxCols = std::max(maxCols, cells.size()); + rows.push_back(std::move(cells)); + } + + if (rows.empty() || maxCols == 0) break; + + std::vector colWidths(maxCols, 0); + for (const auto& row : rows) + { + for (size_t i = 0; i < row.size(); ++i) + { + colWidths[i] = std::max(colWidths[i], row[i].size()); + } + } + + auto separator = [&]() + { + p_Oss << p_Indent; + for (size_t i = 0; i < maxCols; ++i) + { + p_Oss << std::string(colWidths[i] + 2, '-'); + if (i + 1 < maxCols) p_Oss << "+"; + } + p_Oss << "\n"; + }; + + bool drawBorder = table.is_bordered_; + if (drawBorder) separator(); + + for (size_t r = 0; r < rows.size(); ++r) + { + p_Oss << p_Indent; + for (size_t i = 0; i < maxCols; ++i) + { + const std::string& cell = (i < rows[r].size()) ? rows[r][i] : ""; + p_Oss << " " << cell << std::string(colWidths[i] - cell.size(), ' ') << " "; + if (i + 1 < maxCols) p_Oss << "|"; + } + p_Oss << "\n"; + if (drawBorder && r == 0) separator(); + } + if (drawBorder) separator(); + break; + } + case td::td_api::pageBlockDetails::ID: + { + const auto& details = static_cast(p_Block); + std::string header = GetRichText(details.header_.get()); + p_Oss << p_Indent << (details.is_open_ ? "v " : "> ") << header << "\n"; + for (const auto& sub : details.blocks_) + { + if (sub) AppendPageBlock(*sub, p_Oss, p_Indent + " "); + } + break; + } + case td::td_api::pageBlockCover::ID: + { + const auto& cover = static_cast(p_Block); + if (cover.cover_) AppendPageBlock(*cover.cover_, p_Oss, p_Indent); + break; + } + case td::td_api::pageBlockCollage::ID: + case td::td_api::pageBlockSlideshow::ID: + case td::td_api::pageBlockEmbeddedPost::ID: + { + const auto& blocks = static_cast(p_Block).blocks_; + for (const auto& sub : blocks) + { + if (sub) AppendPageBlock(*sub, p_Oss, p_Indent); + } + break; + } + case td::td_api::pageBlockAnchor::ID: + case td::td_api::pageBlockThinking::ID: + default: + break; + } +} + +std::string TgChat::Impl::GetRichMessageText(const td::td_api::richMessage& p_RichMessage) +{ + std::ostringstream oss; + for (const auto& block : p_RichMessage.blocks_) + { + if (block) AppendPageBlock(*block, oss, ""); + } + + std::string result = oss.str(); + while (!result.empty() && (result.back() == '\n' || result.back() == ' ')) + { + result.pop_back(); + } + return result; +} + void TgChat::Impl::TdMessageContentConvert(td::td_api::MessageContent& p_TdMessageContent, int64_t p_SenderId, std::string& p_Text, std::string& p_FileInfo) { @@ -3551,6 +3848,14 @@ void TgChat::Impl::TdMessageContentConvert(td::td_api::MessageContent& p_TdMessa { p_Text = "[UnsupportedByCurrentTdlibVersion]"; } + else if (p_TdMessageContent.get_id() == td::td_api::messageRichMessage::ID) + { + auto& messageRichMessage = static_cast(p_TdMessageContent); + if (messageRichMessage.message_) + { + p_Text = GetRichMessageText(*messageRichMessage.message_); + } + } else { p_Text = "[UnknownMessage " + std::to_string(p_TdMessageContent.get_id()) + "]"; From 2bdc8355329701b01879bb26e00a7946938708e6 Mon Sep 17 00:00:00 2001 From: Gabriel Monteiro Tomasin Date: Tue, 7 Jul 2026 08:10:14 -0300 Subject: [PATCH 2/2] fix: correct table column width for UTF-8 display width MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous implementation used std::string::size() to calculate column widths, which returns byte count, not display width. This caused misaligned tables when cells contained accented characters (é, á, ã) or emoji (✅), since these take more bytes than display columns. Now uses StrUtil::ToWString + StrUtil::WStringWidth (wcswidth) to measure actual terminal display width, and pads cells accordingly. Also fixes separator line to have leading/trailing '+' for proper border. --- lib/tgchat/src/tgchat.cpp | 31 +++++++++++++++++++++++-------- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/lib/tgchat/src/tgchat.cpp b/lib/tgchat/src/tgchat.cpp index 40b278d5..79196a21 100644 --- a/lib/tgchat/src/tgchat.cpp +++ b/lib/tgchat/src/tgchat.cpp @@ -3427,6 +3427,21 @@ void TgChat::Impl::AppendPageBlock(const td::td_api::PageBlock& p_Block, std::os p_Oss << p_Indent << caption << "\n"; } + // Compute display width of a UTF-8 string (handles accents, emoji, wide chars) + auto displayWidth = [](const std::string& s) -> int + { + std::wstring ws = StrUtil::ToWString(s); + return StrUtil::WStringWidth(ws); + }; + + // Pad a string to a given display width with spaces on the right + auto padRight = [&](const std::string& s, int width) -> std::string + { + int dw = displayWidth(s); + if (dw >= width) return s; + return s + std::string(width - dw, ' '); + }; + std::vector> rows; size_t maxCols = 0; for (const auto& row : table.cells_) @@ -3447,24 +3462,25 @@ void TgChat::Impl::AppendPageBlock(const td::td_api::PageBlock& p_Block, std::os if (rows.empty() || maxCols == 0) break; - std::vector colWidths(maxCols, 0); + // Calculate column widths using display width (not byte length) + std::vector colWidths(maxCols, 0); for (const auto& row : rows) { for (size_t i = 0; i < row.size(); ++i) { - colWidths[i] = std::max(colWidths[i], row[i].size()); + colWidths[i] = std::max(colWidths[i], displayWidth(row[i])); } } + // Separator line: +---+---+---+ auto separator = [&]() { p_Oss << p_Indent; for (size_t i = 0; i < maxCols; ++i) { - p_Oss << std::string(colWidths[i] + 2, '-'); - if (i + 1 < maxCols) p_Oss << "+"; + p_Oss << "+" << std::string(colWidths[i] + 2, '-'); } - p_Oss << "\n"; + p_Oss << "+\n"; }; bool drawBorder = table.is_bordered_; @@ -3476,10 +3492,9 @@ void TgChat::Impl::AppendPageBlock(const td::td_api::PageBlock& p_Block, std::os for (size_t i = 0; i < maxCols; ++i) { const std::string& cell = (i < rows[r].size()) ? rows[r][i] : ""; - p_Oss << " " << cell << std::string(colWidths[i] - cell.size(), ' ') << " "; - if (i + 1 < maxCols) p_Oss << "|"; + p_Oss << "| " << padRight(cell, colWidths[i]) << " "; } - p_Oss << "\n"; + p_Oss << "|\n"; if (drawBorder && r == 0) separator(); } if (drawBorder) separator();