From c032c686f6cfa85ae13843a0521ad3e44ddd82f3 Mon Sep 17 00:00:00 2001 From: Jason Volk Date: Sun, 24 Sep 2017 18:05:42 -0700 Subject: [PATCH] Checkpoint matrix application basis. --- charybdis/console.cc | 224 +++++- include/ircd/m.h | 4 +- include/ircd/m/event.h | 48 +- include/ircd/m/events.h | 69 -- include/ircd/m/filter.h | 114 +++ include/ircd/m/id.h | 5 +- include/ircd/m/room.h | 93 ++- include/ircd/m/{timeline.h => user.h} | 34 +- ircd/matrix.cc | 972 ++++++++++++++++++-------- ircd/resource.cc | 9 +- modules/client/events.cc | 37 +- modules/client/login.cc | 108 ++- modules/client/logout.cc | 14 +- modules/client/publicrooms.cc | 5 +- modules/client/pushrules.cc | 5 +- modules/client/register.cc | 153 ++-- modules/client/rooms.cc | 177 ++++- modules/client/sync.cc | 108 ++- modules/client/user.cc | 115 ++- modules/client/voip/turnserver.cc | 5 +- modules/db/events.cc | 153 +++- 21 files changed, 1835 insertions(+), 617 deletions(-) delete mode 100644 include/ircd/m/events.h create mode 100644 include/ircd/m/filter.h rename include/ircd/m/{timeline.h => user.h} (67%) diff --git a/charybdis/console.cc b/charybdis/console.cc index 56996b3c6..271ef6ac8 100644 --- a/charybdis/console.cc +++ b/charybdis/console.cc @@ -248,15 +248,104 @@ try break; } + const auto args(tokens_after(line, " ", 0)); + const params token{args, " ", {"room_id"}}; + const auto room_id{token.at(0, "!ircd:cdc.z"s)}; + m::request request { - "GET", "_matrix/client/r0/events" + "GET", "_matrix/client/r0/events", {}, json::members + { + { "room_id", string_view{room_id} } + } }; - static char buf[1024]; + static char buf[65536]; ircd::parse::buffer pb{buf}; - const auto doc((*moi)(pb, request)); - std::cout << doc << std::endl; + const json::object response{(*moi)(pb, request)}; + const json::array chunk(response["chunk"]); + for(const string_view &chunk : chunk) + std::cout << chunk << std::endl; + + break; + } + + case hash("context"): + { + if(!moi) + { + std::cerr << "No current session" << std::endl; + break; + } + + const auto args(tokens_after(line, " ", 0)); + const params token{args, " ", {"room_id", "event_id"}}; + const auto &room_id{token.at(0)}; + const auto &event_id{token.at(1)}; + + char url[512]; const auto url_len + { + fmt::snprintf(url, sizeof(url), "_matrix/client/r0/rooms/%s/context/%s", + room_id, + event_id) + }; + + m::request request + { + "GET", string_view{url, size_t(url_len)}, {} + }; + + static char buf[65536]; + ircd::parse::buffer pb{buf}; + const json::object response{(*moi)(pb, request)}; + for(const auto &member : response) + std::cout << member.first << " " << member.second << std::endl; + + break; + } + + case hash("state"): + { + if(!moi) + { + std::cerr << "No current session" << std::endl; + break; + } + + const auto args(tokens_after(line, " ", 0)); + const params token{args, " ", {"room_id", "event_type", "state_key"}}; + const auto &room_id{token.at(0)}; + const auto event_type{token[1]}; + const auto state_key{token[2]}; + + char url[512]; const auto url_len + { + event_type && state_key? + fmt::snprintf(url, sizeof(url), "_matrix/client/r0/rooms/%s/state/%s/%s", + room_id, + event_type, + state_key): + + event_type? + fmt::snprintf(url, sizeof(url), "_matrix/client/r0/rooms/%s/state/%s", + room_id, + event_type): + + fmt::snprintf(url, sizeof(url), "_matrix/client/r0/rooms/%s/state", + room_id) + }; + + m::request request + { + "GET", string_view{url, size_t(url_len)}, {} + }; + + static char buf[65536]; + ircd::parse::buffer pb{buf}; + const json::array response{(*moi)(pb, request)}; + for(const auto &event : response) + std::cout << event << std::endl; + break; } @@ -336,7 +425,7 @@ try static char buf[4096]; ircd::parse::buffer pb{buf}; - const auto doc((*moi)(pb, request)); + const string_view doc((*moi)(pb, request)); std::cout << doc << std::endl; break; } @@ -350,7 +439,32 @@ try } const auto args(tokens_after(line, " ", 0)); - const params token{args, " ", {"username", "password"}}; + if(args.empty()) + { + m::request request + { + "GET", "_matrix/client/r0/login", {}, {} + }; + + static char buf[4096]; + ircd::parse::buffer pb{buf}; + const json::object doc((*moi)(pb, request)); + const json::array flows(doc.at("flows")); + + size_t i(0); + for(const auto &flow : flows) + std::cout << i++ << ": " << flow << std::endl; + + break; + } + + const params token + { + args, " ", + { + "username", "password" + } + }; m::request request { @@ -364,7 +478,7 @@ try static char buf[4096]; ircd::parse::buffer pb{buf}; - const auto doc((*moi)(pb, request)); + const json::object doc((*moi)(pb, request)); std::cout << doc << std::endl; moi->access_token = std::string(unquote(doc.at("access_token"))); break; @@ -475,6 +589,102 @@ try break; } */ + case hash("setfilter"): + { + if(!moi) + { + std::cerr << "No current session" << std::endl; + break; + } + + const auto args + { + tokens_after(line, " ", 0) + }; + + const auto user_id + { + token(args, " ", 0) + }; + + const json::object filter + { + tokens_after(args, " ", 0) + }; + + static char url[128]; const auto url_len + { + fmt::snprintf(url, sizeof(url), "_matrix/client/r0/user/%s/filter", user_id) + }; + + static char query[512]; const auto query_len + { + fmt::snprintf(query, sizeof(query), "%s=%s", + "access_token", + moi->access_token) + }; + + m::request request + { + "POST", url, query, filter + }; + + static char buf[4096]; + ircd::parse::buffer pb{buf}; + const json::object response{(*moi)(pb, request)}; + std::cout << string_view{response} << std::endl; + break; + } + + case hash("getfilter"): + { + if(!moi) + { + std::cerr << "No current session" << std::endl; + break; + } + + const auto args + { + tokens_after(line, " ", 0) + }; + + const auto user_id + { + token(args, " ", 0) + }; + + const auto filter_id + { + tokens_after(args, " ", 0) + }; + + static char url[128]; const auto url_len + { + fmt::snprintf(url, sizeof(url), "_matrix/client/r0/user/%s/filter/%s", + user_id, + filter_id) + }; + + static char query[512]; const auto query_len + { + fmt::snprintf(query, sizeof(query), "%s=%s", + "access_token", + moi->access_token) + }; + + m::request request + { + "GET", url, query, {} + }; + + static char buf[4096]; + ircd::parse::buffer pb{buf}; + const json::object response{(*moi)(pb, request)}; + std::cout << string_view{response} << std::endl; + break; + } + default: std::cerr << "Bad command or filename" << std::endl; } diff --git a/include/ircd/m.h b/include/ircd/m.h index 8666671f3..a7b7725d2 100644 --- a/include/ircd/m.h +++ b/include/ircd/m.h @@ -29,9 +29,9 @@ #include "m/error.h" #include "m/id.h" #include "m/event.h" -#include "m/events.h" #include "m/room.h" -#include "m/timeline.h" +#include "m/user.h" +#include "m/filter.h" #include "m/request.h" #include "m/session.h" diff --git a/include/ircd/m/event.h b/include/ircd/m/event.h index c3c4a5112..b0b90a9c4 100644 --- a/include/ircd/m/event.h +++ b/include/ircd/m/event.h @@ -50,33 +50,47 @@ namespace ircd::m namespace ircd::m::name { - extern constexpr const char *const content {"content"}; - extern constexpr const char *const event_id {"event_id"}; - extern constexpr const char *const origin_server_ts {"origin_server_ts"}; - extern constexpr const char *const prev_ids {"prev_ids"}; - extern constexpr const char *const room_id {"room_id"}; - extern constexpr const char *const sender {"sender"}; - extern constexpr const char *const signatures {"signatures"}; - extern constexpr const char *const state_key {"state_key"}; - extern constexpr const char *const type {"type"}; - extern constexpr const char *const unsigned_ {"unsigned"}; + constexpr const char *const content {"content"}; + constexpr const char *const event_id {"event_id"}; + constexpr const char *const origin_server_ts {"origin_server_ts"}; + constexpr const char *const prev_ids {"prev_ids"}; + constexpr const char *const room_id {"room_id"}; + constexpr const char *const sender {"sender"}; + constexpr const char *const signatures {"signatures"}; + constexpr const char *const state_key {"state_key"}; + constexpr const char *const type {"type"}; + constexpr const char *const unsigned_ {"unsigned"}; } struct ircd::m::event :json::tuple < - json::property, json::property, + json::property, json::property, - json::property, - json::property, - json::property, - json::property, json::property, - json::property, - json::property + json::property, + json::property, + json::property, + json::property, + json::property, + json::property > { + using id = m::id::event; + + static database *events; + using cursor = db::cursor; + using const_iterator = cursor::const_iterator; + using iterator = const_iterator; + using where = cursor::where_type; + + // Queue of contexts waiting to see the next inserted event + static ctx::view inserted; + + static const_iterator find(const id &); + static void insert(json::iov &); + using super_type::tuple; using super_type::operator=; }; diff --git a/include/ircd/m/events.h b/include/ircd/m/events.h deleted file mode 100644 index cc88c77e1..000000000 --- a/include/ircd/m/events.h +++ /dev/null @@ -1,69 +0,0 @@ -/* - * charybdis: 21st Century IRC++d - * - * Copyright (C) 2016 Charybdis Development Team - * Copyright (C) 2016 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. - * - * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR - * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, - * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR - * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) - * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, - * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING - * IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - * - */ - -#pragma once -#define HAVE_IRCD_M_EVENTS_H - -namespace ircd::m::events -{ - IRCD_M_EXCEPTION(m::error, INVALID_TRANSITION, http::BAD_REQUEST) - - struct transition; - - using transition_list = std::list; - extern transition_list transitions; - - extern database *events; - using cursor = db::cursor; - using const_iterator = cursor::const_iterator; - using iterator = const_iterator; - using where = cursor::where_type; - - const_iterator find(const id &); - const_iterator begin(); - const_iterator end(); - - void insert(const event &); - void insert(json::iov &); -} - -struct ircd::m::events::transition -{ - struct method - { - std::function valid; - std::function effects; - }; - - const char *name; - struct method method; - unique_const_iterator it; - - virtual bool valid(const event &event) const; - virtual void effects(const event &e); - - transition(const char *const &name, struct method method); - transition(const char *const &name); - virtual ~transition() noexcept; -}; diff --git a/include/ircd/m/filter.h b/include/ircd/m/filter.h new file mode 100644 index 000000000..b80c342d6 --- /dev/null +++ b/include/ircd/m/filter.h @@ -0,0 +1,114 @@ +/* + * charybdis: 21st Century IRC++d + * + * Copyright (C) 2016 Charybdis Development Team + * Copyright (C) 2016 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. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, + * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING + * IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + */ + +#pragma once +#define HAVE_IRCD_M_FILTER_H + +namespace ircd::m +{ + struct filter; + struct room_filter; + struct event_filter; + struct room_event_filter; +} + +namespace ircd::m::name +{ + constexpr const char *const event_fields {"event_fields"}; + constexpr const char *const event_format {"event_format"}; + constexpr const char *const account_data {"account_data"}; + constexpr const char *const presence {"presence"}; + constexpr const char *const room {"room"}; + constexpr const char *const timeline {"timeline"}; + constexpr const char *const ephemeral {"ephemeral"}; + constexpr const char *const state {"state"}; + constexpr const char *const rooms {"rooms"}; + constexpr const char *const not_rooms {"not_rooms"}; + constexpr const char *const include_leave {"include_leave"}; + constexpr const char *const types {"types"}; + constexpr const char *const not_types {"not_types"}; + constexpr const char *const senders {"senders"}; + constexpr const char *const not_senders {"not_senders"}; + constexpr const char *const limit {"limit"}; +} + +struct ircd::m::event_filter +:json::tuple +< + json::property, + json::property, + json::property, + json::property, + json::property +> +{ + using super_type::tuple; + using super_type::operator=; +}; + +struct ircd::m::room_event_filter +:json::tuple +< + json::property, + json::property, + json::property, + json::property, + json::property, + json::property, + json::property +> +{ + using super_type::tuple; + using super_type::operator=; +}; + +struct ircd::m::room_filter +:json::tuple +< + json::property, + json::property, + json::property, + json::property, + json::property, + json::property, + json::property +> +{ + using super_type::tuple; + using super_type::operator=; +}; + +struct ircd::m::filter +:json::tuple +< + json::property, + json::property, + json::property, + json::property, + json::property +> +{ + using super_type::tuple; + using super_type::operator=; +}; diff --git a/include/ircd/m/id.h b/include/ircd/m/id.h index aa2d41e7c..3b89fbc43 100644 --- a/include/ircd/m/id.h +++ b/include/ircd/m/id.h @@ -100,7 +100,10 @@ template b; diff --git a/include/ircd/m/room.h b/include/ircd/m/room.h index f07604c01..1a8efa2f3 100644 --- a/include/ircd/m/room.h +++ b/include/ircd/m/room.h @@ -34,42 +34,85 @@ namespace ircd::m struct room; } -namespace ircd::m::rooms -{ -} - struct ircd::m::room { struct alias; + struct events; + struct state; - string_view room_id; + using id = m::id::room; - using event_closure = std::function; - void for_each(const events::where &, const event_closure &) const; - void for_each(const event_closure &) const; + id room_id; - using event_closure_bool = std::function; - size_t count(const events::where &, const event_closure_bool &) const; - size_t count(const events::where &) const; - bool any(const events::where &, const event_closure_bool &) const; - bool any(const events::where &) const; - - bool is_member(const m::id::user &, const string_view &membership = "join"); + void send(json::iov &event); + void send(const json::members &event); bool is_member(const m::id::user &, const string_view &membership = "join"); void membership(const m::id::user &, json::iov &content); + void leave(const m::id::user &, json::iov &content); void join(const m::id::user &, json::iov &content); - room(const id::room &room_id); - room(const id::alias &alias); + room(const id &room_id) + :room_id{room_id} + {} + + room(const id::alias &alias) + :room_id{} + {} }; -inline -ircd::m::room::room(const id::room &room_id) -:room_id{room_id} -{} +struct ircd::m::room::events +{ + id room_id; -inline -ircd::m::room::room(const id::alias &alias) -:room_id{} -{} + using event_closure = std::function; + using event_closure_bool = std::function; + + bool query(const event::where &, const event_closure_bool &) const; + bool rquery(const event::where &, const event_closure_bool &) const; + void for_each(const event::where &, const event_closure &) const; + void rfor_each(const event::where &, const event_closure &) const; + size_t count(const event::where &, const event_closure_bool &) const; + bool any(const event::where &, const event_closure_bool &) const; + + bool query(const event_closure_bool &) const; + bool rquery(const event_closure_bool &) const; + void for_each(const event_closure &) const; + void rfor_each(const event_closure &) const; + size_t count(const event::where &) const; + bool any(const event::where &) const; + + events(const id &room_id) + :room_id{room_id} + {} + + events(const room &room) + :room_id{room.room_id} + {} +}; + +struct ircd::m::room::state +{ + id room_id; + + using event_closure = std::function; + using event_closure_bool = std::function; + + bool query(const event::where &, const event_closure_bool &) const; + void for_each(const event::where &, const event_closure &) const; + size_t count(const event::where &, const event_closure_bool &) const; + bool any(const event::where &, const event_closure_bool &) const; + + bool query(const event_closure_bool &) const; + void for_each(const event_closure &) const; + size_t count(const event::where &) const; + bool any(const event::where &) const; + + state(const id &room_id) + :room_id{room_id} + {} + + state(const room &room) + :room_id{room.room_id} + {} +}; diff --git a/include/ircd/m/timeline.h b/include/ircd/m/user.h similarity index 67% rename from include/ircd/m/timeline.h rename to include/ircd/m/user.h index 312770381..086ea0ed4 100644 --- a/include/ircd/m/timeline.h +++ b/include/ircd/m/user.h @@ -23,23 +23,27 @@ */ #pragma once -#define HAVE_IRCD_M_TIMELINE_H +#define HAVE_IRCD_M_USER_H -namespace ircd { -namespace m { - -/////////////////////////////////////////////////////////////////////////////// -// -// The timeline class represents a sequence of events. This is the preferred -// interface to the events DB. The timeline can be used to compose a selection -// of events and run algorithms over them. The timeline interface can also be -// used to insert new events. Timelines form the basis for composing higher -// level structures like rooms. -// -struct timeline +namespace ircd::m { + struct user; +} +namespace ircd::m::users +{ +} + +struct ircd::m::user +{ + using id = m::id::user; + + id user_id; + + user(const id &user_id); }; -} // namespace m -} // namespace ircd +inline +ircd::m::user::user(const id &user_id) +:user_id{user_id} +{} diff --git a/ircd/matrix.cc b/ircd/matrix.cc index df4c2ce22..2a195c2ec 100644 --- a/ircd/matrix.cc +++ b/ircd/matrix.cc @@ -46,7 +46,7 @@ ircd::m::session::operator()(parse::buffer &pb, r.method, r.path, r.query, - std::string(r), + r.content, write_closure(*this), { { "Content-Type"s, "application/json"s } @@ -77,56 +77,97 @@ ircd::m::session::operator()(parse::buffer &pb, // m.h // -namespace ircd { -namespace m { +namespace ircd::m +{ + std::map modules; + ircd::listener *listener; -std::map modules; -ircd::listener *listener; - -void bootstrap(); - -} // namespace m -} // namespace ircd + static void leave_ircd_room(); + static void join_ircd_room(); + void bootstrap(); +} ircd::m::init::init() +try { - if(db::sequence(*events::events) == 0) - bootstrap(); - for(const auto &name : mods::available()) if(startswith(name, "client_")) modules.emplace(name, name); + if(db::sequence(*event::events) == 0) + bootstrap(); + modules.emplace("root.so"s, "root.so"s); - //TODO: conf obviously - listener = new ircd::listener + const auto options{json::string(json::members { - json::string(json::members - { - { "name", "Chat Matrix" }, - { "host", "0.0.0.0" }, - { "port", 8447 }, - { "ssl_certificate_file", "/home/jason/zemos.net.tls.crt" }, - { "ssl_certificate_chain_file", "/home/jason/zemos.net.tls.crt" }, - { "ssl_tmp_dh_file", "/home/jason/zemos.net.tls.dh" }, - { "ssl_private_key_file_pem", "/home/jason/zemos.net.tls.key" }, - }) - }; + { "name", "Chat Matrix" }, + { "host", "0.0.0.0" }, + { "port", 8447 }, + { "ssl_certificate_file", "/home/jason/zemos.net.tls.crt" }, + { "ssl_certificate_chain_file", "/home/jason/zemos.net.tls.crt" }, + { "ssl_tmp_dh_file", "/home/jason/zemos.net.tls.dh" }, + { "ssl_private_key_file_pem", "/home/jason/zemos.net.tls.key" }, + })}; + + //TODO: conf obviously + listener = new ircd::listener{options}; + + join_ircd_room(); +} +catch(const m::error &e) +{ + log::critical("%s %s", e.what(), e.content); } ircd::m::init::~init() -noexcept +noexcept try { + leave_ircd_room(); + delete listener; modules.clear(); } +catch(const m::error &e) +{ + log::critical("%s %s", e.what(), e.content); + std::terminate(); +} + +void +ircd::m::join_ircd_room() +try +{ + // ircd.start event + const m::id::room::buf room_id{"ircd", "cdc.z"}; + const m::id::user::buf user_id{"ircd", "cdc.z"}; + m::room ircd_room{room_id}; + + json::iov content; + ircd_room.join(user_id, content); +} +catch(const m::ALREADY_MEMBER &e) +{ + log::warning("IRCd did not shut down correctly..."); +} + +void +ircd::m::leave_ircd_room() +{ + // ircd.start event + const m::id::room::buf room_id{"ircd", "cdc.z"}; + const m::id::user::buf user_id{"ircd", "cdc.z"}; + m::room ircd_room{room_id}; + + json::iov content; + ircd_room.leave(user_id, content); +} void ircd::m::bootstrap() { - assert(events::events); - assert(db::sequence(*events::events) == 0); + assert(event::events); + assert(db::sequence(*event::events) == 0); ircd::log::notice ( @@ -134,50 +175,48 @@ ircd::m::bootstrap() "database is empty. I will be bootstrapping it with initial events now..." ); - // ircd.create event - const m::id::id::room::buf room_id{"ircd", "localhost"}; - const m::id::id::user::buf user_id{"ircd", "localhost"}; + const m::id::user::buf user_id{"ircd", "cdc.z"}; + const m::id::room::buf ircd_room_id{"ircd", "cdc.z"}; + m::room ircd_room{ircd_room_id}; + ircd_room.send( { - const auto type{"m.room.create"}; - - char content[512]; - print(content, sizeof(content), json::members + { "type", "m.room.create" }, + { "sender", user_id }, + { "state_key", "" }, + { "content", json::members { - { "creator", user_id }, - }); - - m::event event - { - { "room_id", room_id }, - { "type", type }, - { "state_key", "" }, - { "sender", user_id }, - { "content", content }, - }; - - m::events::insert(event); - } + { "creator", user_id } + }} + }); + const m::id::room::buf accounts_room_id{"accounts", "cdc.z"}; + m::room accounts_room{accounts_room_id}; + accounts_room.send( { - const auto type{"m.room.member"}; - - char content[512]; - print(content, sizeof(content), json::members + { "type", "m.room.create" }, + { "sender", user_id }, + { "state_key", "" }, + { "content", json::members { - { "membership", "join" }, - }); + { "creator", user_id } + }} + }); - m::event event + json::iov content; + accounts_room.join(user_id, content); + + const m::id::room::buf filters_room_id{"filters", "cdc.z"}; + m::room filters_room{filters_room_id}; + filters_room.send( + { + { "type", "m.room.create" }, + { "sender", user_id }, + { "state_key", "" }, + { "content", json::members { - { "room_id", room_id }, - { "type", type }, - { "state_key", user_id }, - { "sender", user_id }, - { "content", content }, - }; - - m::events::insert(event); - } + { "creator", user_id } + }} + }); } /////////////////////////////////////////////////////////////////////////////// @@ -199,13 +238,13 @@ ircd::m::dbs::init::init() init_modules(); init_databases(); - ircd::m::events::events = databases.at("events").get(); + ircd::m::event::events = databases.at("events").get(); } ircd::m::dbs::init::~init() noexcept { - ircd::m::events::events = nullptr; + ircd::m::event::events = nullptr; databases.clear(); modules.clear(); @@ -235,177 +274,20 @@ ircd::m::dbs::init_modules() modules.emplace(name, name); } -/////////////////////////////////////////////////////////////////////////////// -// -// m/events.h -// - -namespace ircd::m::events -{ - void write(const event &); -} - -ircd::database *ircd::m::events::events; - -void -ircd::m::events::insert(json::iov &iov) -{ - const id::event::buf generated_event_id - { - iov.has("event_id")? id::event::buf{} : id::event::buf{id::generate, "cdc.z"} - }; - - const json::iov::add event_id - { - iov, { "event_id", generated_event_id } - }; - - const json::iov::add origin_server_ts - { - iov, { "origin_server_ts", time() } - }; - - insert(event{iov}); -} - -void -ircd::m::events::insert(const event &event) -{ - if(!json::val(event)) - throw BAD_JSON("Required event field: '%s'", name::type); - - if(!json::val(event)) - throw BAD_JSON("Required event field: '%s'", name::sender); - - if(!json::val(event)) - throw BAD_JSON("Required event field: '%s'", name::event_id); - - if(!json::val(event)) - throw BAD_JSON("Required event field: '%s'", name::origin_server_ts); - - for(const auto &transition : events::transitions) - if(!transition->valid(event)) - throw INVALID_TRANSITION("Event insertion refused: '%s'", - transition->name); - - write(event); -} - -ircd::m::events::const_iterator -ircd::m::events::find(const id &id) -{ - cursor cur{"!room_id$event_id"}; - return cur.find(id); -} - -ircd::m::events::const_iterator -ircd::m::events::begin() -{ - cursor cur{"!room_id$event_id"}; - return cur.begin(); -} - -ircd::m::events::const_iterator -ircd::m::events::end() -{ - cursor cur{"!room_id$event_id"}; - return cur.end(); -} - -ircd::m::events::transition_list -ircd::m::events::transitions -{}; - -ircd::m::events::transition::transition(const char *const &name) -:name{name} -,it -{ - events::transitions, events::transitions.emplace(events::transitions.end(), this) -} -{ -} - -ircd::m::events::transition::transition(const char *const &name, - struct method method) -:name{name} -,method{std::move(method)} -,it -{ - events::transitions, events::transitions.emplace(events::transitions.end(), this) -} -{ -} - -ircd::m::events::transition::~transition() -noexcept -{ -} - -bool -ircd::m::events::transition::valid(const event &event) -const -{ - return method.valid(event); -} - -void -ircd::m::events::transition::effects(const event &event) -{ - method.effects(event); -} - -void -ircd::m::events::write(const event &event) -{ - const auto &master_index - { - at(event) - }; - - constexpr const size_t num_indexes(1); - constexpr const size_t num_deltas - { - event.size() + num_indexes - }; - - size_t i(0); - db::delta deltas[num_deltas]; - for_each(event, [&i, &deltas, &master_index] - (const auto &key, const auto &val) - { - deltas[i++] = - { - key, // col - master_index, // key - byte_view<>{val}, // val - }; - }); - - char buf[id::event::buf::SIZE + id::room::buf::SIZE]; - if(!json::val(event).empty()) - { - strlcpy(buf, json::val(event).data(), sizeof(buf)); - strlcat(buf, json::val(event).data(), sizeof(buf)); - deltas[i++] = db::delta - { - "!room_id$event_id", // col - buf, // key - }; - } - - (*events)(begin(deltas), begin(deltas) + i); -} - /////////////////////////////////////////////////////////////////////////////// // // m/room.h // +// +// m::room +// + void ircd::m::room::join(const m::id::user &user_id, json::iov &content) { - json::iov::set membership_join + const json::iov::set membership_join { content, { "membership", "join" } }; @@ -413,28 +295,46 @@ ircd::m::room::join(const m::id::user &user_id, membership(user_id, content); } +void +ircd::m::room::leave(const m::id::user &user_id, + json::iov &content) +{ + const json::iov::set membership_leave + { + content, { "membership", "leave" } + }; + + membership(user_id, content); +} + void ircd::m::room::membership(const m::id::user &user_id, json::iov &content) { - if(is_member(user_id, content.at("membership"))) - throw m::ALREADY_MEMBER - { - "Already a member with this membership." - }; - - char buffer[512]; - const auto printed_content + const string_view membership { - stringify(buffer, content) + content.at("membership") }; - json::iov event + if(is_member(user_id, membership)) + throw m::ALREADY_MEMBER + { + "Member '%s' is already '%s'.", string_view{user_id}, membership + }; + + //TODO: child iov + const std::string c { - { "type", "m.room.member" }, - { "state_key", user_id }, - { "sender", user_id }, - { "content", printed_content } + json::string(content) + }; + + json::iov event; + json::iov::push members[] + { + { event, json::member { "type", "m.room.member" }}, + { event, json::member { "state_key", user_id }}, + { event, json::member { "sender", user_id }}, + { event, json::member { "content", string_view{c} }} }; send(event); @@ -444,105 +344,595 @@ bool ircd::m::room::is_member(const m::id::user &user_id, const string_view &membership) { - const m::events::where::equal query + const m::event::where::equal event { { "type", "m.room.member" }, { "state_key", user_id } }; - return count(query, [](const auto &event) + bool ret{false}; + const events events{*this}; + events.query(event, [&ret, &membership] + (const auto &event) { const json::object &content { json::val(event) }; - const auto &membership + const auto &existing { unquote(content["membership"]) }; - return membership == membership; + ret = membership == existing; + return true; + }); + + return ret; +} + +void +ircd::m::room::send(const json::members &event) +{ + size_t i(0); + json::iov iov; + json::iov::push members[event.size()]; + for(const auto &member : event) + new (members + i++) json::iov::push(iov, member); + + send(iov); +} + +void +ircd::m::room::send(json::iov &event) +{ + const json::iov::add room_id + { + event, { "room_id", this->room_id } + }; + + const id::event::buf generated_event_id + { + id::generate, this->room_id.host() + }; + + const json::iov::add event_id + { + event, { "event_id", generated_event_id } + }; + + m::event::insert(event); +} + +// +// m::room::events +// + +bool +ircd::m::room::events::any(const event::where &where) +const +{ + return any(where, [](const auto &event) + { + return true; }); } bool -ircd::m::room::any(const events::where &where) +ircd::m::room::events::any(const event::where &where, + const event_closure_bool &closure) const { - events::cursor cursor{"!room_id$event_id"}; - cursor.where = &where; - return bool(cursor.find(room_id)); + return query(where, closure); +} + +size_t +ircd::m::room::events::count(const event::where &where) +const +{ + return count(where, [](const auto &event) + { + return true; + }); +} + +size_t +ircd::m::room::events::count(const event::where &where, + const event_closure_bool &closure) +const +{ + size_t i(0); + for_each(where, [&closure, &i](const auto &event) + { + i += closure(event); + }); + + return i; +} + +void +ircd::m::room::events::rfor_each(const event_closure &closure) +const +{ + const m::event::where::noop where{}; + rfor_each(where, closure); +} + +void +ircd::m::room::events::rfor_each(const event::where &where, + const event_closure &closure) +const +{ + rquery(where, [&closure](const auto &event) + { + closure(event); + return false; + }); +} + +void +ircd::m::room::events::for_each(const event::where &where, + const event_closure &closure) +const +{ + query(where, [&closure](const auto &event) + { + closure(event); + return false; + }); +} + +void +ircd::m::room::events::for_each(const event_closure &closure) +const +{ + const m::event::where::noop where{}; + for_each(where, closure); } bool -ircd::m::room::any(const events::where &where, - const event_closure_bool &closure) +ircd::m::room::events::rquery(const event_closure_bool &closure) const { - events::cursor cursor{"!room_id$event_id"}; - cursor.where = &where; + const m::event::where::noop where{}; + return rquery(where, closure); +} - for(auto it(cursor.find(room_id)); bool(it); ++it) +bool +ircd::m::room::events::rquery(const event::where &where, + const event_closure_bool &closure) +const +{ + event::cursor cursor + { + "event_id in room_id", + &where + }; + + for(auto it(cursor.rbegin(room_id)); bool(it); ++it) if(closure(*it)) return true; return false; } -size_t -ircd::m::room::count(const events::where &where) +bool +ircd::m::room::events::query(const event_closure_bool &closure) const { - events::cursor cursor{"!room_id$event_id"}; - cursor.where = &where; - - size_t i(0); - for(auto it(cursor.find(room_id)); bool(it); ++it, i++) - return i; + const m::event::where::noop where{}; + return query(where, closure); } -size_t -ircd::m::room::count(const events::where &where, - const event_closure_bool &closure) +bool +ircd::m::room::events::query(const event::where &where, + const event_closure_bool &closure) const { - events::cursor cursor{"!room_id$event_id"}; - cursor.where = &where; - - size_t i(0); - for(auto it(cursor.find(room_id)); bool(it); ++it) + event::cursor cursor { - const m::event &e(*it); - i += closure(e); - } + "event_id in room_id", + &where + }; + + for(auto it(cursor.begin(room_id)); bool(it); ++it) + if(closure(*it)) + return true; + + return false; +} + +// +// m::room::state +// + +bool +ircd::m::room::state::any(const event::where &where) +const +{ + return any(where, [](const auto &event) + { + return true; + }); +} + +bool +ircd::m::room::state::any(const event::where &where, + const event_closure_bool &closure) +const +{ + return query(where, closure); +} + +size_t +ircd::m::room::state::count(const event::where &where) +const +{ + return count(where, [](const auto &event) + { + return true; + }); +} + +size_t +ircd::m::room::state::count(const event::where &where, + const event_closure_bool &closure) +const +{ + size_t i(0); + for_each(where, [&closure, &i](const auto &event) + { + i += closure(event); + }); return i; } void -ircd::m::room::for_each(const events::where &where, - const event_closure &closure) +ircd::m::room::state::for_each(const event::where &where, + const event_closure &closure) const { - events::cursor cursor{"!room_id$event_id"}; - cursor.where = &where; - auto it(cursor.find(room_id)); - if(!it) - return; - - for(; bool(it); ++it) - closure(*it); + query(where, [&closure](const auto &event) + { + closure(event); + return false; + }); } void -ircd::m::room::for_each(const event_closure &closure) +ircd::m::room::state::for_each(const event_closure &closure) const { - const m::events::where::noop where{}; - for_each(where, closure); + query([&closure](const auto &event) + { + closure(event); + return false; + }); } +bool +ircd::m::room::state::query(const event_closure_bool &closure) +const +{ + const m::event::where::noop where{}; + return query(where, closure); +} + +bool +ircd::m::room::state::query(const event::where &where, + const event_closure_bool &closure) +const +{ + event::cursor cursor + { + "event_id for type,state_key in room_id", + &where + }; + + for(auto it(cursor.begin(room_id)); bool(it); ++it) + if(closure(*it)) + return true; + + return false; +} + +/////////////////////////////////////////////////////////////////////////////// +// +// m/event.h +// + +namespace ircd::m +{ + void append_indexes(const event &, db::iov &); +} + +ircd::database * +ircd::m::event::events +{}; + +ircd::ctx::view +ircd::m::event::inserted +{}; + +void +ircd::m::event::insert(json::iov &iov) +{ + const id::event::buf generated_event_id + { + iov.has("event_id")? id::event::buf{} : id::event::buf{id::generate, "cdc.z"} + }; + + const json::iov::add_if event_id + { + iov, !iov.has("event_id"), { "event_id", generated_event_id } + }; + + const json::iov::set origin_server_ts + { + iov, { "origin_server_ts", time() } + }; + + const m::event event + { + iov + }; + + if(!json::at(event)) + throw BAD_JSON("Required event field: '%s'", name::type); + + if(!json::at(event)) + throw BAD_JSON("Required event field: '%s'", name::sender); + + db::iov txn + { + *event::events + }; + + db::iov::append + { + txn, json::at(event), iov + }; + + append_indexes(event, txn); + + txn(*event::events); + + event::inserted.notify(event); +} + +ircd::m::event::const_iterator +ircd::m::event::find(const event::id &id) +{ + cursor c{name::event_id}; + return c.begin(string_view{id}); +} + +namespace ircd::m +{ + struct indexer; + + extern std::set> indexers; +} + +struct ircd::m::indexer +{ + struct concat; + struct concat_v; + struct concat_2v; + + std::string name; + + virtual void operator()(const event &, db::iov &iov) const = 0; + + indexer(std::string name) + :name{std::move(name)} + {} + + virtual ~indexer() noexcept; +}; + +ircd::m::indexer::~indexer() +noexcept +{ +} + +void +ircd::m::append_indexes(const event &event, + db::iov &iov) +{ + for(const auto &ptr : indexers) + { + const m::indexer &indexer{*ptr}; + indexer(event, iov); + } +} + + +struct ircd::m::indexer::concat +:indexer +{ + std::string col_a; + std::string col_b; + + void operator()(const event &, db::iov &) const override; + + concat(std::string col_a, std::string col_b) + :indexer + { + fmt::snstringf(512, "%s in %s", col_a, col_b) + } + ,col_a{col_a} + ,col_b{col_b} + {} +}; + +void +ircd::m::indexer::concat::operator()(const event &event, + db::iov &iov) +const +{ + const size_t buf_max + { + 1024 + }; + + char index[buf_max]; + index[0] = '\0'; + const auto function + { + [&index, &buf_max](const auto &val) + { + strlcat(index, byte_view{val}, buf_max); + } + }; + + at(event, col_b, function); + at(event, col_a, function); + db::iov::append + { + iov, db::delta + { + name, // col + index, // key + {}, // val + } + }; +} + +struct ircd::m::indexer::concat_v +:indexer +{ + std::string col_a; + std::string col_b; + std::string col_c; + + void operator()(const event &, db::iov &) const override; + + concat_v(std::string col_a, std::string col_b, std::string col_c) + :indexer + { + fmt::snstringf(512, "%s for %s in %s", col_a, col_b, col_c) + } + ,col_a{col_a} + ,col_b{col_b} + ,col_c{col_c} + {} +}; + +void +ircd::m::indexer::concat_v::operator()(const event &event, + db::iov &iov) +const +{ + const size_t buf_max + { + 1024 + }; + + char index[buf_max]; + index[0] = '\0'; + const auto concat + { + [&index, &buf_max](const auto &val) + { + strlcat(index, byte_view{val}, buf_max); + } + }; + + at(event, col_c, concat); + at(event, col_b, concat); + + string_view val; + at(event, col_a, [&val](const auto &_val) + { + val = byte_view{_val}; + }); + + db::iov::append + { + iov, db::delta + { + name, // col + index, // key + val, // val + } + }; +} + +struct ircd::m::indexer::concat_2v +:indexer +{ + std::string col_a; + std::string col_b0; + std::string col_b1; + std::string col_c; + + void operator()(const event &, db::iov &) const override; + + concat_2v(std::string col_a, std::string col_b0, std::string col_b1, std::string col_c) + :indexer + { + fmt::snstringf(512, "%s for %s,%s in %s", col_a, col_b0, col_b1, col_c) + } + ,col_a{col_a} + ,col_b0{col_b0} + ,col_b1{col_b1} + ,col_c{col_c} + {} +}; + +void +ircd::m::indexer::concat_2v::operator()(const event &event, + db::iov &iov) +const +{ + const size_t buf_max + { + 2048 + }; + + char index[buf_max]; + index[0] = '\0'; + const auto concat + { + [&index, &buf_max](const auto &val) + { + strlcat(index, byte_view{val}, buf_max); + } + }; + + at(event, col_c, concat); + strlcat(index, "..", buf_max); //TODO: special + at(event, col_b0, concat); + at(event, col_b1, concat); + + string_view val; + at(event, col_a, [&val](const auto &_val) + { + val = byte_view{_val}; + }); + + db::iov::append + { + iov, db::delta + { + name, // col + index, // key + val, // val + } + }; +} + +std::set> ircd::m::indexers +{{ + std::make_shared("event_id", "sender"), + std::make_shared("event_id", "room_id"), + std::make_shared("event_id", "room_id", "type"), + std::make_shared("event_id", "type", "state_key", "room_id"), +}}; + /////////////////////////////////////////////////////////////////////////////// // // m/id.h @@ -639,14 +1029,14 @@ ircd::m::id::id(const enum sigil &sigil, break; }; - const size_t len + const auto len { fmt::snprintf(buf, max, "%s:%s", name, host) }; - return string_view { buf, len }; + return string_view { buf, size_t(len) }; }()} ,sigil{sigil} { diff --git a/ircd/resource.cc b/ircd/resource.cc index b17c44bec..e3c01b1d6 100644 --- a/ircd/resource.cc +++ b/ircd/resource.cc @@ -131,7 +131,7 @@ try { "state_key", access_token } }; - if(!accounts.any(query)) + if(!m::room::events{accounts}.any(query)) throw m::error { // When credentials are required but missing or invalid, the HTTP call will return with @@ -215,6 +215,13 @@ catch(const json::error &e) http::BAD_REQUEST, "M_NOT_JSON", "%s", e.what() }; } +catch(const std::out_of_range &e) +{ + throw m::error + { + http::NOT_FOUND, "M_NOT_FOUND", "%s", e.what() + }; +} ircd::resource::method::method(struct resource &resource, const string_view &name, diff --git a/modules/client/events.cc b/modules/client/events.cc index 7719fcf6a..b39dc6180 100644 --- a/modules/client/events.cc +++ b/modules/client/events.cc @@ -39,33 +39,44 @@ const m::id::room::buf locops_room_id const m::id::room::buf ircd_room_id { - "ircd", "localhost" + "ircd", "cdc.z" }; resource::response get_events(client &client, const resource::request &request) { - m::room room{accounts_room_id}; - std::vector ret; - - room.for_each([&ret](const auto &event) + m::room room { - ret.emplace_back(json::string(event)); + m::id::room + { + unquote(request.at("room_id")) + } + }; + + const m::room::events events + { + room + }; + + size_t i(0); + events.for_each([&i](const auto &event) + { + ++i; }); - std::vector jo(ret.size()); - std::transform(ret.begin(), ret.end(), jo.begin(), [] - (const auto &string) -> json::object + size_t j(0); + json::value ret[i]; + events.for_each([&i, &j, &ret](const m::event &event) { - return string; + if(j < i) + ret[j++] = event; }); - char buf[16384]; return resource::response { - client, + client, json::members { - { "chunk", json::stringify(buf, jo) } + { "chunk", { ret, j } } } }; } diff --git a/modules/client/login.cc b/modules/client/login.cc index 5a99118a2..dac6865df 100644 --- a/modules/client/login.cc +++ b/modules/client/login.cc @@ -34,14 +34,14 @@ const auto home_server "cdc.z" }; -namespace name +namespace { namespace name { - extern constexpr auto password{"password"}; - extern constexpr auto medium{"medium"}; - extern constexpr auto type{"type"}; - extern constexpr auto user{"user"}; - extern constexpr auto address{"address"}; -} + constexpr const auto password{"password"}; + constexpr const auto medium{"medium"}; + constexpr const auto type{"type"}; + constexpr const auto user{"user"}; + constexpr const auto address{"address"}; +}} struct body :json::tuple @@ -69,15 +69,19 @@ m::room accounts_room accounts_room_id }; +const m::room::events accounts_room_events +{ + accounts_room +}; + resource::response post_login_password(client &client, - const resource::request &request, - const resource::request::body &body) + const resource::request::object &request) { // Build a canonical MXID from a the user field const m::id::user::buf user_id { - unquote(at(body)), home_server + unquote(at(request)), home_server }; if(!user_id.valid() || user_id.host() != home_server) @@ -88,14 +92,23 @@ post_login_password(client &client, const auto &supplied_password { - at(body) + at(request) }; - const auto check{[&supplied_password] + // Sets up the query to find the user_id in the accounts room + const m::event::where::equal member_event + { + { "type", "m.room.member" }, + { "state_key", user_id } + }; + + // Once the query finds the result this closure views the event in the + // database and returns true if the login is authentic. + const m::event::where::test correct_password{[&supplied_password] (const auto &event) { const json::object &content - { + { json::val(event) }; @@ -109,56 +122,79 @@ post_login_password(client &client, const auto &correct_password { - content.at("password") + content.get("password") }; + if(!correct_password) + return false; + if(supplied_password != correct_password) return false; return true; }}; - const m::events::where::equal query + const auto query { - { "type", "m.room.member" }, - { "state_key", user_id } + member_event && correct_password }; - if(!accounts_room.any(query, check)) + // The query to the database is made here. Know that this ircd::ctx + // may suspend and global state may have changed after this call. + if(!accounts_room_events.query(query)) throw m::error { http::FORBIDDEN, "M_FORBIDDEN", "Access denied." }; + // Generate the access token + static constexpr const auto token_len{127}; + static const auto token_dict{rand::dict::alpha}; + char token_buf[token_len + 1]; + const string_view access_token + { + rand::string(token_dict, token_len, token_buf, sizeof(token_buf)) + }; + + // Log the user in by issuing an event in the accounts room containing + // the generated token. When this call completes without throwing the + // access_token will be committed and the user will be logged in. + accounts_room.send( + { + { "type", "ircd.access_token" }, + { "sender", user_id }, + { "state_key", access_token }, + { "content", json::members + { + { "ip", string(remote_addr(client)) }, + { "device", "unknown" }, + }} + }); + // Send response to user return resource::response { client, { - { "user_id", string_view{user_id} }, - { "home_server", home_server }, -// { "access_token", access_token }, + { "user_id", user_id }, + { "home_server", home_server }, + { "access_token", access_token }, } }; } resource::response -post_login(client &client, const resource::request &request) +post_login(client &client, const resource::request::object &request) { - const resource::request::body body - { - request - }; - // x.x.x Required. The login type being used. // Currently only "m.login.password" is supported. const auto type { - unquote(at(body)) + unquote(at(request)) }; if(type == "m.login.password") - return post_login_password(client, request, body); + return post_login_password(client, request); else throw m::error { @@ -174,14 +210,22 @@ resource::method method_post resource::response get_login(client &client, const resource::request &request) { - static const json::object flows + json::member login_password { - R"({"flows":[{"type":"m.login.password"}]})" + "type", "m.login.password" + }; + + json::value flows[1] + { + { login_password } }; return resource::response { - client, flows + client, json::members + { + { "flows", { flows, 1 } } + } }; } diff --git a/modules/client/logout.cc b/modules/client/logout.cc index 4b37ed77e..dd58b6903 100644 --- a/modules/client/logout.cc +++ b/modules/client/logout.cc @@ -31,18 +31,14 @@ resource logout_resource resource::response logout(client &client, const resource::request &request) { - const auto &access_token(request.query.at("access_token")); - const auto it(resource::tokens.find(access_token)); - if(unlikely(it == end(resource::tokens))) - throw http::error{http::INTERNAL_SERVER_ERROR}; + const auto &access_token + { + request.query.at("access_token") + }; - resource::tokens.erase(it); return resource::response { - client, - { - { } - } + client, http::OK }; } diff --git a/modules/client/publicrooms.cc b/modules/client/publicrooms.cc index 643068b66..64d488836 100644 --- a/modules/client/publicrooms.cc +++ b/modules/client/publicrooms.cc @@ -33,10 +33,7 @@ get_publicrooms(client &client, const resource::request &request) { return resource::response { - client, - { - { } - } + client, http::OK }; } diff --git a/modules/client/pushrules.cc b/modules/client/pushrules.cc index b868d705a..1d04f07df 100644 --- a/modules/client/pushrules.cc +++ b/modules/client/pushrules.cc @@ -36,10 +36,7 @@ try { return resource::response { - client, - { - { } - } + client, http::OK }; } catch(...) diff --git a/modules/client/register.cc b/modules/client/register.cc index d193f6f80..4ed6a1721 100644 --- a/modules/client/register.cc +++ b/modules/client/register.cc @@ -27,13 +27,13 @@ const auto home_server "cdc.z" }; -namespace name +namespace { namespace name { - constexpr auto username{"username"}; - constexpr auto bind_email{"bind_email"}; - constexpr auto password{"password"}; - constexpr auto auth{"auth"}; -} + constexpr const auto username {"username"}; + constexpr const auto bind_email {"bind_email"}; + constexpr const auto password {"password"}; + constexpr const auto auth {"auth"}; +}} struct body :json::tuple @@ -44,7 +44,7 @@ struct body json::property > { - using super_type::tuple; + using super_type::tuple; }; // Generate !accounts:host which is the MXID of the room where the members @@ -54,40 +54,26 @@ const m::id::room::buf accounts_room_id "accounts", home_server }; +// This object is a lightweight handle to the accounts room, which is a +// chatroom whose members are the representation of the accounts registered +// to this server itself. m::room accounts_room { accounts_room_id }; -static void -join_accounts_room(const m::id::user &user_id, - const json::members &contents) -try -{ - const json::builder content - { - nullptr, &contents - }; - - accounts_room.join(user_id, content); -} -catch(const m::ALREADY_MEMBER &e) -{ - throw m::error - { - http::CONFLICT, "M_USER_IN_USE", "The desired user ID is already in use." - }; -} +static void validate_user_id(const m::id::user &user_id); +static void validate_password(const string_view &password); +static void join_accounts_room(const m::id::user &user_id, const json::members &contents); resource::response handle_post_kind_user(client &client, - resource::request &request, - const resource::request::body &body) + const resource::request::object &request) { // 3.3.1 Additional authentication information for the user-interactive authentication API. const auto &auth { - at(body) + at(request) }; // 3.3.1 Required. The login type that the client is attempting to complete. @@ -96,6 +82,7 @@ handle_post_kind_user(client &client, unquote(auth.at("type")) }; + // We only support this for now, for some reason. TODO: XXX if(type != "m.login.dummy") throw m::error { @@ -106,53 +93,41 @@ handle_post_kind_user(client &client, // generate a Matrix ID local part. const auto &username { - unquote(get(body)) + unquote(get(request)) }; - // Generate canonical mxid + // Generate canonical mxid. The home_server is appended if one is not + // specified. We do not generate a user_id here if the local part is not + // specified. TODO: isn't that guest reg? const m::id::user::buf user_id { username, home_server }; - if(!user_id.valid()) - throw m::error - { - "M_INVALID_USERNAME", "The desired user ID is not a valid user name." - }; - - if(user_id.host() != home_server) - throw m::error - { - "M_INVALID_USERNAME", "Can only register with host '%s'", home_server - }; + // Check if the the user_id is acceptably formed for this server or throws + validate_user_id(user_id); // 3.3.1 Required. The desired password for the account. const auto &password { - at(body) + at(request) }; - if(password.size() > 255) - throw m::error - { - "M_INVALID_PASSWORD", "The desired password is too long" - }; - // 3.3.1 If true, the server binds the email used for authentication to the // Matrix ID with the ID Server. Defaults to false. const auto &bind_email { - get(body, false) + get(request, false) }; - // Register the user by joining them to the accounts room. Join the user - // to the accounts room by issuing a join event. The content will store - // keys from the registration options including the password - do not - // expose this to clients //TODO: store hashed pass - // Once this call completes the join was successful and the user - // is registered, otherwise throws. - char content[384]; + // Check if the password is acceptable for this server or throws + validate_password(password); + + // Register the user by joining them to the accounts room. The content of + // the join event will store keys from the registration options including + // the password - do not expose this to clients //TODO: store hashed pass + // Once this call completes the join was successful and the user is + // registered, otherwise throws. join_accounts_room(user_id, { { "password", password }, @@ -173,8 +148,7 @@ handle_post_kind_user(client &client, resource::response handle_post_kind_guest(client &client, - resource::request &request, - const resource::request::body &body) + const resource::request::object &request) { const m::id::user::buf user_id { @@ -187,30 +161,25 @@ handle_post_kind_guest(client &client, { { "user_id", user_id }, { "home_server", home_server }, - //{ "access_token", access_token }, +// { "access_token", access_token }, } }; } resource::response handle_post(client &client, - resource::request &request) + const resource::request::object &request) { - const resource::request::body body - { - request - }; - const auto kind { request.query["kind"] }; if(kind == "user") - return handle_post_kind_user(client, request, body); + return handle_post_kind_user(client, request); if(kind.empty() || kind == "guest") - return handle_post_kind_guest(client, request, body); + return handle_post_kind_guest(client, request); throw m::error { @@ -233,3 +202,51 @@ mapi::header IRCD_MODULE { "registers the resource 'client/register' to handle requests" }; + +void +join_accounts_room(const m::id::user &user_id, + const json::members &contents) +try +{ + json::iov content; + json::iov::push members[contents.size()]; + + size_t i(0); + for(const auto &member : contents) + new (members + i++) json::iov::push(content, member); + + accounts_room.join(user_id, content); +} +catch(const m::ALREADY_MEMBER &e) +{ + throw m::error + { + http::CONFLICT, "M_USER_IN_USE", "The desired user ID is already in use." + }; +} + +void +validate_user_id(const m::id::user &user_id) +{ + if(!user_id.valid()) + throw m::error + { + "M_INVALID_USERNAME", "The desired user ID is not a valid user name." + }; + + if(user_id.host() != home_server) + throw m::error + { + "M_INVALID_USERNAME", "Can only register with host '%s'", home_server + }; +} + +void +validate_password(const string_view &password) +{ + if(password.size() > 255) + throw m::error + { + "M_INVALID_PASSWORD", "The desired password is too long" + }; +} diff --git a/modules/client/rooms.cc b/modules/client/rooms.cc index 003a3168c..450d0dd32 100644 --- a/modules/client/rooms.cc +++ b/modules/client/rooms.cc @@ -24,15 +24,188 @@ using namespace ircd; struct room :resource { + static constexpr const auto base_url + { + "_matrix/client/r0/rooms/" + }; + using resource::resource; } rooms_resource { - "_matrix/client/r0/rooms/", - "Rooms (7.0)" + room::base_url, resource::opts + { + resource::DIRECTORY, + "Rooms (7.0)" + } }; mapi::header IRCD_MODULE { "registers the resource 'client/rooms'" }; + +resource::response +get_state(client &client, + const resource::request &request, + const string_view ¶ms, + const m::room::state &state, + const m::event::where &query) +{ + const auto count + { + state.count(query) + }; + + size_t j(0); + json::value ret[count]; + state.for_each(query, [&count, &j, &ret] + (const auto &event) + { + if(j < count) + ret[j++] = event; + }); + + return resource::response + { + client, json::value + { + ret, j + } + }; +} + +resource::response +get_state(client &client, + const resource::request &request, + const string_view ¶ms, + const m::room::state &state, + const string_view &type, + const string_view &state_key) +{ + const m::event::where::equal query + { + { "type", type }, + { "state_key", state_key }, + }; + + return get_state(client, request, params, state, query); +} + +resource::response +get_state(client &client, + const resource::request &request, + const string_view ¶ms, + const m::room::state &state, + const string_view &type) +{ + const m::event::where::equal query + { + { "type", type } + }; + + return get_state(client, request, params, state, query); +} + +resource::response +get_state(client &client, + const resource::request &request, + const string_view ¶ms, + const m::room::id &room_id) +{ + string_view token[4]; + tokens(params, "/", token); + + const string_view &type + { + token[2] + }; + + const string_view &state_key + { + token[3] + }; + + const m::room::state state + { + room_id + }; + + if(type && state_key) + return get_state(client, request, params, state, type, state_key); + + if(type) + return get_state(client, request, params, state, type); + + const m::event::where::noop query; + return get_state(client, request, params, state, query); +} + +resource::response +get_context(client &client, + const resource::request &request, + const string_view ¶ms, + const m::room::id &room_id) +{ + const m::event::id &event_id + { + token(params, "/", 2) + }; + + const auto it + { + m::event::find(event_id) + }; + + if(!it) + throw http::error(http::NOT_FOUND, "event not found"); + + const auto event + { + json::string(*it) + }; + + return resource::response + { + client, json::members + { + { "event", event } + } + }; +} + +resource::response +get_rooms(client &client, const resource::request &request) +{ + const auto params + { + lstrip(request.head.path, room::base_url) + }; + + string_view token[2]; + if(tokens(params, "/", token) != 2) + throw http::error(http::code::NOT_FOUND, "/rooms command required"); + + const m::room::id &room_id + { + token[0] + }; + + const string_view &cmd + { + token[1] + }; + + if(cmd == "context") + return get_context(client, request, params, room_id); + + if(cmd == "state") + return get_state(client, request, params, room_id); + + throw http::error(http::code::NOT_FOUND, "/rooms command not found"); +} + +resource::method method_get +{ + rooms_resource, "GET", get_rooms +}; diff --git a/modules/client/sync.cc b/modules/client/sync.cc index 2db8905c1..ac35f5dbd 100644 --- a/modules/client/sync.cc +++ b/modules/client/sync.cc @@ -26,13 +26,27 @@ resource sync_resource "_matrix/client/r0/sync", R"( - Synchronise the client's state with the latest state on the server. + 6.2. Synchronise the client's state with the latest state on the server. Clients use this API when they first log in to get an initial snapshot of the state on the server, and then continue to call this API to get - incremental deltas to the state, and to receive new messages. (6.2) + incremental deltas to the state, and to receive new messages. )" }; +struct polldata +{ + std::weak_ptr client; + steady_point timeout; +}; + +std::deque +polling +{}; + +ircd::ctx::dock +polldock +{}; + resource::response sync(client &client, const resource::request &request) { @@ -72,12 +86,22 @@ sync(client &client, const resource::request &request) request.get("set_presence", "offline") }; - const auto timeout + const milliseconds timeout { // 6.2.1 The maximum time to poll in milliseconds before returning this request. - request.get("timeout", -1) + request.get("timeout", 30 * 1000) }; + // A reference to the client is saved. We save a weak reference to still + // allow the client to disconnect. + polling.emplace_back(polldata + { + weak_from(client), + now() + timeout + }); + + // This handler returns no response. As long as this handler doesn't throw + // an exception IRCd will keep the client alive. return {}; } @@ -89,7 +113,81 @@ resource::method get_sync } }; +void worker(); +ircd::context synchronizer_context +{ + "synchronizer", + 1_MiB, + &worker, + ircd::context::POST, +}; + +const auto on_unload{[] +{ + synchronizer_context.interrupt(); + synchronizer_context.join(); +}}; + mapi::header IRCD_MODULE { - "registers the resource 'client/sync' to handle requests." + "registers the resource 'client/sync' to handle requests.", + nullptr, + on_unload }; + +void +handle_event(const m::event &event, + const polldata &request) +try +{ + const life_guard client + { + request.client + }; + + resource::response + { + *client, json::members + { + { "event", json::string(event) } + } + }; +} +catch(const std::exception &e) +{ + log::error("%s", e.what()); +} + +void +synchronize(const m::event &event) +{ + if(polling.empty()) + return; + + const auto &request + { + polling.front() + }; + + handle_event(event, request); + polling.pop_front(); +} + +void +worker() +try +{ + for(;; ctx::interruption_point()) + { + const auto &event + { + m::event::inserted.wait() + }; + + synchronize(event); + } +} +catch(const ircd::ctx::interrupted &e) +{ + ircd::log::debug("Synchronizer interrupted"); +} diff --git a/modules/client/user.cc b/modules/client/user.cc index f8ef1ba12..b3cece38e 100644 --- a/modules/client/user.cc +++ b/modules/client/user.cc @@ -30,35 +30,58 @@ resource user_resource } }; -namespace ircd +m::room filters_room { - static string_view extract_user_id(const auto &path, const auto &suffix); -} + m::room::id{"!filters:cdc.z"} +}; -ircd::string_view -ircd::extract_user_id(const auto &path, - const auto &suffix) +const m::room::events filters_room_events { - // Extract the user_id from _matrix/client/r0/user/$user/filter - return lstrip(between(path, user_resource.path, suffix), '/'); -} + filters_room +}; resource::response get_filter(client &client, const resource::request &request) try { - const auto user_id + const m::user::id user_id { - extract_user_id(request.head.path, "/filter") + token(request.head.path, '/', 4) }; - return resource::response + const auto filter_id { - client, - { - { } - } + token(request.head.path, '/', 6) }; + + const m::event::where::equal query + { + { "type", "ircd.filter" }, + { "sender", user_id }, + { "state_key", filter_id } + }; + + const auto result{[&client] + (const m::event &event) + { + const json::object &filter + { + json::at(event) + }; + + resource::response + { + client, filter + }; + + return true; + }}; + + if(!filters_room_events.any(query, result)) + throw m::NOT_FOUND("No matching filter with that ID"); + + // Response already made + return {}; } catch(...) { @@ -67,65 +90,88 @@ catch(...) resource::method get { - user_resource, "GET", get_filter + user_resource, "GET", get_filter, + { + resource::method::REQUIRES_AUTH + } }; // (5.2) Uploads a new filter definition to the homeserver. Returns a filter ID that // may be used in future requests to restrict which events are returned to the client. resource::response -post_filter(client &client, const resource::request &request) +post_filter(client &client, const resource::request::object &request) try { - const auto user_id + const auto &user_id { // (5.2) Required. The id of the user uploading the filter. The access // token must be authorized to make requests for this user id. - extract_user_id(request.head.path, "/filter") + token(request.head.path, '/', 4) }; - const auto event_fields + const auto &event_fields { // (5.2) List of event fields to include. If this list is absent then all fields are // included. The entries may include '.' charaters to indicate sub-fields. So // ['content.body'] will include the 'body' field of the 'content' object. A literal '.' // character in a field name may be escaped using a '\'. A server may include more // fields than were requested. - request["event_fields"] + json::get(request) }; - const auto event_format + const auto &event_format { // (5.2) The format to use for events. 'client' will return the events in a format suitable // for clients. 'federation' will return the raw event as receieved over federation. // The default is 'client'. One of: ["client", "federation"] - request["event_format"] + json::get(request) }; - const auto account_data + const auto &account_data { // (5.2) The user account data that isn't associated with rooms to include. - request["account_data"] + json::val(request) }; - const auto room + const auto &room { // (5.2) Filters to be applied to room data. - request["room"] + json::val(request) }; - const auto presence + const auto &state + { + json::val(room) + }; + + std::cout << json::get(state) << std::endl; + + const auto &presence { // (5.2) The presence updates to include. - request["presence"] + json::val(request) }; - std::cout << "do filter: " << request.content << std::endl; + const auto filter_id + { + "abc123" + }; + json::iov event; + json::iov::push members[] + { + { event, json::member { "type", "ircd.filter" }}, + { event, json::member { "state_key", filter_id }}, + { event, json::member { "sender", user_id }}, + { event, json::member { "content", request.body }} + }; + + filters_room.send(event); return resource::response { client, http::CREATED, { - { "filter_id", "abc321" } + { "filter_id", filter_id } } }; } @@ -136,7 +182,10 @@ catch(...) resource::method post { - user_resource, "POST", post_filter + user_resource, "POST", post_filter, + { + post.REQUIRES_AUTH + } }; mapi::header IRCD_MODULE diff --git a/modules/client/voip/turnserver.cc b/modules/client/voip/turnserver.cc index fdcac5c79..0d140ee29 100644 --- a/modules/client/voip/turnserver.cc +++ b/modules/client/voip/turnserver.cc @@ -38,10 +38,7 @@ get_turnserver(client &client, const resource::request &request) { return resource::response { - client, - { - { }, - } + client, http::OK }; } diff --git a/modules/db/events.cc b/modules/db/events.cc index bf3864644..ecd95f5cd 100644 --- a/modules/db/events.cc +++ b/modules/db/events.cc @@ -278,10 +278,58 @@ const database::descriptor events_signatures_descriptor } }; -const database::descriptor index_room_id_to_event_id +/// prefix transform for event_id suffixes +/// +/// This transform expects a concatenation ending with an event_id which means +/// the prefix can be the same for multiple event_id's; therefor we can find +/// or iterate "event_id in X" where X is some key like a room_id +/// +const ircd::db::prefix_transform event_id_in +{ + "event_id in", + [](const string_view &key) + { + return key.find('$') != key.npos; + }, + [](const string_view &key) + { + return rsplit(key, '$').first; + } +}; + +const database::descriptor event_id_in_sender { // name - "!room_id$event_id", + "event_id in sender", + + // explanation + R"(### developer note: + + key is "@sender$event_id" + the prefix transform is in effect. this column indexes events by + sender offering an iterable bound of the index prefixed by sender + + )", + + // typing (key, value) + { + typeid(string_view), typeid(string_view) + }, + + // options + {}, + + // comparator + {}, + + // prefix transform + event_id_in, +}; + +const database::descriptor event_id_in_room_id +{ + // name + "event_id in room_id", // explanation R"(### developer note: @@ -300,27 +348,99 @@ const database::descriptor index_room_id_to_event_id // options {}, + // comparator - sorts from highest to lowest + ircd::db::reverse_cmp_string_view{}, + + // prefix transform + event_id_in, +}; + +/// prefix transform for room_id +/// +/// This transform expects a concatenation ending with a room_id which means +/// the prefix can be the same for multiple room_id's; therefor we can find +/// or iterate "room_id in X" where X is some repeated prefix +/// +const ircd::db::prefix_transform room_id_in +{ + "room_id in", + [](const string_view &key) + { + return key.find('!') != key.npos; + }, + [](const string_view &key) + { + return rsplit(key, '!').first; + } +}; + +const database::descriptor event_id_for_room_id_in_type +{ + // name + "event_id for room_id in type", + + // explanation + R"(### developer note: + + )", + + // typing (key, value) + { + typeid(string_view), typeid(string_view) + }, + + // options + {}, + // comparator {}, // prefix transform + room_id_in, +}; + +/// prefix transform for type,state_key in room_id +/// +/// This transform is special for concatenating room_id with type and state_key +/// in that order with prefix being the room_id (this may change to room_id+ +/// type +/// +const ircd::db::prefix_transform type_state_key_in_room_id +{ + "type,state_key in room_id", + [](const string_view &key) { - "!room_id$event_id"s, - [](const string_view &key) - { - return key.find('$') != key.npos; - }, - [](const string_view &key) - { - return split(key, '$').first; - } + return key.find("..") != key.npos; + }, + [](const string_view &key) + { + return split(key, "..").first; } +}; - // hooks -// { +const database::descriptor event_id_for_type_state_key_in_room_id +{ + // name + "event_id for type,state_key in room_id", + // explanation + R"(### developer note: -// } + )", + + // typing (key, value) + { + typeid(string_view), typeid(string_view) + }, + + // options + {}, + + // comparator + {}, + + // prefix transform + type_state_key_in_room_id }; const database::description events_description @@ -337,7 +457,10 @@ const database::description events_description events_prev_ids_descriptor, events_unsigned_descriptor, events_signatures_descriptor, - index_room_id_to_event_id, + event_id_in_sender, + event_id_in_room_id, + event_id_for_room_id_in_type, + event_id_for_type_state_key_in_room_id, }; std::shared_ptr events_database