/* Copyright (C) 2020 C. McEnroe * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ #include #include #include #include #include #include #include #include #include #include #include #include "server.h" const char *htmlStylesheet = "stylesheet.css"; enum kcgi_err htmlHead(struct khtmlreq *html, const char *title) { return khtml_elem(html, KELEM_DOCTYPE) || khtml_attr(html, KELEM_META, KATTR_CHARSET, "utf-8", KATTR__MAX) || khtml_elem(html, KELEM_TITLE) || khtml_puts(html, title) || khtml_closeelem(html, 1) || khtml_attr( html, KELEM_META, KATTR_NAME, "viewport", KATTR_CONTENT, "width=device-width, initial-scale=1.0", KATTR__MAX ) || khtml_attr( html, KELEM_LINK, KATTR_REL, "stylesheet", KATTR_HREF, htmlStylesheet, KATTR__MAX ) || khtml_elem(html, KELEM_H1) || khtml_puts(html, 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 || khtml_elem(html, KELEM_NAV) || khtml_elem(html, KELEM_OL) || khtml_elem(html, KELEM_LI) || khtml_attr(html, KELEM_A, KATTR_HREF, Pages[Networks], KATTR__MAX) || khtml_puts(html, "Networks") || khtml_closeelem(html, 2); if (error) return error; if (scope.network) { char *href = khttp_urlpart( NULL, NULL, Pages[Contexts], Keys[Network].name, scope.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_closeelem(html, 2); if (error) return error; } if (scope.network && scope.context) { char *href = khttp_urlpart( NULL, NULL, Pages[Events], Keys[Network].name, scope.network, Keys[Context].name, scope.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_closeelem(html, 2); if (error) return error; } char label[256]; snprintf( label, sizeof(label), "Search%s%s", (scope.network ? " " : ""), (scope.context ? scope.context : scope.network ? scope.network : "") ); return 0 || khtml_closeelem(html, 1) || khtml_attr( html, KELEM_FORM, KATTR_METHOD, "get", KATTR_ACTION, Pages[Search], KATTR__MAX ) || khtml_attr( html, KELEM_INPUT, KATTR_TYPE, "search", KATTR_NAME, Keys[Query].name, KATTR_VALUE, (scope.query ? scope.query : ""), KATTR__MAX ) || htmlScopeFields(html, scope) || khtml_attr( html, KELEM_INPUT, KATTR_TYPE, "submit", KATTR_VALUE, label, KATTR__MAX ) || khtml_closeelem(html, 2); } 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 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); } static const char *timestamp(time_t time) { static char stamp[sizeof("0000-00-00 00:00:00")]; strftime(stamp, sizeof(stamp), "%F %T", gmtime(&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( NULL, NULL, Pages[Events], 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"); } char *href = NULL; asprintf(&href, "%s#%" PRId64, (base ? base : ""), event.event); if (!href) err(EX_OSERR, "asprintf"); if (base) free(base); 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__MAX ) || khtml_puts(html, timestamp(event.time)) || 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; char *href = khttp_urlpart( NULL, NULL, Pages[Search], Keys[Network].name, event.network, Keys[Query].name, scope.query, 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_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; } char *href = khttp_urlpart( NULL, NULL, Pages[Search], Keys[Network].name, event.network, Keys[Context].name, event.context, Keys[Query].name, scope.query, 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_closeelem(html, 2); free(href); return error; } static int hash(const char *str) { if (*str == '~') str++; uint32_t hash = 0; for (; *str; ++str) { hash = (hash << 5) | (hash >> 27); hash ^= *str; hash *= 0x27220A95; } 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) { char *mask = NULL; asprintf(&mask, "%s!%s@%s", event.nick, event.user, event.host); if (!mask) err(EX_OSERR, "asprintf"); 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_TITLE, mask, KATTR__MAX ); 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, ")"); } 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); return 0 || khtml_printf(html, "kicked %s (", event.target) || 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) { 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); break; default: { if (event.message) error = htmlIRC(html, event.message); } } return error || khtml_closeelem(html, 1); } static const char *Types[TypesLen] = { #define X(id, name) [id] = name, ENUM_TYPE #undef X }; enum kcgi_err htmlEvent(struct khtmlreq *html, struct Scope scope, 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_CLASS, KATTRX_STRING, type, KATTR__MAX ) || eventTime(html, event) || eventNetwork(html, scope, event) || eventContext(html, scope, 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; } 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); }