Files
strfry/src/apps/web/WebReader.cpp
2024-08-29 14:08:43 -04:00

385 lines
12 KiB
C++

#include "WebServer.h"
#include "WebData.h"
#include "WebStaticFiles.h"
std::string exportUserEvents(lmdb::txn &txn, Decompressor &decomp, std::string_view pubkey) {
std::string output;
env.generic_foreachFull(txn, env.dbi_Event__pubkey, makeKey_StringUint64(pubkey, MAX_U64), "", [&](std::string_view k, std::string_view v){
ParsedKey_StringUint64 parsedKey(k);
if (parsedKey.s != pubkey) return false;
uint64_t levId = lmdb::from_sv<uint64_t>(v);
output += getEventJson(txn, decomp, levId);
output += "\n";
return true;
}, true);
return output;
}
std::string exportEventThread(lmdb::txn &txn, Decompressor &decomp, std::string_view rootId) {
std::string output;
{
auto rootEv = lookupEventById(txn, rootId);
if (rootEv) {
output += getEventJson(txn, decomp, rootEv->primaryKeyId);
output += "\n";
}
}
std::string prefix = "e";
prefix += rootId;
env.generic_foreachFull(txn, env.dbi_Event__tag, prefix, "", [&](std::string_view k, std::string_view v){
ParsedKey_StringUint64 parsedKey(k);
if (parsedKey.s != prefix) return false;
auto levId = lmdb::from_sv<uint64_t>(v);
output += getEventJson(txn, decomp, levId);
output += "\n";
return true;
});
return output;
}
void doSearch(lmdb::txn &txn, Decompressor &decomp, std::string_view search, std::vector<TemplarResult> &results) {
auto doesPubkeyExist = [&](std::string_view pubkey){
bool ret = false;
env.generic_foreachFull(txn, env.dbi_Event__pubkey, makeKey_StringUint64(pubkey, 0), "", [&](std::string_view k, std::string_view v){
ParsedKey_StringUint64 parsedKey(k);
if (parsedKey.s == pubkey) ret = true;
return false;
});
return ret;
};
if (search.size() == 64) {
try {
auto word = from_hex(search);
if (doesPubkeyExist(word)) results.emplace_back(tmpl::search::userResult(User(txn, decomp, word)));
} catch(...) {}
}
if (search.starts_with("npub1")) {
try {
auto word = decodeBech32Simple(search);
if (doesPubkeyExist(word)) results.emplace_back(tmpl::search::userResult(User(txn, decomp, word)));
} catch(...) {}
}
try {
auto e = Event::fromIdExternal(txn, search);
results.emplace_back(tmpl::search::eventResult(e));
} catch(...) {}
}
TemplarResult renderCommunityEvents(lmdb::txn &txn, Decompressor &decomp, UserCache &userCache, const CommunitySpec &communitySpec) {
AlgoScanner a(txn, communitySpec.algo);
auto events = a.getEvents(txn, decomp, 60);
std::vector<TemplarResult> rendered;
auto now = hoytech::curr_time_s();
uint64_t n = 1;
for (auto &fe : events) {
auto ev = Event::fromLevId(txn, fe.levId);
ev.populateJson(txn, decomp);
struct {
uint64_t n;
const Event &ev;
const User &user;
std::string timestamp;
AlgoScanner::EventInfo &info;
} ctx = {
n,
ev,
*userCache.getUser(txn, decomp, ev.getPubkey()),
renderTimestamp(now, ev.getCreatedAt()),
fe.info,
};
rendered.emplace_back(tmpl::community::item(ctx));
n++;
}
return tmpl::community::list(rendered);
}
HTTPResponse WebServer::generateReadResponse(lmdb::txn &txn, Decompressor &decomp, const HTTPRequest &req) {
HTTPResponse httpResp;
auto startTime = hoytech::curr_time_us();
Url u(req.url);
LI << "READ REQUEST: " << req.url;
UserCache userCache;
std::string_view code = "200 OK";
std::string_view contentType = "text/html; charset=utf-8";
// Normal frame:
std::optional<TemplarResult> body;
std::optional<CommunitySpec> communitySpec;
std::string title;
// Or, raw:
std::optional<std::string> rawBody;
if (u.path.size() == 0 || u.path[0] == "algo") {
communitySpec = lookupCommunitySpec(txn, decomp, userCache, cfg().web__homepageCommunity);
httpResp.extraHeaders += "Cache-Control: max-age=600\r\n";
}
if (u.path.size() == 0) {
body = renderCommunityEvents(txn, decomp, userCache, *communitySpec);
} else if (u.path[0] == "algo") {
struct {
std::string community;
const CommunitySpec &communitySpec;
std::string_view descriptor;
} ctx = {
"homepage",
*communitySpec,
cfg().web__homepageCommunity,
};
body = tmpl::community::communityInfo(ctx);
} else if (u.path[0] == "e") {
if (u.path.size() == 2) {
EventThread et(txn, decomp, decodeBech32Simple(u.path[1]));
body = et.render(txn, decomp, userCache);
} else if (u.path.size() == 3) {
if (u.path[2] == "reply") {
auto ev = Event::fromIdExternal(txn, u.path[1]);
ev.populateJson(txn, decomp);
RenderedEventCtx ctx;
ctx.timestamp = renderTimestamp(startTime / 1'000'000, ev.getCreatedAt());
ctx.content = templarInternal::htmlEscape(ev.json.at("content").get_string(), false);
ctx.ev = &ev;
ctx.user = userCache.getUser(txn, decomp, ev.getPubkey());
ctx.showActions = false;
body = tmpl::event::reply(ctx);
} else if (u.path[2] == "raw.json") {
auto ev = Event::fromIdExternal(txn, u.path[1]);
ev.populateJson(txn, decomp);
rawBody = tao::json::to_string(ev.json, 4);
contentType = "application/json; charset=utf-8";
} else if (u.path[2] == "export.jsonl") {
rawBody = exportEventThread(txn, decomp, decodeBech32Simple(u.path[1]));
contentType = "application/jsonl+json; charset=utf-8";
}
}
} else if (u.path[0] == "u") {
if (u.path.size() == 2) {
User user(txn, decomp, decodeBech32Simple(u.path[1]));
title = std::string("profile: ") + user.username;
body = tmpl::user::metadata(user);
} else if (u.path.size() == 3) {
std::string userPubkey;
if (u.path[1].starts_with("npub1")) {
userPubkey = decodeBech32Simple(u.path[1]);
} else {
userPubkey = from_hex(u.path[1]);
}
if (u.path[2] == "notes") {
uint64_t resumeTime = MAX_U64;
try {
auto resumeTimeStr = u.lookupQuery("next");
if (resumeTimeStr) resumeTime = std::stoull(std::string(*resumeTimeStr));
} catch(...) {}
UserEvents uc(txn, decomp, userPubkey, resumeTime);
title = std::string("notes: ") + uc.u.username;
body = uc.render(txn, decomp);
} else if (u.path[2] == "export.jsonl") {
rawBody = exportUserEvents(txn, decomp, userPubkey);
contentType = "application/jsonl+json; charset=utf-8";
} else if (u.path[2] == "metadata.json") {
User user(txn, decomp, userPubkey);
rawBody = user.kind0Found() ? tao::json::to_string(*user.kind0Json) : "{}";
contentType = "application/json; charset=utf-8";
} else if (u.path[2] == "following") {
User user(txn, decomp, userPubkey);
title = std::string("following: ") + user.username;
user.populateContactList(txn, decomp);
struct {
User &user;
std::function<const User*(const std::string &)> getUser;
} ctx = {
user,
[&](const std::string &pubkey){ return userCache.getUser(txn, decomp, pubkey); },
};
body = tmpl::user::following(ctx);
} else if (u.path[2] == "followers") {
User user(txn, decomp, userPubkey);
title = std::string("followers: ") + user.username;
auto followers = user.getFollowers(txn, decomp, user.pubkey);
struct {
const User &user;
const std::vector<std::string> &followers;
std::function<const User*(const std::string &)> getUser;
} ctx = {
user,
followers,
[&](const std::string &pubkey){ return userCache.getUser(txn, decomp, pubkey); },
};
body = tmpl::user::followers(ctx);
}
}
} else if (u.path[0] == "search") {
std::vector<TemplarResult> results;
if (u.query.starts_with("q=")) {
std::string_view search = u.query.substr(2);
doSearch(txn, decomp, search, results);
struct {
std::string_view search;
const std::vector<TemplarResult> &results;
} ctx = {
search,
results,
};
body = tmpl::searchPage(ctx);
}
} else if (u.path[0] == "post") {
body = tmpl::newPost(nullptr);
} else if (u.path[0] == "static" && u.path.size() >= 2) {
httpResp.extraHeaders += "Cache-Control: max-age=31536000\r\n";
if (u.path[1] == "oddbean.js") {
rawBody = std::string(oddbeanStatic__oddbean_js());
contentType = "application/javascript";
} else if (u.path[1] == "oddbean.css") {
rawBody = std::string(oddbeanStatic__oddbean_css());
contentType = "text/css";
} else if (u.path[1] == "oddbean.svg") {
rawBody = std::string(oddbeanStatic__oddbean_svg());
contentType = "image/svg+xml";
}
} else if (u.path[0] == "favicon.ico") {
rawBody = std::string(oddbeanStatic__favicon_ico());
contentType = "image/x-icon";
httpResp.extraHeaders += "Cache-Control: max-age=2592000\r\n";
httpResp.noCompress = true;
} else if (u.path[0] == "login") {
body = tmpl::login(0);
} else if (u.path[0] == "about") {
body = tmpl::about(0);
}
std::string responseData;
if (body) {
if (title.size()) title += " | ";
struct {
const TemplarResult &body;
const std::optional<CommunitySpec> &communitySpec;
std::string_view title;
std::string staticFilesPrefix;
std::string_view staticOddbeanCssHash;
std::string_view staticOddbeanJsHash;
std::string_view staticOddbeanSvgHash;
} ctx = {
*body,
communitySpec,
title,
cfg().web__staticFilesPrefix.size() ? cfg().web__staticFilesPrefix : "/static",
oddbeanStatic__oddbean_css__hash().substr(0, 16),
oddbeanStatic__oddbean_js__hash().substr(0, 16),
oddbeanStatic__oddbean_svg__hash().substr(0, 16),
};
responseData = std::move(tmpl::main(ctx).str);
} else if (rawBody) {
responseData = std::move(*rawBody);
} else {
code = "404 Not Found";
body = TemplarResult{ "Not found" };
}
httpResp.body = responseData;
httpResp.code = code;
httpResp.contentType = contentType;
LI << "Reply: " << code << " / " << responseData.size() << " bytes in " << (hoytech::curr_time_us() - startTime) << "us";
return httpResp;
}
void WebServer::runReader(ThreadPool<MsgWebReader>::Thread &thr) {
Decompressor decomp;
while(1) {
auto newMsgs = thr.inbox.pop_all();
auto txn = env.txn_ro();
for (auto &newMsg : newMsgs) {
if (auto msg = std::get_if<MsgWebReader::Request>(&newMsg.msg)) {
try {
HTTPResponse resp = generateReadResponse(txn, decomp, msg->req);
std::string payload = resp.encode(msg->req.acceptGzip);
sendHttpResponseAndUnlock(msg->lockedThreadId, msg->req, payload);
} catch (std::exception &e) {
HTTPResponse res;
res.code = "500 Server Error";
res.body = "Server error";
std::string payload = res.encode(false);
sendHttpResponseAndUnlock(msg->lockedThreadId, msg->req, payload);
LE << "500 server error: " << e.what();
}
}
}
}
}