search: add nick keyword search

- search messages by author with `from:nick`
- add section to Help.vue
- should be easy to adapt this into global keyword-based search

https://github.com/thelounge/thelounge/issues/4209
This commit is contained in:
Carter Konz 2022-05-31 19:14:24 -05:00
parent 2e3d9a6265
commit 27d69b00ff
3 changed files with 128 additions and 2 deletions

View file

@ -825,6 +825,23 @@
<p>Retrieve information about the given user on the current network.</p>
</div>
</div>
<h2>Search</h2>
<p>
You can search messages simply by clicking the magnifying glass icon on the right
side of a channel's titlebar and entering a search term. Here are some additional
keywords you can use to fine-tune your search.
</p>
<div class="help-item">
<div class="subject">
<code>from:<em>nick</em></code>
</div>
<div class="description">
<p>Search messages by author.</p>
</div>
</div>
</div>
</div>
</template>

View file

@ -208,9 +208,41 @@ class MessageStorage {
// Using the '@' character to escape '%' and '_' in patterns.
const escapedSearchTerm = query.searchTerm.replace(/([%_@])/g, "@$1");
// Additional filtering:
// - "from:nick" to find messages sent by a given nick
const keywords = ["from"];
const joinedKeywords = keywords.map((kw) => kw + ":").join("|");
const filterExp = RegExp(`((${joinedKeywords})(\\S+))+`, "g");
let filters;
const filterMatch = escapedSearchTerm.match(filterExp);
if (filterMatch && filterMatch.length) {
filters = filterMatch.reduce((params, filter) => {
const [key, value] = filter.split(":");
params[key] = value;
return params;
}, {});
}
// strip "key:value" filter pairs out of search string
const mainSearchTerm = escapedSearchTerm.replace(filterExp, "").trim();
let select =
'SELECT msg, type, time, network, channel FROM messages WHERE type = "message" AND json_extract(msg, "$.text") LIKE ? ESCAPE \'@\'';
const params = [`%${escapedSearchTerm}%`];
'SELECT msg, type, time, network, channel FROM messages WHERE type = "message"';
const params = [];
if (mainSearchTerm.length) {
select += " AND json_extract(msg, \"$.text\") LIKE ? ESCAPE '@'";
params.push(`%${mainSearchTerm}%`);
}
if (filters && Object.keys(filters).length) {
if (filters.from) {
select += ' AND json_extract(msg, "$.from.nick") LIKE ?';
params.push(filters.from);
}
}
if (query.networkUuid) {
select += " AND network = ? ";

View file

@ -243,6 +243,83 @@ describe("SQLite Message Storage", function () {
}
});
it("should search messages with keyword queries", function () {
function assertResults(query, expected) {
return store
.search({
searchTerm: query,
networkUuid: "this-is-a-network-guid3",
})
.then((messages) => {
expect(messages.results.map((i) => i.text)).to.deep.equal(expected);
});
}
const originalMaxHistory = Config.values.maxHistory;
try {
Config.values.maxHistory = 3;
store.index(
{uuid: "this-is-a-network-guid3"},
{name: "#channel"},
new Msg({
time: 123456793,
text: "my first message",
from: {
mode: "",
nick: "thelounge007",
},
})
);
store.index(
{uuid: "this-is-a-network-guid3"},
{name: "#channel"},
new Msg({
time: 123456794,
text: "no, my first message!",
from: {
mode: "",
nick: "thelounge008",
},
})
);
store.index(
{uuid: "this-is-a-network-guid3"},
{name: "#channel"},
new Msg({
time: 123456795,
text: "another unrelated message",
from: {
mode: "",
nick: "thelounge007",
},
})
);
return (
store
.getMessages({uuid: "this-is-a-network-guid3"}, {name: "#channel"})
// .getMessages() waits for store.index() transactions to commit
.then(() =>
assertResults("from:thelounge007", [
"my first message",
"another unrelated message",
])
)
.then(() => assertResults("from:thelounge007 first", ["my first message"]))
.then(() =>
assertResults("first", ["my first message", "no, my first message!"])
)
.then(() => assertResults("from:nobody", []))
);
} finally {
Config.values.maxHistory = originalMaxHistory;
}
});
it("should close database", function (done) {
store.close((err) => {
expect(err).to.be.null;