diff options
Diffstat (limited to '')
-rw-r--r-- | contexts.c | 16 | ||||
-rw-r--r-- | events.c | 136 | ||||
-rw-r--r-- | html.c | 559 | ||||
-rw-r--r-- | networks.c | 4 | ||||
-rw-r--r-- | search.c | 39 | ||||
-rw-r--r-- | server.h | 26 |
6 files changed, 360 insertions, 420 deletions
diff --git a/contexts.c b/contexts.c index 5eba403..5c5e5cc 100644 --- a/contexts.c +++ b/contexts.c @@ -58,8 +58,8 @@ const char *ContextsQuery = SQL( ); enum kcgi_err contextsPage(struct kreq *req) { - struct Scope scope = pageScope(req); - if (!scope.network) return httpFail(req, KHTTP_400); + if (!req->fieldmap[Network]) return httpFail(req, KHTTP_400); + const char *network = req->fieldmap[Network]->parsed.s; enum kcgi_err error = 0 || httpHead(req, KHTTP_200, KMIME_TEXT_HTML) @@ -69,12 +69,12 @@ enum kcgi_err contextsPage(struct kreq *req) { struct khtmlreq html; error = error || khtml_open(&html, req, 0) - || htmlHead(&html, scope.network) - || htmlNav(&html, scope); + || htmlHead(&html, network) + || htmlNav(&html, req); if (error) return error; sqlite3_reset(stmt.contexts); - dbBindText(stmt.contexts, ":network", scope.network); + dbBindText(stmt.contexts, ":network", network); dbBindInt(stmt.contexts, ":recent", contextsRecent); dbBindInt(stmt.contexts, ":public", contextsPublic); @@ -94,7 +94,7 @@ enum kcgi_err contextsPage(struct kreq *req) { bool active = sqlite3_column_int(stmt.contexts, i++); enum State prev = state; - state = (active ? Active : (query ? Queries : Channels)); + state = (active ? Active : query ? Queries : Channels); if (state != prev) { error = 0 || khtml_closeelem(&html, 1) @@ -107,7 +107,7 @@ enum kcgi_err contextsPage(struct kreq *req) { char *href = khttp_urlpart( NULL, NULL, Pages[Events], - Keys[Network].name, scope.network, + Keys[Network].name, network, Keys[Context].name, context, NULL ); @@ -126,7 +126,7 @@ enum kcgi_err contextsPage(struct kreq *req) { if (error) return error; sqlite3_reset(stmt.motd); - dbBindText(stmt.motd, ":network", scope.network); + dbBindText(stmt.motd, ":network", network); result = sqlite3_step(stmt.motd); if (result == SQLITE_ROW) { diff --git a/events.c b/events.c index e5b0777..932f535 100644 --- a/events.c +++ b/events.c @@ -34,23 +34,10 @@ static const char *timestamp(time_t time) { return stamp; } -static enum kcgi_err tidyField(struct kreq *req, struct khtmlreq *html) { - if (!req->fieldmap[Tidy]) return KCGI_OK; - return khtml_attr( - html, KELEM_INPUT, - KATTR_TYPE, "hidden", - KATTR_NAME, Keys[Tidy].name, - KATTR_VALUE, req->fieldmap[Tidy]->parsed.s, - KATTR__MAX - ); -} - static enum kcgi_err -dateForm(struct kreq *req, struct khtmlreq *html, struct Scope scope) { - struct kpair *field = req->fieldmap[After]; - if (!field) field = req->fieldmap[Before]; +dateForm(struct khtmlreq *html, struct kreq *req, const char *current) { struct tm tm = {0}; - if (!strptime(field->parsed.s, "%F", &tm)) { + if (!strptime(current, "%F", &tm)) { tm = *gmtime(&(time_t) { time(NULL) }); } char date[sizeof("0000-00-00")]; @@ -62,7 +49,8 @@ dateForm(struct kreq *req, struct khtmlreq *html, struct Scope scope) { KATTR_ACTION, Pages[Events], KATTR__MAX ) - || htmlScopeFields(html, scope) + || htmlHidden(html, req, Network) + || htmlHidden(html, req, Context) || khtml_attr( html, KELEM_INPUT, KATTR_TYPE, "date", @@ -70,20 +58,14 @@ dateForm(struct kreq *req, struct khtmlreq *html, struct Scope scope) { KATTR_VALUE, date, KATTR__MAX ) - || tidyField(req, html) - || khtml_attr( - html, KELEM_INPUT, - KATTR_TYPE, "submit", - KATTR_VALUE, "Jump", - KATTR__MAX - ) - || khtml_closeelem(html, 1); + || htmlHidden(html, req, Tidy) + || khtml_elem(html, KELEM_BUTTON) + || khtml_puts(html, "Jump") + || khtml_closeelem(html, 2); } static enum kcgi_err -displayForm(struct kreq *req, struct khtmlreq *html, struct Scope scope) { - struct kpair *time = req->fieldmap[After]; - if (!time) time = req->fieldmap[Before]; +displayForm(struct khtmlreq *html, struct kreq *req, bool tidy) { return 0 || khtml_attr( html, KELEM_FORM, @@ -91,32 +73,40 @@ displayForm(struct kreq *req, struct khtmlreq *html, struct Scope scope) { KATTR_ACTION, Pages[Events], KATTR__MAX ) - || htmlScopeFields(html, scope) - || khtml_attr( - html, KELEM_INPUT, - KATTR_TYPE, "hidden", - KATTR_NAME, time->key, - KATTR_VALUE, time->parsed.s, - KATTR__MAX - ) + || htmlHidden(html, req, Network) + || htmlHidden(html, req, Context) + || htmlHidden(html, req, After) + || htmlHidden(html, req, Before) || khtml_elem(html, KELEM_LABEL) || khtml_attr( html, KELEM_INPUT, KATTR_TYPE, "checkbox", KATTR_NAME, Keys[Tidy].name, KATTR_VALUE, "1", - (req->fieldmap[Tidy] ? KATTR_CHECKED : KATTR__MAX), "checked", + (tidy ? KATTR_CHECKED : KATTR__MAX), "checked", KATTR__MAX ) || khtml_puts(html, "Hide general events") || khtml_closeelem(html, 1) - || khtml_attr( - html, KELEM_INPUT, - KATTR_TYPE, "submit", - KATTR_VALUE, "Apply", - KATTR__MAX - ) - || khtml_closeelem(html, 1); + || khtml_elem(html, KELEM_BUTTON) + || khtml_puts(html, "Apply") + || khtml_closeelem(html, 2); +} + +static char *pageURL( + const char *network, const char *context, + enum Key key, time_t time, bool tidy +) { + char *url = khttp_urlpart( + NULL, NULL, Pages[Events], + Keys[Network].name, network, + Keys[Context].name, context, + Keys[key].name, timestamp(time), + (tidy ? Keys[Tidy].name : NULL), "1", + NULL + ); + if (!url) err(EX_OSERR, "khttp_urlpart"); + return url; } const char *EventsTopicQuery = SQL( @@ -180,14 +170,17 @@ const char *EventsBeforeQuery = SQL( ); 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[Network] || !req->fieldmap[Context]) { + return httpFail(req, KHTTP_400); + } + const char *network = req->fieldmap[Network]->parsed.s; + const char *context = req->fieldmap[Context]->parsed.s; if (!req->fieldmap[After] && !req->fieldmap[Before]) { char *url = khttp_urlpart( NULL, NULL, Pages[Events], - Keys[Network].name, scope.network, - Keys[Context].name, scope.context, + Keys[Network].name, network, + Keys[Context].name, context, Keys[Before].name, timestamp(time(NULL)), NULL ); @@ -200,6 +193,7 @@ enum kcgi_err eventsPage(struct kreq *req) { const char *time = req->fieldmap[Before] ? req->fieldmap[Before]->parsed.s : req->fieldmap[After]->parsed.s; + bool tidy = (req->fieldmap[Tidy] ? req->fieldmap[Tidy]->parsed.i : false); enum kcgi_err error = 0 || httpHead(req, KHTTP_200, KMIME_TEXT_HTML) @@ -209,18 +203,18 @@ enum kcgi_err eventsPage(struct kreq *req) { struct khtmlreq html; error = error || khtml_open(&html, req, 0) - || htmlHead(&html, scope.context) - || htmlNav(&html, scope) + || htmlHead(&html, context) + || htmlNav(&html, req) || khtml_elem(&html, KELEM_DIV) - || dateForm(req, &html, scope) - || displayForm(req, &html, scope) + || dateForm(&html, req, time) + || displayForm(&html, req, tidy) || khtml_closeelem(&html, 1) || khtml_elem(&html, KELEM_TABLE); if (error) return error; sqlite3_reset(stmt.topic); - dbBindText(stmt.topic, ":network", scope.network); - dbBindText(stmt.topic, ":context", scope.context); + dbBindText(stmt.topic, ":network", network); + dbBindText(stmt.topic, ":context", context); dbBindText(stmt.topic, ":time", time); dbBindInt(stmt.topic, ":public", contextsPublic); @@ -241,10 +235,10 @@ enum kcgi_err eventsPage(struct kreq *req) { if (req->fieldmap[Before]) events = stmt.eventsBefore; sqlite3_reset(events); - dbBindText(events, ":network", scope.network); - dbBindText(events, ":context", scope.context); + dbBindText(events, ":network", network); + dbBindText(events, ":context", context); dbBindText(events, ":time", time); - dbBindInt(events, ":tidy", (req->fieldmap[Tidy] ? Join : TypesLen)); + dbBindInt(events, ":tidy", (tidy ? Join : TypesLen)); dbBindInt(events, ":public", contextsPublic); dbBindInt(events, ":limit", eventsLimit); @@ -264,21 +258,12 @@ enum kcgi_err eventsPage(struct kreq *req) { 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), - (req->fieldmap[Tidy] ? Keys[Tidy].name : NULL), "1", - NULL - ); - if (!base) err(EX_OSERR, "khttp_urlpart"); - char *href = NULL; - asprintf(&href, "%s#%" PRId64, base, event.event); + char *page = pageURL( + network, context, Before, event.time + eventsOverlap, tidy + ); + asprintf(&href, "%s#%" PRId64, page, event.event); if (!href) err(EX_OSERR, "asprintf"); - free(base); - error = 0 || khtml_attr(&html, KELEM_TR, KATTR_CLASS, "page", KATTR__MAX) || khtml_attr(&html, KELEM_TH, KATTR_COLSPAN, "3", KATTR__MAX) @@ -286,6 +271,7 @@ enum kcgi_err eventsPage(struct kreq *req) { || khtml_puts(&html, "Earlier messages") || khtml_closeelem(&html, 3); free(href); + free(page); if (error) return error; } @@ -300,21 +286,15 @@ enum kcgi_err eventsPage(struct kreq *req) { prevEvent = event.event; prevTime = event.time; - error = htmlEvent(&html, scope, event); + error = htmlEvent(&html, req, &event); if (error) return error; } if (result != SQLITE_DONE) errx(EX_SOFTWARE, "%s", sqlite3_errmsg(db)); if (rows && (rows == eventsLimit || req->fieldmap[Before])) { - char *href = khttp_urlpart( - NULL, NULL, Pages[Events], - Keys[Network].name, scope.network, - Keys[Context].name, scope.context, - Keys[After].name, timestamp(prevTime - eventsOverlap), - (req->fieldmap[Tidy] ? Keys[Tidy].name : NULL), "1", - NULL + char *href = pageURL( + network, context, After, prevTime - eventsOverlap, tidy ); - if (!href) err(EX_OSERR, "khttp_urlpart"); error = 0 || khtml_attr(&html, KELEM_TR, KATTR_CLASS, "page", KATTR__MAX) || khtml_attr(&html, KELEM_TH, KATTR_COLSPAN, "3", KATTR__MAX) diff --git a/html.c b/html.c index 2d3ce2f..a28d84b 100644 --- a/html.c +++ b/html.c @@ -47,32 +47,19 @@ 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 +htmlHidden(struct khtmlreq *html, struct kreq *req, enum Key key) { + if (!req->fieldmap[key]) return KCGI_OK; + return khtml_attr( + html, KELEM_INPUT, + KATTR_TYPE, "hidden", + KATTR_NAME, Keys[key].name, + KATTR_VALUE, req->fieldmap[key]->val, + KATTR__MAX + ); } -enum kcgi_err -htmlNav(struct khtmlreq *html, struct Scope scope) { +enum kcgi_err htmlNav(struct khtmlreq *html, struct kreq *req) { enum kcgi_err error = 0 || khtml_elem(html, KELEM_NAV) || khtml_elem(html, KELEM_OL) @@ -82,44 +69,53 @@ htmlNav(struct khtmlreq *html, struct Scope scope) { || khtml_closeelem(html, 2); if (error) return error; - if (scope.network) { + const char *network = NULL; + const char *context = NULL; + if (req->fieldmap[Network]) { + network = req->fieldmap[Network]->parsed.s; + if (req->fieldmap[Context]) { + context = req->fieldmap[Context]->parsed.s; + } + } + + if (network) { char *href = khttp_urlpart( NULL, NULL, Pages[Contexts], - Keys[Network].name, scope.network, + Keys[Network].name, network, NULL ); if (!href) err(EX_OSERR, "khttp_urlpart"); error = 0 || khtml_elem(html, KELEM_LI) || khtml_attr(html, KELEM_A, KATTR_HREF, href, KATTR__MAX) - || khtml_puts(html, scope.network) + || khtml_puts(html, network) || khtml_closeelem(html, 2); + free(href); if (error) return error; } - if (scope.network && scope.context) { + if (context) { + const char *context = req->fieldmap[Context]->parsed.s; char *href = khttp_urlpart( NULL, NULL, Pages[Events], - Keys[Network].name, scope.network, - Keys[Context].name, scope.context, + Keys[Network].name, network, + Keys[Context].name, context, NULL ); if (!href) err(EX_OSERR, "khttp_urlpart"); error = 0 || khtml_elem(html, KELEM_LI) || khtml_attr(html, KELEM_A, KATTR_HREF, href, KATTR__MAX) - || khtml_puts(html, scope.context) + || khtml_puts(html, context) || khtml_closeelem(html, 2); + free(href); if (error) return error; } - char label[256]; - snprintf( - label, sizeof(label), "Search%s%s", - (scope.network ? " " : ""), - (scope.context ? scope.context : scope.network ? scope.network : "") - ); - + const char *query = NULL; + if (req->fieldmap[Query]) { + query = req->fieldmap[Query]->parsed.s; + } return 0 || khtml_closeelem(html, 1) || khtml_attr( @@ -132,44 +128,124 @@ htmlNav(struct khtmlreq *html, struct Scope scope) { html, KELEM_INPUT, KATTR_TYPE, "search", KATTR_NAME, Keys[Query].name, - KATTR_VALUE, (scope.query ? scope.query : ""), + KATTR_VALUE, (query ? query : ""), KATTR__MAX ) - || htmlScopeFields(html, scope) - || khtml_attr( - html, KELEM_INPUT, - KATTR_TYPE, "submit", - KATTR_VALUE, label, - KATTR__MAX + || htmlHidden(html, req, Network) + || htmlHidden(html, req, Context) + || khtml_elem(html, KELEM_BUTTON) + || khtml_printf( + html, "Search %s", + (context ? context : network ? network : "") ) - || khtml_closeelem(html, 2); + || khtml_closeelem(html, 3); } -static const char *SourceURL = "https://git.causal.agency/scooper"; -static const char *SyntaxURL = { - "https://www.sqlite.org/fts5.html#full_text_query_syntax" -}; -static const char *Columns = { - "network, channel, query, nick, user, target, message" -}; +enum kcgi_err linkify(struct khtmlreq *html, const char *str, size_t len) { + static const char *Pattern = "https?://([^[:space:]>\"()]|[(][^)]*[)])+"; + static regex_t regex; + if (!regex.re_nsub) { + int error = regcomp(®ex, Pattern, REG_EXTENDED); + assert(!error); + } -enum kcgi_err htmlFooter(struct khtmlreq *html) { - return 0 - || khtml_closeto(html, 0) - || khtml_elem(html, KELEM_FOOTER) - || khtml_elem(html, KELEM_SPAN) - || khtml_attr(html, KELEM_A, KATTR_HREF, SourceURL, KATTR__MAX) - || khtml_puts(html, "scooper is AGPLv3+") - || khtml_closeelem(html, 2) - || khtml_putc(html, ' ') - || khtml_elem(html, KELEM_SPAN) - || khtml_attr(html, KELEM_A, KATTR_HREF, SyntaxURL, KATTR__MAX) - || khtml_puts(html, "Search syntax") - || khtml_closeelem(html, 1) - || khtml_putc(html, ' ') - || khtml_attr(html, KELEM_SPAN, KATTR_TITLE, Columns, KATTR__MAX) - || khtml_puts(html, "Columns") - || khtml_closeto(html, 0); + regmatch_t match = {0}; + while (!regexec(®ex, str, 1, &match, 0)) { + if ((size_t)match.rm_eo > len) break; + + char *href = strndup(&str[match.rm_so], match.rm_eo - match.rm_so); + if (!href) err(EX_OSERR, "strndup"); + + enum kcgi_err error = 0 + || khtml_write(str, match.rm_so, html) + || khtml_attr(html, KELEM_A, KATTR_HREF, href, KATTR__MAX) + || khtml_write(&str[match.rm_so], match.rm_eo - match.rm_so, html) + || khtml_closeelem(html, 1); + free(href); + if (error) return error; + + str += match.rm_eo; + len -= match.rm_eo; + } + if (len) return khtml_write(str, len, html); + return KCGI_OK; +} + +static const struct Style { + int fg, bg; + bool b, r, i, u; +} Default = { .fg = 99, .bg = 99 }; + +enum kcgi_err htmlStyle(struct khtmlreq *html, struct Style style) { + enum kcgi_err error = KCGI_OK; + char class[sizeof("fg99")]; + if (style.fg != Default.fg) { + snprintf(class, sizeof(class), "fg%02d", style.fg); + error = error || khtml_attr( + html, KELEM_SPAN, + KATTR_CLASS, class, + KATTR__MAX + ); + } + if (style.bg != Default.bg) { + snprintf(class, sizeof(class), "bg%02d", style.bg); + error = error || khtml_attr( + html, KELEM_SPAN, + KATTR_CLASS, class, + KATTR__MAX + ); + } + if (style.b) error = error || khtml_elem(html, KELEM_B); + if (style.r) error = error || khtml_elem(html, KELEM_MARK); + if (style.i) error = error || khtml_elem(html, KELEM_I); + if (style.u) error = error || khtml_elem(html, KELEM_U); + return error; +} + +enum kcgi_err htmlIRC(struct khtmlreq *html, const char *str) { + enum kcgi_err error = khtml_attr( + html, KELEM_SPAN, + KATTR_CLASS, "irc", + KATTR__MAX + ); + if (error) return error; + + size_t top = khtml_elemat(html); + struct Style style = Default; + for (;;) { + size_t len = strcspn(str, "\2\3\17\26\35\37"); + if (len) { + error = 0 + || khtml_closeto(html, top) + || htmlStyle(html, style) + || linkify(html, str, len); + if (error) return error; + } + + str += len; + if (!*str) break; + switch (*str++) { + break; case '\2': style.b ^= true; + break; case '\17': style = Default; + break; case '\26': style.r ^= true; + break; case '\35': style.i ^= true; + break; case '\37': style.u ^= true; + break; case '\3': { + if (!isdigit(*str)) { + style.fg = Default.fg; + style.bg = Default.bg; + break; + } + style.fg = *str++ - '0'; + if (isdigit(*str)) style.fg = style.fg * 10 + *str++ - '0'; + if (str[0] != ',' || !isdigit(str[1])) break; + str++; + style.bg = *str++ - '0'; + if (isdigit(*str)) style.bg = style.bg * 10 + *str++ - '0'; + } + } + } + return khtml_closeto(html, top) || khtml_closeelem(html, 1); } static const char *timestamp(time_t time) { @@ -178,74 +254,73 @@ static const char *timestamp(time_t time) { return stamp; } -static enum kcgi_err eventTime(struct khtmlreq *html, struct Event event) { - char *base = NULL; - if (event.network && event.context) { - base = khttp_urlpart( +static enum kcgi_err +eventTime(struct khtmlreq *html, const struct Event *event) { + char *page = NULL; + char *href = NULL; + if (event->network && event->context) { + page = khttp_urlpart( NULL, NULL, Pages[Events], - Keys[Network].name, event.network, - Keys[Context].name, event.context, - Keys[After].name, timestamp(event.time - eventsOverlap), + Keys[Network].name, event->network, + Keys[Context].name, event->context, + Keys[After].name, timestamp(event->time - eventsOverlap), NULL ); - if (!base) err(EX_OSERR, "khttp_urlpart"); + if (!page) err(EX_OSERR, "khttp_urlpart"); } - char *href = NULL; - asprintf(&href, "%s#%" PRId64, (base ? base : ""), event.event); + asprintf(&href, "%s#%" PRId64, (page ? page : ""), event->event); if (!href) err(EX_OSERR, "asprintf"); - if (base) free(base); + if (page) free(page); + const char *stamp = timestamp(event->time); enum kcgi_err error = 0 || khtml_attr(html, KELEM_TD, KATTR_CLASS, "time", KATTR__MAX) || khtml_attr(html, KELEM_A, KATTR_HREF, href, KATTR__MAX) || khtml_attr( html, KELEM_TIME, - KATTR_DATETIME, timestamp(event.time), + KATTR_DATETIME, stamp, KATTR__MAX ) - || khtml_puts(html, timestamp(event.time)) + || khtml_puts(html, stamp) || khtml_closeelem(html, 3); - free(href); return error; } static enum kcgi_err -eventNetwork(struct khtmlreq *html, struct Scope scope, struct Event event) { - if (scope.network || !scope.query || !event.network) return KCGI_OK; +eventNetwork(struct khtmlreq *html, struct kreq *req, struct Event *event) { + if (!req->fieldmap[Query] || req->fieldmap[Network]) return KCGI_OK; char *href = khttp_urlpart( NULL, NULL, Pages[Search], - Keys[Network].name, event.network, - Keys[Query].name, scope.query, + Keys[Query].name, req->fieldmap[Query]->parsed.s, + Keys[Network].name, event->network, NULL ); if (!href) err(EX_OSERR, "khttp_urlpart"); enum kcgi_err error = 0 || khtml_attr(html, KELEM_TD, KATTR_CLASS, "network", KATTR__MAX) || khtml_attr(html, KELEM_A, KATTR_HREF, href, KATTR__MAX) - || khtml_puts(html, event.network) + || khtml_puts(html, event->network) || khtml_closeelem(html, 2); free(href); return error; } static enum kcgi_err -eventContext(struct khtmlreq *html, struct Scope scope, struct Event event) { - if (scope.context || !scope.query || !event.network || !event.context) { - return KCGI_OK; - } +eventContext(struct khtmlreq *html, struct kreq *req, struct Event *event) { + if (!req->fieldmap[Query] || req->fieldmap[Context]) return KCGI_OK; char *href = khttp_urlpart( NULL, NULL, Pages[Search], - Keys[Network].name, event.network, - Keys[Context].name, event.context, - Keys[Query].name, scope.query, + Keys[Query].name, req->fieldmap[Query]->parsed.s, + Keys[Network].name, event->network, + Keys[Context].name, event->context, NULL ); if (!href) err(EX_OSERR, "khttp_urlpart"); enum kcgi_err error = 0 || khtml_attr(html, KELEM_TD, KATTR_CLASS, "context", KATTR__MAX) || khtml_attr(html, KELEM_A, KATTR_HREF, href, KATTR__MAX) - || khtml_puts(html, event.context) + || khtml_puts(html, event->context) || khtml_closeelem(html, 2); free(href); return error; @@ -262,115 +337,93 @@ static int hash(const char *str) { return 2 + hash % 74; } -static const char *colorClass(int color) { - static char class[sizeof("fg99")]; - snprintf(class, sizeof(class), "fg%02d", color); - return class; -} - -static enum kcgi_err eventNick(struct khtmlreq *html, struct Event event) { +static enum kcgi_err +eventNick(struct khtmlreq *html, const struct Event *event) { char *mask = NULL; - asprintf(&mask, "%s!%s@%s", event.nick, event.user, event.host); + char class[sizeof("fg99")]; + snprintf(class, sizeof(class), "fg%02d", hash(event->user)); + asprintf(&mask, "%s!%s@%s", event->nick, event->user, event->host); if (!mask) err(EX_OSERR, "asprintf"); - + const char *format = "%s"; + switch (event->type) { + break; case Privmsg: format = "<%s>"; + break; case Notice: format = "-%s-"; + break; case Action: format = "* %s"; + break; default:; + } enum kcgi_err error = 0 || khtml_attr(html, KELEM_TD, KATTR_CLASS, "nick", KATTR__MAX) || khtml_attr( html, KELEM_SPAN, - KATTR_CLASS, colorClass(hash(event.user)), + KATTR_CLASS, class, KATTR_TITLE, mask, KATTR__MAX - ); + ) + || khtml_printf(html, format, event->nick) + || khtml_closeelem(html, 2); free(mask); - if (error) return error; - - switch (event.type) { - break; case Privmsg: error = khtml_printf(html, "<%s>", event.nick); - break; case Action: error = khtml_printf(html, "* %s", event.nick); - break; case Notice: error = khtml_printf(html, "-%s-", event.nick); - break; default: error = khtml_puts(html, event.nick); - } - return error || khtml_closeelem(html, 2); -} - -static enum kcgi_err typeJoin(struct khtmlreq *html, struct Event event) { - (void)event; - return khtml_puts(html, "joined"); -} - -static enum kcgi_err typePart(struct khtmlreq *html, struct Event event) { - if (!event.message) return khtml_puts(html, "left"); - return 0 - || khtml_puts(html, "left (") - || htmlIRC(html, event.message) - || khtml_puts(html, ")"); -} - -static enum kcgi_err typeQuit(struct khtmlreq *html, struct Event event) { - if (!event.message) return khtml_puts(html, "quit"); - return 0 - || khtml_puts(html, "quit (") - || htmlIRC(html, event.message) - || khtml_puts(html, ")"); + return error; } -static enum kcgi_err typeKick(struct khtmlreq *html, struct Event event) { - if (!event.target) return KCGI_OK; - if (!event.message) return khtml_printf(html, "kicked %s", event.target); +static enum kcgi_err +maybeMessage(struct khtmlreq *html, const struct Event *event) { + if (!event->message) return KCGI_OK; return 0 - || khtml_printf(html, "kicked %s (", event.target) - || htmlIRC(html, event.message) + || khtml_puts(html, " (") + || htmlIRC(html, event->message) || khtml_puts(html, ")"); } -static enum kcgi_err typeNick(struct khtmlreq *html, struct Event event) { - if (!event.target) return KCGI_OK; - return 0 - || khtml_puts(html, "changed nick to ") - || khtml_attr( - html, KELEM_SPAN, - KATTR_CLASS, colorClass(hash(event.user)), - KATTR__MAX - ) - || khtml_puts(html, event.target) - || khtml_closeelem(html, 1); -} - -static enum kcgi_err typeTopic(struct khtmlreq *html, struct Event event) { - if (!event.message) return KCGI_OK; - return 0 - || khtml_puts(html, "set the topic: ") - || htmlIRC(html, event.message); -} - -static enum kcgi_err typeBan(struct khtmlreq *html, struct Event event) { - if (!event.target) return KCGI_OK; - return khtml_printf(html, "banned %s", event.target); -} - -static enum kcgi_err typeUnban(struct khtmlreq *html, struct Event event) { - if (!event.target) return KCGI_OK; - return khtml_printf(html, "unbanned %s", event.target); -} - -static enum kcgi_err eventMessage(struct khtmlreq *html, struct Event event) { +static enum kcgi_err +eventMessage(struct khtmlreq *html, const struct Event *event) { enum kcgi_err error = khtml_attr( html, KELEM_TD, KATTR_CLASS, "message", KATTR__MAX ); if (error) return error; - switch (event.type) { - break; case Join: error = typeJoin(html, event); - break; case Part: error = typePart(html, event); - break; case Quit: error = typeQuit(html, event); - break; case Kick: error = typeKick(html, event); - break; case Nick: error = typeNick(html, event); - break; case Topic: error = typeTopic(html, event); - break; case Ban: error = typeBan(html, event); - break; case Unban: error = typeUnban(html, event); + switch (event->type) { + break; case Join: { + error = khtml_puts(html, "joined"); + } + break; case Part: { + error = khtml_puts(html, "left") || maybeMessage(html, event); + } + break; case Quit: { + error = khtml_puts(html, "quit") || maybeMessage(html, event); + } + break; case Kick: { + if (!event->target) break; + error = 0 + || khtml_printf(html, "kicked %s", event->target) + || maybeMessage(html, event); + } + break; case Nick: { + if (!event->target) break; + char class[sizeof("fg99")]; + snprintf(class, sizeof(class), "fg%02d", hash(event->user)); + error = 0 + || khtml_puts(html, "changed nick to ") + || khtml_attr(html, KELEM_SPAN, KATTR_CLASS, class, KATTR__MAX) + || khtml_puts(html, event->target) + || khtml_closeelem(html, 1); + } + break; case Topic: { + if (!event->message) break; + error = 0 + || khtml_puts(html, "set the topic: ") + || htmlIRC(html, event->message); + } + break; case Ban: { + if (!event->target) break; + error = khtml_printf(html, "banned %s", event->target); + } + break; case Unban: { + if (!event->target) break; + error = khtml_printf(html, "unbanned %s", event->target); + } break; default: { - if (event.message) error = htmlIRC(html, event.message); + if (event->message) error = htmlIRC(html, event->message); } } return error || khtml_closeelem(html, 1); @@ -383,126 +436,46 @@ static const char *Types[TypesLen] = { }; enum kcgi_err -htmlEvent(struct khtmlreq *html, struct Scope scope, struct Event event) { - const char *type = (event.type < TypesLen ? Types[event.type] : "unknown"); +htmlEvent(struct khtmlreq *html, struct kreq *req, struct Event *event) { + const char *type = (event->type < TypesLen ? Types[event->type] : "unknown"); return 0 || khtml_attrx( html, KELEM_TR, - KATTR_ID, KATTRX_INT, event.event, + KATTR_ID, KATTRX_INT, event->event, KATTR_CLASS, KATTRX_STRING, type, KATTR__MAX ) || eventTime(html, event) - || eventNetwork(html, scope, event) - || eventContext(html, scope, event) + || eventNetwork(html, req, event) + || eventContext(html, req, event) || eventNick(html, event) || eventMessage(html, event) || khtml_closeelem(html, 1); } -enum kcgi_err linkify(struct khtmlreq *html, const char *str, size_t len) { - static const char *Pattern = "https?://([^[:space:]>\"()]|[(][^)]*[)])+"; - static regex_t regex; - if (!regex.re_nsub) { - int error = regcomp(®ex, Pattern, REG_EXTENDED); - assert(!error); - } - - regmatch_t match = {0}; - while (!regexec(®ex, str, 1, &match, 0)) { - if ((size_t)match.rm_eo > len) break; - - char *href = strndup(&str[match.rm_so], match.rm_eo - match.rm_so); - if (!href) err(EX_OSERR, "strndup"); - - enum kcgi_err error = 0 - || khtml_write(str, match.rm_so, html) - || khtml_attr(html, KELEM_A, KATTR_HREF, href, KATTR__MAX) - || khtml_write(&str[match.rm_so], match.rm_eo - match.rm_so, html) - || khtml_closeelem(html, 1); - free(href); - if (error) return error; - - str += match.rm_eo; - len -= match.rm_eo; - } - if (len) return khtml_write(str, len, html); - return KCGI_OK; -} - -static const struct Style { - int fg, bg; - bool b, r, i, u; -} Default = { .fg = 99, .bg = 99 }; - -enum kcgi_err htmlStyle(struct khtmlreq *html, struct Style style) { - enum kcgi_err error = KCGI_OK; - char class[sizeof("fg99")]; - if (style.fg != Default.fg) { - snprintf(class, sizeof(class), "fg%02d", style.fg); - error = error || khtml_attr( - html, KELEM_SPAN, - KATTR_CLASS, class, - KATTR__MAX - ); - } - if (style.bg != Default.bg) { - snprintf(class, sizeof(class), "bg%02d", style.bg); - error = error || khtml_attr( - html, KELEM_SPAN, - KATTR_CLASS, class, - KATTR__MAX - ); - } - if (style.b) error = error || khtml_elem(html, KELEM_B); - if (style.r) error = error || khtml_elem(html, KELEM_MARK); - if (style.i) error = error || khtml_elem(html, KELEM_I); - if (style.u) error = error || khtml_elem(html, KELEM_U); - return error; -} - -enum kcgi_err htmlIRC(struct khtmlreq *html, const char *str) { - enum kcgi_err error = khtml_attr( - html, KELEM_SPAN, - KATTR_CLASS, "irc", - KATTR__MAX - ); - if (error) return error; - - size_t top = khtml_elemat(html); - struct Style style = Default; - for (;;) { - size_t len = strcspn(str, "\2\3\17\26\35\37"); - if (len) { - error = 0 - || khtml_closeto(html, top) - || htmlStyle(html, style) - || linkify(html, str, len); - if (error) return error; - } +static const char *SourceURL = "https://git.causal.agency/scooper"; +static const char *SyntaxURL = { + "https://www.sqlite.org/fts5.html#full_text_query_syntax" +}; +static const char *Columns = { + "network, channel, query, nick, user, target, message" +}; - str += len; - if (!*str) break; - switch (*str++) { - break; case '\2': style.b ^= true; - break; case '\17': style = Default; - break; case '\26': style.r ^= true; - break; case '\35': style.i ^= true; - break; case '\37': style.u ^= true; - break; case '\3': { - if (!isdigit(*str)) { - style.fg = Default.fg; - style.bg = Default.bg; - break; - } - style.fg = *str++ - '0'; - if (isdigit(*str)) style.fg = style.fg * 10 + *str++ - '0'; - if (str[0] != ',' || !isdigit(str[1])) break; - str++; - style.bg = *str++ - '0'; - if (isdigit(*str)) style.bg = style.bg * 10 + *str++ - '0'; - } - } - } - return khtml_closeto(html, top) || khtml_closeelem(html, 1); +enum kcgi_err htmlFooter(struct khtmlreq *html) { + return 0 + || khtml_closeto(html, 0) + || khtml_elem(html, KELEM_FOOTER) + || khtml_elem(html, KELEM_SPAN) + || khtml_attr(html, KELEM_A, KATTR_HREF, SourceURL, KATTR__MAX) + || khtml_puts(html, "scooper is AGPLv3+") + || khtml_closeelem(html, 2) + || khtml_putc(html, ' ') + || khtml_elem(html, KELEM_SPAN) + || khtml_attr(html, KELEM_A, KATTR_HREF, SyntaxURL, KATTR__MAX) + || khtml_puts(html, "Search syntax") + || khtml_closeelem(html, 1) + || khtml_putc(html, ' ') + || khtml_attr(html, KELEM_SPAN, KATTR_TITLE, Columns, KATTR__MAX) + || khtml_puts(html, "Columns") + || khtml_closeto(html, 0); } diff --git a/networks.c b/networks.c index c92da21..af3ad80 100644 --- a/networks.c +++ b/networks.c @@ -45,8 +45,6 @@ const char *NetworksQuery = SQL( ); enum kcgi_err networksPage(struct kreq *req) { - struct Scope scope = {0}; - enum kcgi_err error = 0 || httpHead(req, KHTTP_200, KMIME_TEXT_HTML) || khttp_body(req); @@ -56,7 +54,7 @@ enum kcgi_err networksPage(struct kreq *req) { error = error || khtml_open(&html, req, 0) || htmlHead(&html, "Litterbox") - || htmlNav(&html, scope); + || htmlNav(&html, req); if (error) return error; sqlite3_reset(stmt.networks); diff --git a/search.c b/search.c index c284732..67ce79c 100644 --- a/search.c +++ b/search.c @@ -44,15 +44,15 @@ const char *SearchQuery = SQL( LIMIT :offset, :limit; ); -static char *offsetURL(struct Scope scope, int64_t offset) { - const char *networkKey = (scope.network ? Keys[Network].name : NULL); - const char *contextKey = (scope.context ? Keys[Context].name : NULL); +static char *offsetURL( + const char *network, const char *context, const char *query, int64_t offset +) { char *url = khttp_urlpartx( NULL, NULL, Pages[Search], - Keys[Query].name, KATTRX_STRING, scope.query, + Keys[Query].name, KATTRX_STRING, query, Keys[Offset].name, KATTRX_INT, offset, - networkKey, KATTRX_STRING, scope.network, - contextKey, KATTRX_STRING, scope.context, + (network ? Keys[Network].name : NULL), KATTRX_STRING, network, + (context ? Keys[Context].name : NULL), KATTRX_STRING, context, NULL ); if (!url) err(EX_OSERR, "khttp_urlpartx"); @@ -60,12 +60,15 @@ static char *offsetURL(struct Scope scope, int64_t offset) { } 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); - + if (!req->fieldmap[Query]) return httpFail(req, KHTTP_400); + const char *query = req->fieldmap[Query]->parsed.s; + const char *network = NULL; + const char *context = NULL; int64_t offset = 0; + if (req->fieldmap[Network]) network = req->fieldmap[Network]->parsed.s; + if (req->fieldmap[Context]) context = req->fieldmap[Context]->parsed.s; if (req->fieldmap[Offset]) offset = req->fieldmap[Offset]->parsed.i; + if (context && !network) return httpFail(req, KHTTP_400); enum kcgi_err error = 0 || httpHead(req, KHTTP_200, KMIME_TEXT_HTML) @@ -75,14 +78,14 @@ enum kcgi_err searchPage(struct kreq *req) { struct khtmlreq html; error = error || khtml_open(&html, req, 0) - || htmlHead(&html, scope.query) - || htmlNav(&html, scope) + || htmlHead(&html, query) + || htmlNav(&html, req) || khtml_elem(&html, KELEM_TABLE); if (error) return error; if (offset) { int64_t prev = offset - eventsLimit; - char *href = offsetURL(scope, (prev > 0 ? prev : 0)); + char *href = offsetURL(network, context, query, (prev > 0 ? prev : 0)); error = 0 || khtml_attr(&html, KELEM_TR, KATTR_CLASS, "page", KATTR__MAX) || khtml_attr(&html, KELEM_TH, KATTR_COLSPAN, "5", KATTR__MAX) @@ -95,9 +98,9 @@ enum kcgi_err searchPage(struct kreq *req) { sqlite3_reset(stmt.search); dbBindText(stmt.search, ":highlight", "\26"); - dbBindText(stmt.search, ":network", scope.network); - dbBindText(stmt.search, ":context", scope.context); - dbBindText(stmt.search, ":query", scope.query); + dbBindText(stmt.search, ":network", network); + dbBindText(stmt.search, ":context", context); + dbBindText(stmt.search, ":query", query); dbBindInt(stmt.search, ":public", contextsPublic); dbBindInt(stmt.search, ":limit", eventsLimit); dbBindInt(stmt.search, ":offset", offset); @@ -117,7 +120,7 @@ enum kcgi_err searchPage(struct kreq *req) { event.host = sqlite3_column_text(stmt.search, i++); event.target = sqlite3_column_text(stmt.search, i++); event.message = sqlite3_column_text(stmt.search, i++); - error = htmlEvent(&html, scope, event); + error = htmlEvent(&html, req, &event); if (error) return error; } if (result != SQLITE_DONE) { @@ -130,7 +133,7 @@ enum kcgi_err searchPage(struct kreq *req) { } if (rows == eventsLimit) { - char *href = offsetURL(scope, offset + eventsLimit); + char *href = offsetURL(network, context, query, offset + eventsLimit); error = 0 || khtml_attr(&html, KELEM_TR, KATTR_CLASS, "page", KATTR__MAX) || khtml_attr(&html, KELEM_TH, KATTR_COLSPAN, "5", KATTR__MAX) diff --git a/server.h b/server.h index 1177871..20d1803 100644 --- a/server.h +++ b/server.h @@ -55,11 +55,11 @@ extern const char *Pages[PagesLen]; X(Context, "context", kvalid_stringne) \ X(After, "after", kvalid_stringne) \ X(Before, "before", kvalid_stringne) \ - X(Tidy, "tidy", kvalid_stringne) \ + X(Tidy, "tidy", kvalid_int) \ X(Query, "query", kvalid_stringne) \ X(Offset, "offset", kvalid_int) -enum { +enum Key { #define X(key, name, valid) key, ENUM_KEYS #undef X @@ -67,20 +67,6 @@ enum { }; extern const struct kvalid Keys[KeysLen]; -struct Scope { - const char *network; - const char *context; - const char *query; -}; - -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; @@ -181,13 +167,13 @@ struct Event { 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 htmlHidden(struct khtmlreq *html, struct kreq *req, enum Key key); +enum kcgi_err htmlNav(struct khtmlreq *html, struct kreq *req); enum kcgi_err htmlIRC(struct khtmlreq *html, const char *str); enum kcgi_err htmlEvent( - struct khtmlreq *html, struct Scope scope, struct Event event + struct khtmlreq *html, struct kreq *req, struct Event *event ); +enum kcgi_err htmlFooter(struct khtmlreq *html); static inline enum kcgi_err httpHead(struct kreq *req, enum khttp http, enum kmime mime) { |