From bd14377904e8ca9de03af284501cb3f0d8ecc591 Mon Sep 17 00:00:00 2001 From: Jason Volk Date: Sun, 25 Feb 2018 23:24:12 -0800 Subject: [PATCH] ircd::m: Captain hook. --- include/ircd/m/hook.h | 91 +++++++++++++++ include/ircd/m/m.h | 1 + ircd/Makefile.am | 2 +- ircd/m/m.cc | 257 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 350 insertions(+), 1 deletion(-) create mode 100644 include/ircd/m/hook.h diff --git a/include/ircd/m/hook.h b/include/ircd/m/hook.h new file mode 100644 index 000000000..d818cd6b6 --- /dev/null +++ b/include/ircd/m/hook.h @@ -0,0 +1,91 @@ +// Matrix Construct +// +// Copyright (C) Matrix Construct Developers, Authors & Contributors +// Copyright (C) 2016-2018 Jason Volk +// +// 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. + +#pragma once +#define HAVE_IRCD_M_HOOK_H + +namespace ircd::m +{ + struct hook; +} + +struct ircd::m::hook +{ + struct site; + struct list; + + IRCD_EXCEPTION(ircd::error, error) + + struct list static list; + + json::strung _feature; + json::object feature; + std::function function; + bool registered; + + string_view site_name() const; + + public: + hook(const json::members &, decltype(function)); + hook(hook &&) = delete; + hook(const hook &) = delete; + virtual ~hook() noexcept; +}; + +/// The hook::site is the call-site for a hook. Each hook site is named +/// and registers itself with the master extern hook::list. Each hook +/// then registers itself with a hook::site. The site contains internal +/// state to manage the efficient calling of the participating hooks. +struct ircd::m::hook::site +{ + json::strung _feature; + json::object feature; + bool registered; + + string_view name() const; + + std::multimap origin; + std::multimap room_id; + std::multimap sender; + std::multimap state_key; + std::multimap type; + + friend class hook; + bool add(hook &); + bool del(hook &); + + public: + void operator()(const event &); + + site(const json::members &); + site(site &&) = delete; + site(const site &) = delete; + ~site() noexcept; +}; + +/// The hook list is the registry of all of the hook sites. This is a singleton +/// class with an extern instance. Each hook::site will register itself here +/// by human readable name. +struct ircd::m::hook::list +:std::map +{ + friend class site; + bool add(site &); + bool del(site &); + + friend class hook; + bool add(hook &); + bool del(hook &); + + public: + list() = default; + list(list &&) = delete; + list(const list &) = delete; +}; diff --git a/include/ircd/m/m.h b/include/ircd/m/m.h index 1617a3bd8..b619411dc 100644 --- a/include/ircd/m/m.h +++ b/include/ircd/m/m.h @@ -57,6 +57,7 @@ namespace ircd #include "vm.h" #include "keys.h" #include "txn.h" +#include "hook.h" struct ircd::m::init { diff --git a/ircd/Makefile.am b/ircd/Makefile.am index 39459140d..d42943b60 100644 --- a/ircd/Makefile.am +++ b/ircd/Makefile.am @@ -107,9 +107,9 @@ libircd_la_SOURCES = \ m/filter.cc \ m/request.cc \ m/v1.cc \ - m/vm.cc \ m/keys.cc \ m/m.cc \ + m/vm.cc \ ircd.cc \ ### diff --git a/ircd/m/m.cc b/ircd/m/m.cc index dbeff6ae9..be453f440 100644 --- a/ircd/m/m.cc +++ b/ircd/m/m.cc @@ -459,3 +459,260 @@ ircd::m::commit(const room &room, return function(room, event, contents); } + +/////////////////////////////////////////////////////////////////////////////// +// +// m/hook.h +// + +ircd::m::hook::hook(const json::members &members, + decltype(function) function) +try +:_feature +{ + members +} +,feature +{ + _feature +} +,function +{ + std::move(function) +} +,registered +{ + list.add(*this) +} +{ +} +catch(...) +{ + if(registered) + list.del(*this); +} + +ircd::m::hook::~hook() +noexcept +{ + if(registered) + list.del(*this); +} + +ircd::string_view +ircd::m::hook::site_name() +const try +{ + return unquote(feature.at("_site")); +} +catch(const std::out_of_range &e) +{ + throw assertive + { + "Hook %p must name a '_site' to register with.", this + }; +} + +// +// hook::site +// + +ircd::m::hook::site::site(const json::members &members) +try +:_feature +{ + members +} +,feature +{ + _feature +} +,registered +{ + list.add(*this) +} +{ +} +catch(...) +{ + if(registered) + list.del(*this); +} + +ircd::m::hook::site::~site() +noexcept +{ + if(registered) + list.del(*this); +} + +void +ircd::m::hook::site::operator()(const event &event) +{ + std::set matching; //TODO: allocator + const auto match{[&matching] + (auto &map, const string_view &key) + { + auto pit{map.equal_range(key)}; + for(; pit.first != pit.second; ++pit.first) + matching.emplace(pit.first->second); + }}; + + match(origin, at<"origin"_>(event)); + match(room_id, at<"room_id"_>(event)); + match(sender, at<"sender"_>(event)); + match(type, at<"type"_>(event)); + + if(json::get<"state_key"_>(event)) + match(state_key, at<"state_key"_>(event)); + + for(const auto &hook : matching) + hook->function(event); +} + +bool +ircd::m::hook::site::add(hook &hook) +{ + // Note that m::event property names are first class members of + // the hook feature which is why our property names use _underscore. + const m::event feature + { + hook.feature + }; + + if(json::get<"origin"_>(feature)) + origin.emplace(at<"origin"_>(feature), &hook); + + if(json::get<"room_id"_>(feature)) + room_id.emplace(at<"room_id"_>(feature), &hook); + + if(json::get<"sender"_>(feature)) + sender.emplace(at<"sender"_>(feature), &hook); + + if(json::get<"state_key"_>(feature)) + state_key.emplace(at<"state_key"_>(feature), &hook); + + if(json::get<"type"_>(feature)) + type.emplace(at<"type"_>(feature), &hook); + + return true; +} + +bool +ircd::m::hook::site::del(hook &hook) +{ + const m::event feature + { + hook.feature + }; + + const auto unmap{[&hook] + (auto &map, const string_view &key) + { + auto pit{map.equal_range(key)}; + for(; pit.first != pit.second; ++pit.first) + if(pit.first->second == &hook) + return map.erase(pit.first); + + assert(0); + }}; + + if(json::get<"origin"_>(feature)) + unmap(origin, at<"origin"_>(feature)); + + if(json::get<"room_id"_>(feature)) + unmap(room_id, at<"room_id"_>(feature)); + + if(json::get<"sender"_>(feature)) + unmap(sender, at<"sender"_>(feature)); + + if(json::get<"state_key"_>(feature)) + unmap(state_key, at<"state_key"_>(feature)); + + if(json::get<"type"_>(feature)) + unmap(type, at<"type"_>(feature)); + + return true; +} + +ircd::string_view +ircd::m::hook::site::name() +const try +{ + return unquote(feature.at("name")); +} +catch(const std::out_of_range &e) +{ + throw assertive + { + "Hook site %p requires a name", this + }; +} + +// +// hook::list +// + +decltype(ircd::m::hook::list) +ircd::m::hook::list +{}; + +bool +ircd::m::hook::list::del(hook &hook) +try +{ + const auto site(at(hook.site_name())); + assert(site != nullptr); + return site->add(hook); +} +catch(const std::out_of_range &e) +{ + log::critical + { + "Tried to unregister hook(%p) from missing hook::site '%s'", + &hook, + hook.site_name() + }; + + assert(0); + return false; +} + +bool +ircd::m::hook::list::add(hook &hook) +try +{ + const auto site(at(hook.site_name())); + assert(site != nullptr); + return site->add(hook); +} +catch(const std::out_of_range &e) +{ + throw error + { + "No hook::site named '%s' is registered...", hook.site_name() + }; +} + +bool +ircd::m::hook::list::del(site &site) +{ + return erase(site.name()); +} + +bool +ircd::m::hook::list::add(site &site) +{ + const auto iit + { + emplace(site.name(), &site) + }; + + if(unlikely(!iit.second)) + throw error + { + "Hook site name '%s' already in use", site.name() + }; + + return true; +}