From 07b73fd69da1a43270596003eb163eb5c5cfdcf9 Mon Sep 17 00:00:00 2001 From: "C. McEnroe" Date: Fri, 27 Dec 2019 17:07:29 -0500 Subject: Add search query interface --- litterbox.1 | 42 ++++++++++++++++++++++++++++-- litterbox.c | 85 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 124 insertions(+), 3 deletions(-) diff --git a/litterbox.1 b/litterbox.1 index 98766e2..3f0446a 100644 --- a/litterbox.1 +++ b/litterbox.1 @@ -1,4 +1,4 @@ -.Dd December 17, 2019 +.Dd December 27, 2019 .Dt LITTERBOX 1 .Os . @@ -8,7 +8,7 @@ . .Sh SYNOPSIS .Nm -.Op Fl v +.Op Fl Qqv .Op Fl d Ar path .Op Fl h Ar host .Op Fl j Ar join @@ -32,6 +32,24 @@ which may be queried with The arguments are as follows: . .Bl -tag -width "-h host" +.It Fl Q +Enable public search query interface. +This allows anyone to perform searches +in private messages to +.Nm . +Search results are limited +to channels on the current network. +. +.Pp +The searchable columns are +.Li channel , +.Li nick , +.Li user , +.Li target , +.Li message . +For search query syntax, see +.Lk https://www.sqlite.org/fts5.html#full_text_query_syntax +. .It Fl d Ar path Set the path to the database file. The database must be initialized with @@ -65,6 +83,26 @@ Connect to .Ar port . The default port is 6697. . +.It Fl q +Enable private search query interface. +This allows search queries in private messages to +.Nm +from itself, +which is likely only useful when connected to +.Xr pounce 1 . +Search results are limited to the current network. +. +.Pp +The searchable columns are +.Li channel , +.Li query , +.Li nick , +.Li user , +.Li target , +.Li message . +For search query syntax, see +.Lk https://www.sqlite.org/fts5.html#full_text_query_syntax +. .It Fl u Ar user Set the username to .Ar user . diff --git a/litterbox.c b/litterbox.c index 57d30cb..8f0a0ba 100644 --- a/litterbox.c +++ b/litterbox.c @@ -99,6 +99,11 @@ static void require(const struct Message *msg, bool nick, size_t len) { } static const char *join; +static enum { + None, + Private, + Public, +} searchQuery; static char *self; static char *network; @@ -240,6 +245,75 @@ static void insertEvent( dbRun(stmt); } +static void querySearch(struct Message *msg) { + static sqlite3_stmt *stmt; + const char *sql = SQL( + WITH results AS ( + SELECT + contexts.name AS context, + date(events.time) || 'T' || time(events.time) || 'Z' AS time, + events.type, + names.nick, // TODO: names.user for coloring? + events.target, + highlight(search, 6, :bold, :bold) + FROM events + JOIN contexts ON contexts.context = events.context + JOIN names ON names.name = events.name + JOIN search ON search.rowid = events.event + WHERE contexts.network = :network + AND coalesce(contexts.query = :query, true) + AND search MATCH :search + ORDER BY events.time DESC, events.event DESC + LIMIT 10 + ) + SELECT * FROM results ORDER BY context, time; + ); + dbPersist(&stmt, sql); + dbBindText(stmt, ":bold", "\2"); + + dbBindText(stmt, ":network", network); + if (searchQuery == Public) { + dbBindInt(stmt, ":query", false); + } else { + dbBindNull(stmt, ":query"); + } + dbBindText(stmt, ":search", msg->params[1]); + + int result; + while (SQLITE_ROW == (result = sqlite3_step(stmt))) { + const char *context = (const char *)sqlite3_column_text(stmt, 0); + const char *time = (const char *)sqlite3_column_text(stmt, 1); + enum Type type = sqlite3_column_int(stmt, 2); + const char *nick = (const char *)sqlite3_column_text(stmt, 3); + const char *target = (const char *)sqlite3_column_text(stmt, 4); + const char *message = (const char *)sqlite3_column_text(stmt, 5); + if (!target) target = ""; + if (!message) message = ""; + + format("PRIVMSG %s :(%s) [%s] ", msg->nick, context, time); + switch (type) { + break; case Privmsg: format("<%s> %s\r\n", nick, message); + break; case Notice: format("-%s- %s\r\n", nick, message); + break; case Action: format("* %s %s\r\n", nick, message); + break; case Join: format("%s joined\r\n", nick); + break; case Part: format("%s parted: %s\r\n", nick, message); + break; case Quit: format("%s quit: %s\r\n", nick, message); + break; case Kick: { + format("%s kicked %s: %s\r\n", nick, target, message); + } + break; case Nick: { + format("%s changed nick to %s\r\n", nick, target); + } + break; case Topic: { + format("%s set the topic: %s\r\n", nick, message); + } + } + } + if (result != SQLITE_DONE) warnx("%s", sqlite3_errmsg(db)); + + sqlite3_reset(stmt); +} + static void handlePrivmsg(struct Message *msg) { require(msg, true, 2); @@ -256,6 +330,13 @@ static void handlePrivmsg(struct Message *msg) { type = Action; } + if (query && searchQuery && type == Privmsg) { + if (searchQuery == Public || !strcmp(msg->nick, msg->params[0])) { + querySearch(msg); + return; + } + } + insertContext(context, query); insertName(msg); insertEvent(msg, type, context, NULL, message); @@ -511,9 +592,10 @@ int main(int argc, char *argv[]) { const char *pass = NULL; int opt; - while (0 < (opt = getopt(argc, argv, "!d:h:ij:mn:p:u:vw:"))) { + while (0 < (opt = getopt(argc, argv, "!Qd:h:ij:mn:p:qu:vw:"))) { switch (opt) { break; case '!': insecure = true; + break; case 'Q': searchQuery = Public; break; case 'd': path = optarg; break; case 'h': host = optarg; break; case 'i': init = true; @@ -521,6 +603,7 @@ int main(int argc, char *argv[]) { break; case 'm': migrate = true; break; case 'n': nick = optarg; break; case 'p': port = optarg; + break; case 'q': searchQuery = Private; break; case 'u': user = optarg; break; case 'v': verbose = true; break; case 'w': pass = optarg; -- cgit 1.4.1