0
0
Fork 0
mirror of https://github.com/matrix-construct/construct synced 2024-11-18 07:50:57 +01:00
construct/modules/client/search.cc

663 lines
13 KiB
C++

// Matrix Construct
//
// Copyright (C) Matrix Construct Developers, Authors & Contributors
// Copyright (C) 2016-2018 Jason Volk <jason@zemos.net>
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice is present in all copies. The
// full license for this software is available in the LICENSE file.
namespace ircd::m::search
{
static bool handle_result(result &, const query &);
static bool handle_content(result &, const query &, const json::object &);
static bool query_all_rooms(result &, const query &);
static bool query_room(result &, const query &, const room::id &);
static bool query_rooms(result &, const query &);
static void handle_room_events(client &, const resource::request &, const json::object &, json::stack::object &);
static resource::response search_post_handle(client &, const resource::request &);
extern conf::item<size_t> limit_override;
extern conf::item<bool> count_total;
extern resource::method search_post;
extern resource search_resource;
extern log::log log;
}
ircd::mapi::header
IRCD_MODULE
{
"Client 11.14 :Server Side Search"
};
decltype(ircd::m::search::log)
ircd::m::search::log
{
"m.search"
};
decltype(ircd::m::search::search_resource)
ircd::m::search::search_resource
{
"/_matrix/client/r0/search",
{
"(11.14.1) The search API allows clients to perform full text search"
" across events in all rooms that the user has been in, including"
" those that they have left. Only events that the user is allowed to"
" see will be searched, e.g. it won't include events in rooms that"
" happened after you left."
}
};
decltype(ircd::m::search::search_post)
ircd::m::search::search_post
{
search_resource, "POST", search_post_handle,
{
search_post.REQUIRES_AUTH |
search_post.RATE_LIMITED,
// Some queries can take a really long time, especially under
// development. We don't need the default request timer getting
// in the way for now.
60s,
}
};
decltype(ircd::m::search::count_total)
ircd::m::search::count_total
{
{ "name", "ircd.m.search.count.total" },
{ "default", false },
};
decltype(ircd::m::search::limit_override)
ircd::m::search::limit_override
{
{ "name", "ircd.m.search.limit.override" },
{ "default", 1L },
};
ircd::m::resource::response
ircd::m::search::search_post_handle(client &client,
const resource::request &request)
{
const json::object &search_categories
{
request["search_categories"]
};
resource::response::chunked response
{
client, http::OK
};
json::stack out
{
response.buf, response.flusher()
};
json::stack::object top
{
out
};
json::stack::object result_categories
{
top, "search_categories"
};
if(search_categories.has("room_events"))
{
json::stack::object room_events_result
{
result_categories, "room_events"
};
handle_room_events(client, request, search_categories, room_events_result);
}
return response;
}
void
ircd::m::search::handle_room_events(client &client,
const resource::request &request,
const json::object &search_categories,
json::stack::object &room_events_result)
try
{
const m::search::room_events room_events
{
search_categories["room_events"]
};
const string_view search_input
{
at<"search_term"_>(room_events)
};
const pair<string_view> kvs
{
split(search_input, " :")
};
const auto when{[&kvs]
(const std::initializer_list<string_view> &names, auto&& closure)
{
tokens(kvs.first, ' ', [&names, &closure]
(const string_view &kv)
{
const auto &[key, val]
{
split(kv, '=')
};
if(std::find(begin(names), end(names), key) == end(names))
return true;
closure(key, val);
return false;
});
}};
uint filter_keys {0};
m::room_event_filter room_event_filter
{
json::get<"filter"_>(room_events)
};
json::strung senders;
when({"sender", ""}, [&](const auto &key, const auto &val)
{
if(!valid(m::id::USER, val))
return;
senders = json::get<"senders"_>(room_event_filter);
senders = json::append(senders, val);
json::get<"senders"_>(room_event_filter) = senders;
filter_keys += 1;
});
json::strung not_senders;
when({"!sender", "!"}, [&](const auto &key, const auto &val)
{
if(!valid(m::id::USER, val))
return;
not_senders = json::get<"not_senders"_>(room_event_filter);
not_senders = json::append(not_senders, val);
json::get<"not_senders"_>(room_event_filter) = not_senders;
filter_keys += 1;
});
bool case_sensitive {false};
when({"case", "ci"}, [&](const auto &key, const auto &val)
{
case_sensitive = key == "case";
filter_keys += 0; // doesn't count; no case-insensitive wildcard.
});
// Override the limit to 1 to return a result and appease the user as
// quickly as possible. The client can call us again for more results.
size_t limit
{
limit_override?
size_t(limit_override):
size_t(json::get<"limit"_>(room_event_filter))
};
// Key command for the user to override the limit to workaround pagination
// bugs in riot which don't properly request next batch to fill viewport.
when({"limit"}, [&](const auto &key, const auto &val)
{
// limit=-1 is unlimited
// limit=0 is unchanged
// limit=n is overriding
limit = lex_cast<ssize_t>(val)?: limit;
filter_keys += 0; // doesn't count for wildcard.
});
// The limit value is limited to the viewport size so we can force
// pagination rather than search the whole room with one giant result.
limit = std::clamp
(
limit,
1UL,
size_t(m::room::events::viewport_size)
);
const string_view search_term
{
// Any string after the separator is the search term.
kvs.second?
kvs.second:
// When only filter keys are given the search term is wildcard.
filter_keys?
string_view{}:
// No filter keys or separator; it is the search term.
kvs.first
};
const json::object &event_context
{
json::get<"event_context"_>(room_events)
};
// Spec sez default is 5. Reference client does not make any use of
// result context if provided.
const ushort context_default
{
0
};
const search::query query
{
.user_id = request.user_id,
.batch = request.query.get<size_t>("next_batch", 0UL),
.room_events = room_events,
.filter = room_event_filter,
.search_term = search_term,
.limit = limit,
.before_limit = event_context.get("before_limit", context_default),
.after_limit = event_context.get("after_limit", context_default),
.case_sensitive = case_sensitive,
};
log::logf
{
log, log::DEBUG,
"Query '%s' by %s batch:%ld order_by:%s inc_state:%b rooms:%zu limit:%zu filter:%s",
query.search_term,
string_view{query.user_id},
query.batch,
json::get<"order_by"_>(query.room_events),
json::get<"include_state"_>(query.room_events),
json::get<"rooms"_>(query.filter).size(),
query.limit,
string_view{room_event_filter.source},
};
search::result result
{
room_events_result.s
};
const bool finished
{
query_rooms(result, query)
};
// Spec sez this is total results, but riot doesn't use it. Counting total
// results is very expensive right now, so we'll just report the count we
// have for now...
json::stack::member
{
room_events_result, "count", json::value
{
long(result.count + !finished)
}
};
//TODO: XXX
json::stack::array
{
room_events_result, "highlights"
};
//TODO: XXX
json::stack::object
{
room_events_result, "state"
};
if(!finished)
json::stack::member
{
room_events_result, "next_batch", json::value
{
lex_cast(result.skipped + result.checked), json::STRING
}
};
char tmbuf[48];
log::logf
{
log, log::DEBUG,
"Result '%s' by %s batch[%ld -> %ld] count:%lu append:%lu match:%lu check:%lu skip:%lu in %s",
query.search_term,
string_view{query.user_id},
query.batch,
result.event_idx,
result.count,
result.appends,
result.matched,
result.checked,
result.skipped,
result.elapsed.pretty(tmbuf),
};
}
catch(const ctx::interrupted &)
{
throw;
}
catch(const std::exception &e)
{
log::error
{
log, "search :%s",
e.what()
};
throw;
}
bool
ircd::m::search::query_rooms(result &result,
const query &query)
{
const json::array rooms
{
json::get<"rooms"_>(query.filter)
};
json::stack::array results
{
*result.out, "results"
};
if(rooms.empty())
return query_all_rooms(result, query);
for(const json::string room_id : rooms)
if(!query_room(result, query, room_id))
return false;
return true;
}
bool
ircd::m::search::query_room(result &result,
const query &query,
const room::id &room_id)
{
const m::room room
{
room_id
};
if(!visible(room, query.user_id))
throw m::ACCESS_DENIED
{
"You are not permitted to view %s",
string_view{room_id},
};
const m::room::iterate content
{
room, "content"
};
return content.for_each([&result, &query]
(const json::object &content, const auto &depth, const auto &event_idx)
{
result.event_idx = event_idx;
return handle_content(result, query, content);
});
}
bool
ircd::m::search::query_all_rooms(result &result,
const query &query)
{
if(!is_oper(query.user_id))
throw m::ACCESS_DENIED
{
"You are not an operator."
};
return m::events::content::for_each([&result, &query]
(const auto &event_idx, const json::object &content)
{
result.event_idx = event_idx;
return handle_content(result, query, content);
});
}
bool
ircd::m::search::handle_content(result &result,
const query &query,
const json::object &content)
try
{
if(result.skipped < query.batch)
{
++result.skipped;
return true;
}
const json::string body
{
content["body"]
};
const bool match_term
{
false
|| (query.case_sensitive && has(body, query.search_term))
|| (!query.case_sensitive && ihas(body, query.search_term))
};
const bool match
{
true
&& (!query.search_term || match_term)
&& m::match(query.filter, result.event_idx)
};
const bool handled
{
match && handle_result(result, query)
};
result.checked += 1;
result.matched += match;
result.count += handled;
return result.count < query.limit;
}
catch(const ctx::interrupted &e)
{
log::dwarning
{
log, "Query handling '%s' by '%s' event_idx:%lu :%s",
query.search_term,
string_view{query.user_id},
result.event_idx,
e.what(),
};
throw;
}
catch(const std::system_error &e)
{
log::derror
{
log, "Query handling for '%s' by '%s' event_idx:%lu :%s",
query.search_term,
string_view{query.user_id},
result.event_idx,
e.what(),
};
throw;
}
catch(const std::exception &e)
{
log::error
{
log, "Query handling for '%s' by '%s' event_idx:%lu :%s",
query.search_term,
string_view{query.user_id},
result.event_idx,
e.what(),
};
return true;
}
bool
ircd::m::search::handle_result(result &result,
const query &query)
try
{
const m::event_filter event_filter
{
query.filter
};
const m::event::fetch event
{
result.event_idx
};
assert(result.out);
json::stack::checkpoint cp
{
*result.out, false
};
json::stack::object object
{
*result.out
};
json::stack::member
{
object, "rank", json::value(result.rank)
};
bool ret{false};
{
json::stack::object result_event
{
object, "result"
};
ret = event::append
{
result_event, event,
{
.event_idx = &result.event_idx,
.user_id = &query.user_id,
.event_filter = &event_filter,
.query_prev_state = false,
.query_visible = true,
},
};
result.appends += ret;
cp.committing(ret);
}
if(!query.before_limit && !query.after_limit)
return ret;
const m::room room
{
json::get<"room_id"_>(event)
};
m::room::events it
{
room
};
json::stack::object result_context
{
object, "context"
};
size_t before(0);
if(likely(!it.seek(result.event_idx)))
{
json::stack::array events_before
{
result_context, "events_before"
};
for(--it; it && before < query.before_limit; ++before, --it)
{
const event::idx event_idx
{
it.event_idx()
};
result.appends += event::append
{
events_before, event,
{
.event_idx = &event_idx,
.user_id = &query.user_id,
.event_filter = &event_filter,
.query_prev_state = false,
.query_visible = true,
},
};
}
}
size_t after(0);
if(likely(it.seek(result.event_idx)))
{
json::stack::array events_after
{
result_context, "events_after"
};
for(++it; it && after < query.after_limit; ++after, ++it)
{
const event::idx event_idx
{
it.event_idx()
};
result.appends += event::append
{
events_after, event,
{
.event_idx = &event_idx,
.user_id = &query.user_id,
.event_filter = &event_filter,
.query_prev_state = false,
.query_visible = true,
},
};
}
}
return ret;
}
catch(const ctx::interrupted &)
{
throw;
}
catch(const std::system_error &e)
{
throw;
}
catch(const std::exception &e)
{
log::error
{
log, "Result handling for '%s' by '%s' event_idx:%lu :%s",
query.search_term,
string_view{query.user_id},
result.event_idx,
e.what(),
};
return false;
}