about summary refs log tree commit diff
diff options
context:
space:
mode:
authorJune McEnroe <june@causal.agency>2020-07-12 12:17:44 -0400
committerJune McEnroe <june@causal.agency>2020-07-12 12:17:44 -0400
commit7ff6e16e8ccda2433006f949ffc7b1ee5eaf08cf (patch)
tree03abf9d87da7810b364d27f97f9e541df9c9e46f
parentClean up page error handling (diff)
downloadscooper-7ff6e16e8ccda2433006f949ffc7b1ee5eaf08cf.tar.gz
scooper-7ff6e16e8ccda2433006f949ffc7b1ee5eaf08cf.zip
Reorganize code and add earlier messages link
Diffstat (limited to '')
-rw-r--r--contexts.c12
-rw-r--r--events.c96
-rw-r--r--html.c55
-rw-r--r--networks.c5
-rw-r--r--scooper.114
-rw-r--r--search.c12
-rw-r--r--server.c60
-rw-r--r--server.h195
8 files changed, 243 insertions, 206 deletions
diff --git a/contexts.c b/contexts.c
index 04b44ac..b486ef9 100644
--- a/contexts.c
+++ b/contexts.c
@@ -15,12 +15,16 @@
  */
 
 #include <err.h>
+#include <stdbool.h>
 #include <stdio.h>
 #include <stdlib.h>
 #include <sysexits.h>
 
 #include "server.h"
 
