From c4bb4b3f4ce0afde82f00d3e54d4dd66588ae43d Mon Sep 17 00:00:00 2001 From: Doug Hoyte Date: Thu, 19 Dec 2024 19:59:57 -0500 Subject: [PATCH] bugfix: nostr: links in titles/summaries --- src/apps/web/TODO | 1 - src/apps/web/WebData.h | 154 +++++++++++++++++++------------------ src/apps/web/WebReader.cpp | 4 +- 3 files changed, 82 insertions(+), 77 deletions(-) diff --git a/src/apps/web/TODO b/src/apps/web/TODO index 47e6fd0..36517ef 100644 --- a/src/apps/web/TODO +++ b/src/apps/web/TODO @@ -1,5 +1,4 @@ read - * nostr: links not replaced in feed titles * support nprofile/nevent/etc links example nevent: https://oddbean.com/e/note1qmye0at28we63aze93xjr92nzw725td0a5ncz3htwlc3wg78kp6q7802ad example nprofile: https://oddbean.com/e/note1ykjalrpaj6jvxeuc434yd7ksrj8yd2vte478700ta8np250l3clsyjvh4q diff --git a/src/apps/web/WebData.h b/src/apps/web/WebData.h index 5e49a97..1efedac 100644 --- a/src/apps/web/WebData.h +++ b/src/apps/web/WebData.h @@ -264,11 +264,12 @@ struct Event { // FIXME: Don't truncate UTF-8 mid-sequence // FIXME: Don't put ellipsis if truncated text ends in punctuation - Summary summaryHtml() const { + Summary summaryHtml(lmdb::txn &txn, Decompressor &decomp, UserCache &userCache) const { Summary output; std::string content = json.at("content").get_string(); auto firstUrl = stripUrls(content); + preprocessEventContent(txn, decomp, userCache, content, false); auto textAbbrev = [](std::string &str, size_t maxLen){ if (str.size() > maxLen) str = str.substr(0, maxLen-3) + "..."; @@ -329,78 +330,83 @@ struct Event { } } } + + void preprocessEventContent(lmdb::txn &txn, Decompressor &decomp, UserCache &userCache, std::string &content, bool withLinks = true) const { + static RE2 matcher(R"((?is)(.*?)(https?://\S+|#\[\d+\]|nostr:(?:note|npub)1\w+))"); + + std::string output; + + std::string_view contentSv(content); + re2::StringPiece input(contentSv); + re2::StringPiece prefix, match; + + auto sv = [](re2::StringPiece s){ return std::string_view(s.data(), s.size()); }; + auto appendLink = [&](std::string_view url, std::string_view text){ + if (withLinks) { + output += ""; + output += text; + output += ""; + } else { + output += text; + } + }; + + while (RE2::Consume(&input, matcher, &prefix, &match)) { + output += sv(prefix); + + if (match.starts_with("http")) { + appendLink(sv(match), sv(match)); + } else if (match.starts_with("nostr:note1")) { + std::string path = "/e/"; + path += sv(match).substr(6); + appendLink(path, sv(match)); + } else if (match.starts_with("nostr:npub1")) { + bool didTransform = false; + + try { + const auto *u = userCache.getUser(txn, decomp, decodeBech32Simple(sv(match).substr(6))); + appendLink(std::string("/u/") + u->npubId, std::string("@") + u->username); + didTransform = true; + } catch(std::exception &e) { + //LW << "tag parse error: " << e.what(); + } + + if (!didTransform) output += sv(match); + } else if (match.starts_with("#[")) { + bool didTransform = false; + auto offset = std::stoull(std::string(sv(match)).substr(2, match.size() - 3)); + + const auto &tags = json.at("tags").get_array(); + + try { + const auto &tag = tags.at(offset).get_array(); + + if (tag.at(0) == "p") { + const auto *u = userCache.getUser(txn, decomp, from_hex(tag.at(1).get_string())); + appendLink(std::string("/u/") + u->npubId, u->username); + didTransform = true; + } else if (tag.at(0) == "e") { + appendLink(std::string("/e/") + encodeBech32Simple("note", from_hex(tag.at(1).get_string())), sv(match)); + didTransform = true; + } + } catch(std::exception &e) { + //LW << "tag parse error: " << e.what(); + } + + if (!didTransform) output += sv(match); + } + } + + if (output.size()) { + output += std::string_view(input.data(), input.size()); + std::swap(output, content); + } + } }; -inline void preprocessEventContent(lmdb::txn &txn, Decompressor &decomp, const Event &ev, UserCache &userCache, std::string &content) { - static RE2 matcher(R"((?is)(.*?)(https?://\S+|#\[\d+\]|nostr:(?:note|npub)1\w+))"); - - std::string output; - - std::string_view contentSv(content); - re2::StringPiece input(contentSv); - re2::StringPiece prefix, match; - - auto sv = [](re2::StringPiece s){ return std::string_view(s.data(), s.size()); }; - auto appendLink = [&](std::string_view url, std::string_view text){ - output += ""; - output += text; - output += ""; - }; - - while (RE2::Consume(&input, matcher, &prefix, &match)) { - output += sv(prefix); - - if (match.starts_with("http")) { - appendLink(sv(match), sv(match)); - } else if (match.starts_with("nostr:note1")) { - std::string path = "/e/"; - path += sv(match).substr(6); - appendLink(path, sv(match)); - } else if (match.starts_with("nostr:npub1")) { - bool didTransform = false; - - try { - const auto *u = userCache.getUser(txn, decomp, decodeBech32Simple(sv(match).substr(6))); - appendLink(std::string("/u/") + u->npubId, std::string("@") + u->username); - didTransform = true; - } catch(std::exception &e) { - //LW << "tag parse error: " << e.what(); - } - - if (!didTransform) output += sv(match); - } else if (match.starts_with("#[")) { - bool didTransform = false; - auto offset = std::stoull(std::string(sv(match)).substr(2, match.size() - 3)); - - const auto &tags = ev.json.at("tags").get_array(); - - try { - const auto &tag = tags.at(offset).get_array(); - - if (tag.at(0) == "p") { - const auto *u = userCache.getUser(txn, decomp, from_hex(tag.at(1).get_string())); - appendLink(std::string("/u/") + u->npubId, u->username); - didTransform = true; - } else if (tag.at(0) == "e") { - appendLink(std::string("/e/") + encodeBech32Simple("note", from_hex(tag.at(1).get_string())), sv(match)); - didTransform = true; - } - } catch(std::exception &e) { - //LW << "tag parse error: " << e.what(); - } - - if (!didTransform) output += sv(match); - } - } - - if (output.size()) { - output += std::string_view(input.data(), input.size()); - std::swap(output, content); - } -} inline std::string stripUrls(std::string &content) { @@ -530,14 +536,14 @@ struct EventThread { } - std::string getThreadTitle() { + std::string getThreadTitle(lmdb::txn &txn, Decompressor &decomp, UserCache &userCache) { if (!rootEventId.size()) return ""; auto p = eventCache.find(rootEventId); if (p == eventCache.end()) return ""; const auto &elem = p->second; - return elem.summaryHtml().text; + return elem.summaryHtml(txn, decomp, userCache).text; } @@ -564,10 +570,10 @@ struct EventThread { ctx.abbrev = focusOnPubkey && *focusOnPubkey != pubkey; if (ctx.abbrev) { - ctx.content = elem.summaryHtml().text; + ctx.content = elem.summaryHtml(txn, decomp, userCache).text; } else { ctx.content = templarInternal::htmlEscape(elem.json.at("content").get_string(), false); - preprocessEventContent(txn, decomp, elem, userCache, ctx.content); + elem.preprocessEventContent(txn, decomp, userCache, ctx.content); } ctx.ev = &elem; diff --git a/src/apps/web/WebReader.cpp b/src/apps/web/WebReader.cpp index 99f4c08..c6aaed3 100644 --- a/src/apps/web/WebReader.cpp +++ b/src/apps/web/WebReader.cpp @@ -104,7 +104,7 @@ TemplarResult renderFeed(lmdb::txn &txn, Decompressor &decomp, UserCache &userCa auto ev = Event::fromLevId(txn, fe.levId); ev.populateJson(txn, decomp); - auto summary = ev.summaryHtml(); + auto summary = ev.summaryHtml(txn, decomp, userCache); std::string url; if (summary.url.size()) { url = summary.url; @@ -208,7 +208,7 @@ HTTPResponse WebServer::generateReadResponse(lmdb::txn &txn, Decompressor &decom if (u.path.size() == 2) { EventThread et(txn, decomp, decodeBech32Simple(u.path[1])); body = et.render(txn, decomp, userCache); - title = et.getThreadTitle(); + title = et.getThreadTitle(txn, decomp, userCache); } else if (u.path.size() == 3) { if (u.path[2] == "reply") { auto ev = Event::fromIdExternal(txn, u.path[1]);