From 3ec916882f084aa3dc4e88853608ad48cda4f9de Mon Sep 17 00:00:00 2001 From: Doug Hoyte Date: Fri, 20 Dec 2024 00:30:29 -0500 Subject: [PATCH] nevent/nprofile --- src/apps/web/Bech32Utils.h | 40 ++++++++++++++++++++++++++++++++++++-- src/apps/web/TODO | 3 --- src/apps/web/WebData.h | 30 ++++++++++++++++++++++++---- src/apps/web/bech32.cpp | 4 +++- 4 files changed, 67 insertions(+), 10 deletions(-) diff --git a/src/apps/web/Bech32Utils.h b/src/apps/web/Bech32Utils.h index 3f8e159..1701bd8 100644 --- a/src/apps/web/Bech32Utils.h +++ b/src/apps/web/Bech32Utils.h @@ -41,7 +41,7 @@ inline std::string encodeBech32Simple(const std::string &hrp, std::string_view v return bech32::encode(hrp, values5, bech32::Encoding::BECH32); } -inline std::string decodeBech32Simple(std::string_view v) { +inline std::string decodeBech32Raw(std::string_view v) { auto res = bech32::decode(std::string(v)); if (res.encoding == bech32::Encoding::INVALID) throw herr("invalid bech32"); @@ -49,7 +49,43 @@ inline std::string decodeBech32Simple(std::string_view v) { std::vector out; if (!convertbits<5, 8, false>(out, res.data)) throw herr("convertbits failed"); - if (out.size() != 32) throw herr("unexpected size from bech32"); return std::string((char*)out.data(), out.size()); } + +inline std::string decodeBech32Simple(std::string_view v) { + auto out = decodeBech32Raw(v); + if (out.size() != 32) throw herr("unexpected size from bech32"); + return out; +} + +inline uint8_t getByte(std::string_view &encoded) { + if (encoded.size() < 1) throw herr("parse ends prematurely"); + uint8_t output = encoded[0]; + encoded = encoded.substr(1); + return output; +} + +inline std::string getBytes(std::string_view &encoded, size_t n) { + if (encoded.size() < n) throw herr("parse ends prematurely"); + auto res = encoded.substr(0, n); + encoded = encoded.substr(n); + return std::string(res); +}; + +inline std::string decodeBech32GetSpecial(std::string_view origStr) { + auto decoded = decodeBech32Raw(origStr); + std::string_view s(decoded); + + while (s.size()) { + auto tag = getByte(s); + auto len = getByte(s); + auto val = getBytes(s, len); + if (tag == 0) { + if (len != 32) throw herr("unexpected size from bech32 special"); + return std::string(val); + } + } + + throw herr("no special tag found"); +} diff --git a/src/apps/web/TODO b/src/apps/web/TODO index 36517ef..2a6f7a7 100644 --- a/src/apps/web/TODO +++ b/src/apps/web/TODO @@ -1,7 +1,4 @@ read - * support nprofile/nevent/etc links - example nevent: https://oddbean.com/e/note1qmye0at28we63aze93xjr92nzw725td0a5ncz3htwlc3wg78kp6q7802ad - example nprofile: https://oddbean.com/e/note1ykjalrpaj6jvxeuc434yd7ksrj8yd2vte478700ta8np250l3clsyjvh4q * non-500 error pages when bech32 fails to parse, for example * search field: enter anything, pubkey (hex or npub), eventId, etc. maybe even full-text search? * rss diff --git a/src/apps/web/WebData.h b/src/apps/web/WebData.h index 7675fd8..aa13bf3 100644 --- a/src/apps/web/WebData.h +++ b/src/apps/web/WebData.h @@ -337,7 +337,7 @@ 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+))"); + static RE2 matcher(R"((?is)(.*?)(https?://\S+|#\[\d+\]|nostr:(?:note|npub|nevent|nprofile)1\w+))"); std::string output; @@ -367,6 +367,19 @@ struct Event { std::string path = "/e/"; path += sv(match).substr(6); appendLink(path, sv(match)); + } else if (match.starts_with("nostr:nevent1")) { + bool didTransform = false; + + try { + auto id = decodeBech32GetSpecial(sv(match).substr(6)); + std::string path = "/e/"; + path += encodeBech32Simple("note", id); + + appendLink(path, sv(match)); + didTransform = true; + } catch(...) {} + + if (!didTransform) output += sv(match); } else if (match.starts_with("nostr:npub1")) { bool didTransform = false; @@ -374,9 +387,18 @@ struct Event { 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(); - } + } catch(...) {} + + if (!didTransform) output += sv(match); + } else if (match.starts_with("nostr:nprofile")) { + bool didTransform = false; + + try { + auto pubkey = decodeBech32GetSpecial(sv(match).substr(6)); + const auto *u = userCache.getUser(txn, decomp, pubkey); + appendLink(std::string("/u/") + u->npubId, std::string("@") + u->username); + didTransform = true; + } catch(...) {} if (!didTransform) output += sv(match); } else if (match.starts_with("#[")) { diff --git a/src/apps/web/bech32.cpp b/src/apps/web/bech32.cpp index 50fed25..5039a7f 100644 --- a/src/apps/web/bech32.cpp +++ b/src/apps/web/bech32.cpp @@ -37,6 +37,8 @@ namespace bech32 namespace { +const size_t BECH32_MAX_SIZE = 5000; + typedef std::vector data; /** The Bech32 character set for encoding. */ @@ -204,7 +206,7 @@ DecodeResult decode(const std::string& str) { } if (lower && upper) return {}; size_t pos = str.rfind('1'); - if (str.size() > 90 || pos == str.npos || pos == 0 || pos + 7 > str.size()) { + if (str.size() > BECH32_MAX_SIZE || pos == str.npos || pos == 0 || pos + 7 > str.size()) { return {}; } data values(str.size() - 1 - pos);