+bool contextsPublic;
+int contextsRecent = 500;
+
 const char *ContextsQuery = SQL(
 	WITH recentEvents AS (
 		SELECT time, context
@@ -45,8 +49,8 @@ const char *ContextsQuery = SQL(
 	SELECT name, query, 0 FROM allContexts;
 );
 
-enum kcgi_err pageContexts(struct kreq *req) {
-	struct Scope scope = htmlScope(req);
+enum kcgi_err contextsPage(struct kreq *req) {
+	struct Scope scope = pageScope(req);
 	if (!scope.network) return httpFail(req, KHTTP_400);
 
 	enum kcgi_err error = httpHead(req, KHTTP_200, KMIME_TEXT_HTML);
@@ -62,8 +66,8 @@ enum kcgi_err pageContexts(struct kreq *req) {
 
 	sqlite3_reset(stmt.contexts);
 	dbBindText(stmt.contexts, ":network", scope.network);
-	dbBindInt(stmt.contexts, ":recent", pageRecent);
-	dbBindInt(stmt.contexts, ":public", pagePublic);
+	dbBindInt(stmt.contexts, ":recent", contextsRecent);
+	dbBindInt(stmt.contexts, ":public", contextsPublic);
 
 	enum State {
 		None,
diff --git a/events.c b/events.c
index e018087..35e2c5b 100644
--- a/events.c
+++ b/events.c
@@ -15,6 +15,7 @@
  */
 
 #include <err.h>
+#include <inttypes.h>
 #include <stdio.h>
 #include <stdlib.h>
 #include <sysexits.h>
@@ -22,32 +23,21 @@
 
 #include "server.h"
 
-static enum kcgi_err redirect(struct kreq *req, struct Scope scope) {
-	struct tm *tm = gmtime(&(time_t) { time(NULL) });
-	if (!tm) err(EX_OSERR, "gmtime");
-
-	char time[sizeof("0000-00-00T00:00:00")];
-	strftime(time, sizeof(time), "%FT%T", tm);
-
-	char *url = khttp_urlpart(
-		NULL, NULL, Pages[Events],
-		Keys[Network].name, scope.network,
-		Keys[Context].name, scope.context,
-		Keys[Before].name, time,
-		NULL
-	);
-	enum kcgi_err error = httpRedirect(req, url);
-	free(url);
-	return error;
+int eventsGap = 3600;
+int eventsOverlap = 15;
+int eventsLimit = 50;
+
+static const char *timestamp(time_t time) {
+	static char stamp[sizeof("0000-00-00T00:00:00")];
+	strftime(stamp, sizeof(stamp), "%FT%T", gmtime(&time));
+	return stamp;
 }
 
 static enum kcgi_err
 dateForm(struct khtmlreq *html, struct Scope scope, const char *_time) {
 	struct tm tm = {0};
 	if (!strptime(_time, "%F", &tm)) {
-		struct tm *now = gmtime(&(time_t) { time(NULL) });
-		if (!now) err(EX_OSERR, "gmtime");
-		tm = *now;
+		tm = *gmtime(&(time_t) { time(NULL) });
 	}
 	char date[sizeof("0000-00-00")];
 	strftime(date, sizeof(date), "%F", &tm);
@@ -58,20 +48,7 @@ dateForm(struct khtmlreq *html, struct Scope scope, const char *_time) {
 			KATTR_ACTION, Pages[Events],
 			KATTR__MAX
 		)
-		|| khtml_attr(
-			html, KELEM_INPUT,
-			KATTR_TYPE, "hidden",
-			KATTR_NAME, Keys[Network].name,
-			KATTR_VALUE, scope.network,
-			KATTR__MAX
-		)
-		|| khtml_attr(
-			html, KELEM_INPUT,
-			KATTR_TYPE, "hidden",
-			KATTR_NAME, Keys[Context].name,
-			KATTR_VALUE, scope.context,
-			KATTR__MAX
-		)
+		|| htmlScopeFields(html, scope)
 		|| khtml_attr(
 			html, KELEM_INPUT,
 			KATTR_TYPE, "date",
@@ -134,12 +111,24 @@ const char *EventsBeforeQuery = SQL(
 	ORDER BY time, event;
 );
 
-enum kcgi_err pageEvents(struct kreq *req) {
-	struct Scope scope = htmlScope(req);
+enum kcgi_err eventsPage(struct kreq *req) {
+	struct Scope scope = pageScope(req);
 	if (!scope.network || !scope.context) return httpFail(req, KHTTP_400);
+
 	if (!req->fieldmap[After] && !req->fieldmap[Before]) {
-		return redirect(req, scope);
+		char *url = khttp_urlpart(
+			NULL, NULL, Pages[Events],
+			Keys[Network].name, scope.network,
+			Keys[Context].name, scope.context,
+			Keys[Before].name, timestamp(time(NULL)),
+			NULL
+		);
+		if (!url) err(EX_OSERR, "khttp_urlpart");
+		enum kcgi_err error = httpRedirect(req, url);
+		free(url);
+		return error;
 	}
+
 	const char *time = req->fieldmap[Before]
 		? req->fieldmap[Before]->parsed.s
 		: req->fieldmap[After]->parsed.s;
@@ -164,11 +153,12 @@ enum kcgi_err pageEvents(struct kreq *req) {
 	dbBindText(events, ":network", scope.network);
 	dbBindText(events, ":context", scope.context);
 	dbBindText(events, ":time", time);
-	dbBindInt(events, ":public", pagePublic);
-	dbBindInt(events, ":limit", pageLimit);
+	dbBindInt(events, ":public", contextsPublic);
+	dbBindInt(events, ":limit", eventsLimit);
 
+	int rows;
 	int result;
-	while (SQLITE_ROW == (result = sqlite3_step(events))) {
+	for (rows = 0; SQLITE_ROW == (result = sqlite3_step(events)); ++rows) {
 		int i = 0;
 		struct Event event = {0};
 		event.event = sqlite3_column_int64(events, i++);
@@ -179,6 +169,32 @@ enum kcgi_err pageEvents(struct kreq *req) {
 		event.host = sqlite3_column_text(events, i++);
 		event.target = sqlite3_column_text(events, i++);
 		event.message = sqlite3_column_text(events, i++);
+
+		if (!rows) {
+			char *base = khttp_urlpart(
+				NULL, NULL, Pages[Events],
+				Keys[Network].name, scope.network,
+				Keys[Context].name, scope.context,
+				Keys[Before].name, timestamp(event.time + eventsOverlap),
+				NULL
+			);
+			if (!base) err(EX_OSERR, "khttp_urlpart");
+
+			char *href = NULL;
+			asprintf(&href, "%s#%" PRId64, base, event.event);
+			if (!href) err(EX_OSERR, "asprintf");
+			free(base);
+
+			error = 0
+				|| khtml_elem(&html, KELEM_TR)
+				|| khtml_attr(&html, KELEM_TH, KATTR_COLSPAN, "3", KATTR__MAX)
+				|| khtml_attr(&html, KELEM_A, KATTR_HREF, href, KATTR__MAX)
+				|| khtml_puts(&html, "Earlier messages")
+				|| khtml_closeelem(&html, 3);
+			free(href);
+			if (error) return error;
+		}
+
 		error = htmlEvent(&html, scope, event);
 		if (error) return error;
 	}
diff --git a/html.c b/html.c
index 49b65e9..f5ea6bd 100644
--- a/html.c
+++ b/html.c
@@ -48,6 +48,30 @@ enum kcgi_err htmlHead(struct khtmlreq *html, const char *title) {
 		|| khtml_closeelem(html, 1);
 }
 
+enum kcgi_err htmlScopeFields(struct khtmlreq *html, struct Scope scope) {
+	if (scope.network) {
+		enum kcgi_err error = khtml_attr(
+			html, KELEM_INPUT,
+			KATTR_TYPE, "hidden",
+			KATTR_NAME, Keys[Network].name,
+			KATTR_VALUE, scope.network,
+			KATTR__MAX
+		);
+		if (error) return error;
+	}
+	if (scope.context) {
+		enum kcgi_err error = khtml_attr(
+			html, KELEM_INPUT,
+			KATTR_TYPE, "hidden",
+			KATTR_NAME, Keys[Context].name,
+			KATTR_VALUE, scope.context,
+			KATTR__MAX
+		);
+		if (error) return error;
+	}
+	return KCGI_OK;
+}
+
 enum kcgi_err
 htmlNav(struct khtmlreq *html, struct Scope scope) {
 	enum kcgi_err error = 0
@@ -97,7 +121,7 @@ htmlNav(struct khtmlreq *html, struct Scope scope) {
 		(scope.context ? scope.context : scope.network ? scope.network : "")
 	);
 
-	error = 0
+	return 0
 		|| khtml_closeelem(html, 1)
 		|| khtml_attr(
 			html, KELEM_FORM,
@@ -105,6 +129,7 @@ htmlNav(struct khtmlreq *html, struct Scope scope) {
 			KATTR_ACTION, Pages[Search],
 			KATTR__MAX
 		)
+		|| htmlScopeFields(html, scope)
 		|| khtml_attr(
 			html, KELEM_INPUT,
 			KATTR_TYPE, "search",
@@ -117,32 +142,8 @@ htmlNav(struct khtmlreq *html, struct Scope scope) {
 			KATTR_TYPE, "submit",
 			KATTR_VALUE, label,
 			KATTR__MAX
-		);
-	if (error) return error;
-
-	if (scope.network) {
-		error = khtml_attr(
-			html, KELEM_INPUT,
-			KATTR_TYPE, "hidden",
-			KATTR_NAME, Keys[Network].name,
-			KATTR_VALUE, scope.network,
-			KATTR__MAX
-		);
-		if (error) return error;
-	}
-
-	if (scope.context) {
-		error = khtml_attr(
-			html, KELEM_INPUT,
-			KATTR_TYPE, "hidden",
-			KATTR_NAME, Keys[Context].name,
-			KATTR_VALUE, scope.context,
-			KATTR__MAX
-		);
-		if (error) return error;
-	}
-
-	return khtml_closeelem(html, 2);
+		)
+		|| khtml_closeelem(html, 2);
 }
 
 static const char *SourceURL = "https://git.causal.agency/scooper";
diff --git a/networks.c b/networks.c
index 289fa31..f860efc 100644
--- a/networks.c
+++ b/networks.c
@@ -15,6 +15,7 @@
  */
 
 #include <err.h>
+#include <stdbool.h>
 #include <stdio.h>
 #include <stdlib.h>
 #include <sysexits.h>
@@ -43,7 +44,7 @@ const char *NetworksQuery = SQL(
 	SELECT network, 0 FROM allNetworks;
 );
 
-enum kcgi_err pageNetworks(struct kreq *req) {
+enum kcgi_err networksPage(struct kreq *req) {
 	struct Scope scope = {0};
 
 	enum kcgi_err error = httpHead(req, KHTTP_200, KMIME_TEXT_HTML);
@@ -58,7 +59,7 @@ enum kcgi_err pageNetworks(struct kreq *req) {
 	if (error) return error;
 
 	sqlite3_reset(stmt.networks);
-	dbBindInt(stmt.networks, ":recent", pageRecent);
+	dbBindInt(stmt.networks, ":recent", contextsRecent);
 
 	enum State {
 		None,
diff --git a/scooper.1 b/scooper.1
index e17e8bd..222db60 100644
--- a/scooper.1
+++ b/scooper.1
@@ -1,4 +1,4 @@
-.Dd July 11, 2020
+.Dd July 12, 2020
 .Dt SCOOPER 1
 .Os
 .
@@ -9,7 +9,9 @@
 .Sh SYNOPSIS
 .Nm
 .Op Fl p
+.Op Fl g Ar gap
 .Op Fl l Ar limit
+.Op Fl o Ar overlap
 .Op Fl r Ar recent
 .Op Fl s Ar url
 .Ar database
@@ -35,11 +37,21 @@ Prepare all SQL statements against the given
 .Ar database
 and exit.
 .
+.It Fl g Ar gap
+Set the time in seconds between events
+after which to indicate a gap.
+The default is 3600 (one hour).
+.
 .It Fl l Ar limit
 Limit the number of events
 to be displayed on one page.
 The default limit is 50.
 .
+.It Fl o Ar overlap
+Set the overlap in seconds
+between pages of events.
+The default is 15.
+.
 .It Fl p
 Show only public contexts,
 i.e. channels.
diff --git a/search.c b/search.c
index 1cfc0f1..7960d6b 100644
--- a/search.c
+++ b/search.c
@@ -44,8 +44,8 @@ const char *SearchQuery = SQL(
 	LIMIT :offset, :limit;
 );
 
-enum kcgi_err pageSearch(struct kreq *req) {
-	struct Scope scope = htmlScope(req);
+enum kcgi_err searchPage(struct kreq *req) {
+	struct Scope scope = pageScope(req);
 	if (!scope.query) return httpFail(req, KHTTP_400);
 	if (scope.context && !scope.network) return httpFail(req, KHTTP_400);
 
@@ -68,8 +68,8 @@ enum kcgi_err pageSearch(struct kreq *req) {
 	dbBindText(stmt.search, ":network", scope.network);
 	dbBindText(stmt.search, ":context", scope.context);
 	dbBindText(stmt.search, ":query", scope.query);
-	dbBindInt(stmt.search, ":public", pagePublic);
-	dbBindInt(stmt.search, ":limit", pageLimit);
+	dbBindInt(stmt.search, ":public", contextsPublic);
+	dbBindInt(stmt.search, ":limit", eventsLimit);
 	dbBindInt(stmt.search, ":offset", offset);
 	dbBindText(stmt.search, ":highlight", "\26");
 
@@ -88,8 +88,8 @@ enum kcgi_err pageSearch(struct kreq *req) {
 		event.target = sqlite3_column_text(stmt.search, i++);
 		event.message = sqlite3_column_text(stmt.search, i++);
 		error = htmlEvent(&html, scope, event);
-		if (error) break;
+		if (error) return error;
 	}
 	if (result != SQLITE_DONE) errx(EX_SOFTWARE, "%s", sqlite3_errmsg(db));
-	return error || htmlFooter(&html) || khtml_close(&html);
+	return htmlFooter(&html) || khtml_close(&html);
 }
diff --git a/server.c b/server.c
index 6ecd921..fad9a90 100644
--- a/server.c
+++ b/server.c
@@ -26,13 +26,6 @@
 sqlite3 *db;
 struct Statements stmt;
 
-static void prepare(sqlite3_stmt **stmt, const char *query) {
-	int error = sqlite3_prepare_v3(
-		db, query, -1, SQLITE_PREPARE_PERSISTENT, stmt, NULL
-	);
-	if (error) errx(EX_SOFTWARE, "%s: %s", sqlite3_errmsg(db), query);
-}
-
 const char *Pages[PagesLen] = {
 #define X(page, path) [page] = path,
 	ENUM_PAGES
@@ -45,44 +38,44 @@ const struct kvalid Keys[KeysLen] = {
 #undef X
 };
 
-bool pagePublic;
-int pageLimit = 50;
-int pageRecent = 500;
-
 static const char CSS[] = {
 #include "default.css.h"
 };
 static const char *Cache = "public, max-age=86400, immutable";
 
-static enum kcgi_err request(struct kreq *req) {
+static enum kcgi_err stylesheet(struct kreq *req) {
+	if (req->mime != KMIME_TEXT_CSS) return httpFail(req, KHTTP_404);
+	return httpHead(req, KHTTP_200, KMIME_TEXT_CSS)
+		|| khttp_head(req, kresps[KRESP_CACHE_CONTROL], "%s", Cache)
+		|| khttp_body(req)
+		|| khttp_write(req, CSS, sizeof(CSS));
+}
+
+static enum kcgi_err dispatch(struct kreq *req) {
 	if (req->method != KMETHOD_HEAD && req->method != KMETHOD_GET) {
 		return httpFail(req, KHTTP_405);
 	}
 	switch (req->page) {
-		case Networks: return pageNetworks(req);
-		case Contexts: return pageContexts(req);
-		case Events:   return pageEvents(req);
-		case Search:   return pageSearch(req);
-		case Stylesheet: {
-			if (req->mime != KMIME_TEXT_CSS) return httpFail(req, KHTTP_404);
-			return httpHead(req, KHTTP_200, KMIME_TEXT_CSS)
-				|| khttp_head(req, kresps[KRESP_CACHE_CONTROL], "%s", Cache)
-				|| khttp_body(req)
-				|| khttp_write(req, CSS, sizeof(CSS));
-		}
-		default: return httpFail(req, KHTTP_404);
+		case Networks:   return networksPage(req);
+		case Contexts:   return contextsPage(req);
+		case Events:     return eventsPage(req);
+		case Search:     return searchPage(req);
+		case Stylesheet: return stylesheet(req);
+		default:         return httpFail(req, KHTTP_404);
 	}
 }
 
 int main(int argc, char *argv[]) {
 	bool test = false;
 
-	for (int opt; 0 < (opt = getopt(argc, argv, "cl:pr:s:"));) {
+	for (int opt; 0 < (opt = getopt(argc, argv, "cg:l:o:pr:s:"));) {
 		switch (opt) {
+			break; case 'g': eventsGap = strtol(optarg, NULL, 10);
+			break; case 'o': eventsOverlap = strtol(optarg, NULL, 10);
 			break; case 'c': test = true;
-			break; case 'l': pageLimit = strtol(optarg, NULL, 10);
-			break; case 'p': pagePublic = true;
-			break; case 'r': pageRecent = strtol(optarg, NULL, 10);
+			break; case 'l': eventsLimit = strtol(optarg, NULL, 10);
+			break; case 'p': contextsPublic = true;
+			break; case 'r': contextsRecent = strtol(optarg, NULL, 10);
 			break; case 's': htmlStylesheet = optarg;
 			break; default:  return EX_USAGE;
 		}
@@ -108,12 +101,7 @@ int main(int argc, char *argv[]) {
 	}
 	sqlite3_finalize(check);
 
-	prepare(&stmt.networks, NetworksQuery);
-	prepare(&stmt.contexts, ContextsQuery);
-	prepare(&stmt.eventsAfter, EventsAfterQuery);
-	prepare(&stmt.eventsBefore, EventsBeforeQuery);
-	prepare(&stmt.search, SearchQuery);
-	
+	dbPrepareAll();
 	if (test) return EX_OK;
 	
 	if (khttp_fcgi_test()) {
@@ -127,7 +115,7 @@ int main(int argc, char *argv[]) {
 			KCGI_OK == (error = khttp_fcgi_parse(fcgi, &req));
 			khttp_free(&req)
 		) {
-			error = request(&req);
+			error = dispatch(&req);
 			if (error && error != KCGI_HUP) break;
 		}
 		if (error != KCGI_EXIT) {
@@ -140,7 +128,7 @@ int main(int argc, char *argv[]) {
 			&req, Keys, KeysLen, Pages, PagesLen, Networks
 		);
 		if (error) errx(EX_PROTOCOL, "khttp_parse: %s", kcgi_strerror(error));
-		error = request(&req);
+		error = dispatch(&req);
 		if (error) errx(EX_PROTOCOL, "%s", kcgi_strerror(error));
 		khttp_free(&req);
 	}
diff --git a/server.h b/server.h
index c04b871..45008a1 100644
--- a/server.h
+++ b/server.h
@@ -29,56 +29,73 @@
 #define khttp_urlpart(...) kutil_urlpart(NULL, __VA_ARGS__)
 #endif
 
-#define SQL(...) #__VA_ARGS__
-
 // Why does it return (const unsigned char *)?
 #define sqlite3_column_text(...) (const char *)sqlite3_column_text(__VA_ARGS__)
 
+#define SQL(...) #__VA_ARGS__
+
+#define ENUM_PAGES \
+	X(Networks, "networks") \
+	X(Contexts, "contexts") \
+	X(Events, "events") \
+	X(Search, "search") \
+	X(Stylesheet, "stylesheet")
+
 enum {
-	DatabaseVersionMin = 4,
-	DatabaseVersionMax = 5,
+#define X(page, path) page,
+	ENUM_PAGES
+#undef X
+	PagesLen,
 };
+extern const char *Pages[PagesLen];
 
-#define ENUM_TYPE \
-	X(Privmsg, "privmsg") \
-	X(Notice, "notice") \
-	X(Action, "action") \
-	X(Join, "join") \
-	X(Part, "part") \
-	X(Quit, "quit") \
-	X(Kick, "kick") \
-	X(Nick, "nick") \
-	X(Topic, "topic") \
-	X(Ban, "ban") \
-	X(Unban, "unban")
+#define ENUM_KEYS \
+	X(Network, "network", kvalid_stringne) \
+	X(Context, "context", kvalid_stringne) \
+	X(After, "after", kvalid_stringne) \
+	X(Before, "before", kvalid_stringne) \
+	X(Query, "query", kvalid_stringne) \
+	X(Offset, "offset", kvalid_int)
 
-enum Type {
-#define X(id, name) id,
-	ENUM_TYPE
+enum {
+#define X(key, name, valid) key,
+	ENUM_KEYS
 #undef X
-	TypesLen,
+	KeysLen,
 };
+extern const struct kvalid Keys[KeysLen];
 
-struct Event {
-	int64_t event;
-	time_t time;
+struct Scope {
 	const char *network;
 	const char *context;
-	enum Type type;
-	const char *nick;
-	const char *user;
-	const char *host;
-	const char *target;
-	const char *message;
+	const char *query;
 };
 
-extern sqlite3 *db;
+static inline struct Scope pageScope(struct kreq *req) {
+	struct Scope s = {0};
+	if (req->fieldmap[Network]) s.network = req->fieldmap[Network]->parsed.s;
+	if (req->fieldmap[Context]) s.context = req->fieldmap[Context]->parsed.s;
+	if (req->fieldmap[Query]) s.query = req->fieldmap[Query]->parsed.s;
+	return s;
+}
 
+extern int contextsRecent;
+extern bool contextsPublic;
 extern const char *NetworksQuery;
 extern const char *ContextsQuery;
+enum kcgi_err networksPage(struct kreq *req);
+enum kcgi_err contextsPage(struct kreq *req);
+
+extern int eventsGap;
+extern int eventsOverlap;
+extern int eventsLimit;
 extern const char *EventsAfterQuery;
 extern const char *EventsBeforeQuery;
 extern const char *SearchQuery;
+enum kcgi_err eventsPage(struct kreq *req);
+enum kcgi_err searchPage(struct kreq *req);
+
+extern sqlite3 *db;
 
 extern struct Statements {
 	sqlite3_stmt *networks;
@@ -88,12 +105,27 @@ extern struct Statements {
 	sqlite3_stmt *search;
 } stmt;
 
+static inline void dbPrepare(sqlite3_stmt **stmt, const char *query) {
+	int error = sqlite3_prepare_v3(
+		db, query, -1, SQLITE_PREPARE_PERSISTENT, stmt, NULL
+	);
+	if (error) errx(EX_SOFTWARE, "%s: %s", sqlite3_errmsg(db), query);
+}
+
+static inline void dbPrepareAll(void) {
+	dbPrepare(&stmt.networks, NetworksQuery);
+	dbPrepare(&stmt.contexts, ContextsQuery);
+	dbPrepare(&stmt.eventsAfter, EventsAfterQuery);
+	dbPrepare(&stmt.eventsBefore, EventsBeforeQuery);
+	dbPrepare(&stmt.search, SearchQuery);
+}
+
 static inline void dbClose(void) {
-	if (stmt.networks) sqlite3_finalize(stmt.networks);
-	if (stmt.contexts) sqlite3_finalize(stmt.contexts);
-	if (stmt.eventsAfter) sqlite3_finalize(stmt.eventsAfter);
-	if (stmt.eventsBefore) sqlite3_finalize(stmt.eventsBefore);
-	if (stmt.search) sqlite3_finalize(stmt.search);
+	sqlite3_finalize(stmt.networks);
+	sqlite3_finalize(stmt.contexts);
+	sqlite3_finalize(stmt.eventsAfter);
+	sqlite3_finalize(stmt.eventsBefore);
+	sqlite3_finalize(stmt.search);
 	sqlite3_close(db);
 }
 
@@ -115,47 +147,52 @@ dbBindText(sqlite3_stmt *stmt, const char *param, const char *value) {
 	errx(EX_SOFTWARE, "sqlite3_bind_text: %s", sqlite3_errmsg(db));
 }
 
-#define ENUM_PAGES \
-	X(Networks, "networks") \
-	X(Contexts, "contexts") \
-	X(Events, "events") \
-	X(Search, "search") \
-	X(Stylesheet, "stylesheet")
-
 enum {
-#define X(page, path) page,
-	ENUM_PAGES
-#undef X
-	PagesLen,
+	DatabaseVersionMin = 4,
+	DatabaseVersionMax = 5,
 };
 
-extern const char *Pages[PagesLen];
-
-#define ENUM_KEYS \
-	X(Network, "network", kvalid_stringne) \
-	X(Context, "context", kvalid_stringne) \
-	X(After, "after", kvalid_stringne) \
-	X(Before, "before", kvalid_stringne) \
-	X(Query, "query", kvalid_stringne) \
-	X(Offset, "offset", kvalid_int)
+#define ENUM_TYPE \
+	X(Privmsg, "privmsg") \
+	X(Notice, "notice") \
+	X(Action, "action") \
+	X(Join, "join") \
+	X(Part, "part") \
+	X(Quit, "quit") \
+	X(Kick, "kick") \
+	X(Nick, "nick") \
+	X(Topic, "topic") \
+	X(Ban, "ban") \
+	X(Unban, "unban")
 
-enum {
-#define X(key, name, valid) key,
-	ENUM_KEYS
+enum Type {
+#define X(id, name) id,
+	ENUM_TYPE
 #undef X
-	KeysLen,
+	TypesLen,
 };
 
-extern const struct kvalid Keys[KeysLen];
-
-extern bool pagePublic;
-extern int pageLimit;
-extern int pageRecent;
+struct Event {
+	int64_t event;
+	time_t time;
+	const char *network;
+	const char *context;
+	enum Type type;
+	const char *nick;
+	const char *user;
+	const char *host;
+	const char *target;
+	const char *message;
+};
 
-enum kcgi_err pageNetworks(struct kreq *req);
-enum kcgi_err pageContexts(struct kreq *req);
-enum kcgi_err pageEvents(struct kreq *req);
-enum kcgi_err pageSearch(struct kreq *req);
+extern const char *htmlStylesheet;
+enum kcgi_err htmlHead(struct khtmlreq *html, const char *title);
+enum kcgi_err htmlScopeFields(struct khtmlreq *html, struct Scope scope);
+enum kcgi_err htmlNav(struct khtmlreq *html, struct Scope scope);
+enum kcgi_err htmlFooter(struct khtmlreq *html);
+enum kcgi_err htmlEvent(
+	struct khtmlreq *html, struct Scope scope, struct Event event
+);
 
 static inline enum kcgi_err
 httpHead(struct kreq *req, enum khttp http, enum kmime mime) {
@@ -176,25 +213,3 @@ static inline enum kcgi_err httpFail(struct kreq *req, enum khttp http) {
 		|| khttp_body(req)
 		|| khttp_printf(req, "%s\n", khttps[http]);
 }
-
-struct Scope {
-	const char *network;
-	const char *context;
-	const char *query;
-};
-
-static inline struct Scope htmlScope(struct kreq *req) {
-	struct Scope s = {0};
-	if (req->fieldmap[Network]) s.network = req->fieldmap[Network]->parsed.s;
-	if (req->fieldmap[Context]) s.context = req->fieldmap[Context]->parsed.s;
-	if (req->fieldmap[Query]) s.query = req->fieldmap[Query]->parsed.s;
-	return s;
-}
-
-extern const char *htmlStylesheet;
-enum kcgi_err htmlHead(struct khtmlreq *html, const char *title);
-enum kcgi_err htmlNav(struct khtmlreq *html, struct Scope scope);
-enum kcgi_err htmlFooter(struct khtmlreq *html);
-enum kcgi_err htmlEvent(
-	struct khtmlreq *html, struct Scope scope, struct Event event
-);