2018-02-04 03:22:01 +01:00
|
|
|
// 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.
|
2017-11-16 02:37:09 +01:00
|
|
|
|
2017-11-30 20:44:23 +01:00
|
|
|
#include <ircd/m/m.h>
|
|
|
|
|
2017-11-16 02:37:09 +01:00
|
|
|
namespace ircd::m
|
|
|
|
{
|
|
|
|
struct log::log log
|
|
|
|
{
|
|
|
|
"matrix", 'm'
|
|
|
|
};
|
|
|
|
|
|
|
|
std::map<std::string, ircd::module> modules;
|
2017-11-30 19:51:01 +01:00
|
|
|
std::list<ircd::net::listener> listeners;
|
2017-11-16 02:37:09 +01:00
|
|
|
|
|
|
|
static void leave_ircd_room();
|
|
|
|
static void join_ircd_room();
|
|
|
|
}
|
|
|
|
|
|
|
|
const ircd::m::user::id::buf
|
|
|
|
ircd_user_id
|
|
|
|
{
|
|
|
|
"ircd", ircd::my_host() //TODO: hostname
|
|
|
|
};
|
|
|
|
|
|
|
|
ircd::m::user
|
|
|
|
ircd::m::me
|
|
|
|
{
|
|
|
|
ircd_user_id
|
|
|
|
};
|
|
|
|
|
|
|
|
const ircd::m::room::id::buf
|
|
|
|
ircd_room_id
|
|
|
|
{
|
|
|
|
"ircd", ircd::my_host()
|
|
|
|
};
|
|
|
|
|
|
|
|
ircd::m::room
|
|
|
|
ircd::m::my_room
|
|
|
|
{
|
|
|
|
ircd_room_id
|
|
|
|
};
|
|
|
|
|
|
|
|
const ircd::m::room::id::buf
|
|
|
|
control_room_id
|
|
|
|
{
|
|
|
|
"control", ircd::my_host()
|
|
|
|
};
|
|
|
|
|
|
|
|
ircd::m::room
|
|
|
|
ircd::m::control
|
|
|
|
{
|
|
|
|
control_room_id
|
|
|
|
};
|
|
|
|
|
2017-12-12 21:33:14 +01:00
|
|
|
//
|
|
|
|
// init
|
|
|
|
//
|
|
|
|
|
|
|
|
ircd::m::init::init()
|
|
|
|
try
|
|
|
|
:conf
|
2017-11-16 02:37:09 +01:00
|
|
|
{
|
2017-12-12 21:33:14 +01:00
|
|
|
ircd::conf
|
2017-11-16 02:37:09 +01:00
|
|
|
}
|
2017-12-12 21:33:14 +01:00
|
|
|
,_keys
|
2017-11-16 02:37:09 +01:00
|
|
|
{
|
2017-12-12 21:33:14 +01:00
|
|
|
conf
|
2017-11-16 02:37:09 +01:00
|
|
|
}
|
|
|
|
{
|
2017-12-12 21:33:14 +01:00
|
|
|
modules();
|
2018-02-09 08:21:15 +01:00
|
|
|
if(db::sequence(*dbs::events) == 0)
|
2017-12-12 21:33:14 +01:00
|
|
|
bootstrap();
|
2017-11-16 02:37:09 +01:00
|
|
|
|
2017-12-12 21:33:14 +01:00
|
|
|
listeners();
|
2017-11-16 02:37:09 +01:00
|
|
|
join_ircd_room();
|
|
|
|
}
|
|
|
|
catch(const m::error &e)
|
|
|
|
{
|
2017-11-30 19:51:01 +01:00
|
|
|
log.error("%s %s", e.what(), e.content);
|
|
|
|
throw;
|
|
|
|
}
|
|
|
|
catch(const std::exception &e)
|
|
|
|
{
|
|
|
|
log.error("%s", e.what());
|
2017-11-26 01:20:42 +01:00
|
|
|
throw;
|
2017-11-16 02:37:09 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
ircd::m::init::~init()
|
|
|
|
noexcept try
|
|
|
|
{
|
|
|
|
leave_ircd_room();
|
2017-12-12 21:33:14 +01:00
|
|
|
m::listeners.clear();
|
2017-11-30 19:51:01 +01:00
|
|
|
vm::fronts.map.clear();
|
2017-12-12 21:33:14 +01:00
|
|
|
m::modules.clear();
|
2017-11-16 02:37:09 +01:00
|
|
|
}
|
|
|
|
catch(const m::error &e)
|
|
|
|
{
|
|
|
|
log.critical("%s %s", e.what(), e.content);
|
2017-11-26 01:20:42 +01:00
|
|
|
ircd::terminate();
|
2017-11-16 02:37:09 +01:00
|
|
|
}
|
|
|
|
|
2017-12-12 21:33:14 +01:00
|
|
|
void
|
|
|
|
ircd::m::init::modules()
|
|
|
|
{
|
|
|
|
const string_view prefixes[]
|
|
|
|
{
|
|
|
|
"m_", "client_", "key_", "federation_", "media_"
|
|
|
|
};
|
|
|
|
|
|
|
|
for(const auto &name : mods::available())
|
|
|
|
if(startswith_any(name, std::begin(prefixes), std::end(prefixes)))
|
|
|
|
m::modules.emplace(name, name);
|
|
|
|
|
|
|
|
m::modules.emplace("root.so"s, "root.so"s);
|
|
|
|
}
|
|
|
|
|
2017-11-30 19:51:01 +01:00
|
|
|
namespace ircd::m
|
|
|
|
{
|
|
|
|
static void init_listener(const json::object &conf, const json::object &opts, const string_view &bindaddr);
|
|
|
|
static void init_listener(const json::object &conf, const json::object &opts);
|
|
|
|
}
|
|
|
|
|
2017-12-12 21:33:14 +01:00
|
|
|
void
|
|
|
|
ircd::m::init::listeners()
|
2017-11-30 19:51:01 +01:00
|
|
|
{
|
|
|
|
const json::array listeners
|
|
|
|
{
|
|
|
|
conf["listeners"]
|
|
|
|
};
|
|
|
|
|
2017-12-12 21:33:14 +01:00
|
|
|
if(m::listeners.empty())
|
2017-11-30 19:51:01 +01:00
|
|
|
init_listener(conf, {});
|
|
|
|
else
|
|
|
|
for(const json::object opts : listeners)
|
|
|
|
init_listener(conf, opts);
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
ircd::m::init_listener(const json::object &conf,
|
|
|
|
const json::object &opts)
|
|
|
|
{
|
|
|
|
const json::array binds
|
|
|
|
{
|
|
|
|
opts["bind_addresses"]
|
|
|
|
};
|
|
|
|
|
|
|
|
if(binds.empty())
|
|
|
|
init_listener(conf, opts, "0.0.0.0");
|
|
|
|
else
|
|
|
|
for(const auto &bindaddr : binds)
|
|
|
|
init_listener(conf, opts, unquote(bindaddr));
|
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
|
|
|
ircd::m::init_listener(const json::object &conf,
|
|
|
|
const json::object &opts,
|
|
|
|
const string_view &host)
|
|
|
|
{
|
|
|
|
const json::array resources
|
|
|
|
{
|
|
|
|
opts["resources"]
|
|
|
|
};
|
|
|
|
|
|
|
|
// resources has multiple names with different configs which are being
|
|
|
|
// ignored :-/
|
|
|
|
const std::string name{"Matrix"s};
|
|
|
|
|
|
|
|
// Translate synapse options to our options (which reflect asio::ssl)
|
|
|
|
const json::strung options{json::members
|
|
|
|
{
|
|
|
|
{ "name", name },
|
|
|
|
{ "host", host },
|
2018-02-10 05:09:37 +01:00
|
|
|
{ "port", opts.get("port", 8448L) },
|
2017-11-30 19:51:01 +01:00
|
|
|
{ "ssl_certificate_file_pem", conf["tls_certificate_path"] },
|
|
|
|
{ "ssl_private_key_file_pem", conf["tls_private_key_path"] },
|
|
|
|
{ "ssl_tmp_dh_file", conf["tls_dh_params_path"] },
|
|
|
|
}};
|
|
|
|
|
2017-12-12 21:33:14 +01:00
|
|
|
m::listeners.emplace_back(options);
|
2017-11-16 02:37:09 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
void
|
2017-12-12 21:33:14 +01:00
|
|
|
ircd::m::init::bootstrap()
|
2017-11-16 02:37:09 +01:00
|
|
|
{
|
2018-02-08 22:23:01 +01:00
|
|
|
assert(dbs::events);
|
|
|
|
assert(db::sequence(*dbs::events) == 0);
|
2017-11-16 02:37:09 +01:00
|
|
|
|
|
|
|
ircd::log::notice
|
|
|
|
(
|
|
|
|
"This appears to be your first time running IRCd because the events "
|
|
|
|
"database is empty. I will be bootstrapping it with initial events now..."
|
|
|
|
);
|
|
|
|
|
|
|
|
create(my_room, me.user_id);
|
2017-11-30 19:51:01 +01:00
|
|
|
send(my_room, me.user_id, "m.room.name", "",
|
|
|
|
{
|
|
|
|
{ "name", "IRCd's Room" }
|
|
|
|
});
|
|
|
|
|
2017-11-16 02:37:09 +01:00
|
|
|
create(control, me.user_id);
|
2017-11-30 19:51:01 +01:00
|
|
|
send(control, me.user_id, "m.room.name", "",
|
|
|
|
{
|
|
|
|
{ "name", "Control Room" }
|
|
|
|
});
|
|
|
|
|
2017-11-16 02:37:09 +01:00
|
|
|
create(user::accounts, me.user_id);
|
2017-11-30 19:51:01 +01:00
|
|
|
join(user::accounts, me.user_id);
|
|
|
|
send(user::accounts, me.user_id, "m.room.name", "",
|
|
|
|
{
|
|
|
|
{ "name", "User Accounts" }
|
|
|
|
});
|
|
|
|
|
2017-11-16 02:37:09 +01:00
|
|
|
create(user::sessions, me.user_id);
|
2017-11-30 19:51:01 +01:00
|
|
|
send(user::sessions, me.user_id, "m.room.name", "",
|
|
|
|
{
|
|
|
|
{ "name", "User Sessions" }
|
|
|
|
});
|
|
|
|
|
2017-11-16 02:37:09 +01:00
|
|
|
create(filter::filters, me.user_id);
|
2017-11-30 19:51:01 +01:00
|
|
|
send(filter::filters, me.user_id, "m.room.name", "",
|
|
|
|
{
|
|
|
|
{ "name", "User Filters Database" }
|
|
|
|
});
|
|
|
|
|
2017-12-12 21:33:14 +01:00
|
|
|
_keys.bootstrap();
|
2017-11-16 02:37:09 +01:00
|
|
|
|
|
|
|
message(control, me.user_id, "Welcome to the control room.");
|
|
|
|
message(control, me.user_id, "I am the daemon. You can talk to me in this room by highlighting me.");
|
|
|
|
}
|
|
|
|
|
2017-12-12 21:33:14 +01:00
|
|
|
bool
|
|
|
|
ircd::m::self::host(const string_view &s)
|
|
|
|
{
|
|
|
|
return s == host();
|
|
|
|
}
|
|
|
|
|
|
|
|
ircd::string_view
|
|
|
|
ircd::m::self::host()
|
|
|
|
{
|
|
|
|
return "zemos.net"; //me.user_id.host();
|
|
|
|
}
|
|
|
|
|
|
|
|
void
|
|
|
|
ircd::m::join_ircd_room()
|
|
|
|
try
|
|
|
|
{
|
|
|
|
join(my_room, me.user_id);
|
|
|
|
my_room.get([](const auto &event)
|
|
|
|
{
|
|
|
|
std::cout << "mr G: " << event << std::endl;
|
|
|
|
});
|
|
|
|
|
|
|
|
my_room.prev([](const auto &event)
|
|
|
|
{
|
|
|
|
std::cout << "mr P: " << event << std::endl;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
catch(const m::ALREADY_MEMBER &e)
|
|
|
|
{
|
|
|
|
log.warning("IRCd did not shut down correctly...");
|
|
|
|
}
|
|
|
|
|
|
|
|
void
|
|
|
|
ircd::m::leave_ircd_room()
|
|
|
|
{
|
|
|
|
leave(my_room, me.user_id);
|
|
|
|
}
|
|
|
|
|
2017-11-16 02:37:09 +01:00
|
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
|
|
//
|
2017-12-12 21:33:14 +01:00
|
|
|
// m/filter.h
|
2017-11-16 02:37:09 +01:00
|
|
|
//
|
|
|
|
|
|
|
|
const ircd::m::room::id::buf
|
2017-12-12 21:33:14 +01:00
|
|
|
filters_room_id
|
2017-11-16 02:37:09 +01:00
|
|
|
{
|
2017-12-12 21:33:14 +01:00
|
|
|
"filters", ircd::my_host()
|
2017-11-16 02:37:09 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
ircd::m::room
|
2017-12-12 21:33:14 +01:00
|
|
|
ircd::m::filter::filters
|
2017-11-16 02:37:09 +01:00
|
|
|
{
|
2017-12-12 21:33:14 +01:00
|
|
|
filters_room_id
|
2017-11-16 02:37:09 +01:00
|
|
|
};
|
|
|
|
|
2017-12-12 21:33:14 +01:00
|
|
|
ircd::m::filter::filter(const string_view &filter_id,
|
|
|
|
const mutable_buffer &buf)
|
2017-11-16 02:37:09 +01:00
|
|
|
{
|
2017-12-12 21:33:14 +01:00
|
|
|
const m::vm::query<m::vm::where::equal> query
|
2017-11-16 02:37:09 +01:00
|
|
|
{
|
2017-12-12 21:33:14 +01:00
|
|
|
{ "room_id", filters.room_id },
|
|
|
|
{ "type", "ircd.filter" },
|
|
|
|
{ "state_key", filter_id },
|
2017-11-16 02:37:09 +01:00
|
|
|
};
|
|
|
|
|
2017-12-12 21:33:14 +01:00
|
|
|
size_t len{0};
|
|
|
|
m::vm::test(query, [&buf, &len]
|
|
|
|
(const auto &event)
|
2017-11-16 02:37:09 +01:00
|
|
|
{
|
2017-12-12 21:33:14 +01:00
|
|
|
len = copy(buf, json::get<"content"_>(event));
|
|
|
|
return true;
|
|
|
|
});
|
2017-11-16 02:37:09 +01:00
|
|
|
|
2017-12-12 21:33:14 +01:00
|
|
|
new (this) filter{json::object{buf}};
|
2017-11-16 02:37:09 +01:00
|
|
|
}
|
|
|
|
|
2017-12-12 21:33:14 +01:00
|
|
|
size_t
|
|
|
|
ircd::m::filter::size(const string_view &filter_id)
|
2017-11-16 02:37:09 +01:00
|
|
|
{
|
2017-12-12 21:33:14 +01:00
|
|
|
const m::vm::query<m::vm::where::equal> query
|
2017-11-16 02:37:09 +01:00
|
|
|
{
|
2017-12-12 21:33:14 +01:00
|
|
|
{ "room_id", filters.room_id },
|
|
|
|
{ "type", "ircd.filter" },
|
|
|
|
{ "state_key", filter_id },
|
2017-11-16 02:37:09 +01:00
|
|
|
};
|
|
|
|
|
2017-12-12 21:33:14 +01:00
|
|
|
size_t ret{0};
|
|
|
|
m::vm::test(query, [&ret]
|
|
|
|
(const auto &event)
|
2017-11-16 02:37:09 +01:00
|
|
|
{
|
2017-12-12 21:33:14 +01:00
|
|
|
const string_view content
|
|
|
|
{
|
|
|
|
json::get<"content"_>(event)
|
|
|
|
};
|
2017-11-16 02:37:09 +01:00
|
|
|
|
2017-12-12 21:33:14 +01:00
|
|
|
ret = content.size();
|
|
|
|
return true;
|
|
|
|
});
|
2017-11-16 02:37:09 +01:00
|
|
|
|
2017-12-12 21:33:14 +01:00
|
|
|
return ret;
|
2017-11-16 02:37:09 +01:00
|
|
|
}
|
|
|
|
|
2017-12-12 21:33:14 +01:00
|
|
|
///////////////////////////////////////////////////////////////////////////////
|
|
|
|
//
|
|
|
|
// m/user.h
|
|
|
|
//
|
|
|
|
|
|
|
|
const ircd::m::room::id::buf
|
|
|
|
accounts_room_id
|
2017-11-16 02:37:09 +01:00
|
|
|
{
|
2017-12-12 21:33:14 +01:00
|
|
|
"accounts", ircd::my_host()
|
|
|
|
};
|
2017-11-16 02:37:09 +01:00
|
|
|
|
2017-12-12 21:33:14 +01:00
|
|
|
ircd::m::room
|
|
|
|
ircd::m::user::accounts
|
|
|
|
{
|
|
|
|
accounts_room_id
|
|
|
|
};
|
2017-11-16 02:37:09 +01:00
|
|
|
|
2017-12-12 21:33:14 +01:00
|
|
|
const ircd::m::room::id::buf
|
|
|
|
sessions_room_id
|
|
|
|
{
|
|
|
|
"sessions", ircd::my_host()
|
|
|
|
};
|
2017-11-16 02:37:09 +01:00
|
|
|
|
2017-12-12 21:33:14 +01:00
|
|
|
ircd::m::room
|
|
|
|
ircd::m::user::sessions
|
|
|
|
{
|
|
|
|
sessions_room_id
|
|
|
|
};
|
2017-11-16 02:37:09 +01:00
|
|
|
|
2017-12-12 21:33:14 +01:00
|
|
|
/// Register the user by joining them to the accounts room.
|
|
|
|
///
|
|
|
|
/// The content of the join event may store keys including the registration
|
|
|
|
/// options. Once this call completes the join was successful and the user is
|
|
|
|
/// registered, otherwise throws.
|
|
|
|
void
|
|
|
|
ircd::m::user::activate(const json::members &contents)
|
|
|
|
try
|
|
|
|
{
|
|
|
|
json::iov content;
|
|
|
|
json::iov::push push[]
|
2017-11-16 02:37:09 +01:00
|
|
|
{
|
2017-12-12 21:33:14 +01:00
|
|
|
{ content, { "membership", "join" }},
|
2017-11-16 02:37:09 +01:00
|
|
|
};
|
|
|
|
|
2017-12-12 21:33:14 +01:00
|
|
|
size_t i(0);
|
|
|
|
json::iov::push _content[contents.size()];
|
|
|
|
for(const auto &member : contents)
|
|
|
|
new (_content + i++) json::iov::push(content, member);
|
2017-11-16 02:37:09 +01:00
|
|
|
|
2017-12-12 21:33:14 +01:00
|
|
|
send(accounts, me.user_id, "ircd.user", user_id, content);
|
|
|
|
join(control, user_id);
|
|
|
|
}
|
|
|
|
catch(const m::ALREADY_MEMBER &e)
|
|
|
|
{
|
|
|
|
throw m::error
|
2017-11-16 02:37:09 +01:00
|
|
|
{
|
2017-12-12 21:33:14 +01:00
|
|
|
http::CONFLICT, "M_USER_IN_USE", "The desired user ID is already in use."
|
2017-11-16 02:37:09 +01:00
|
|
|
};
|
2017-12-12 21:33:14 +01:00
|
|
|
}
|
2017-11-16 02:37:09 +01:00
|
|
|
|
2017-12-12 21:33:14 +01:00
|
|
|
void
|
|
|
|
ircd::m::user::deactivate(const json::members &contents)
|
|
|
|
{
|
|
|
|
json::iov content;
|
|
|
|
json::iov::push push[]
|
2017-11-16 02:37:09 +01:00
|
|
|
{
|
2017-12-12 21:33:14 +01:00
|
|
|
{ content, { "membership", "leave" }},
|
2017-11-16 02:37:09 +01:00
|
|
|
};
|
|
|
|
|
2017-12-12 21:33:14 +01:00
|
|
|
size_t i(0);
|
|
|
|
json::iov::push _content[contents.size()];
|
|
|
|
for(const auto &member : contents)
|
|
|
|
new (_content + i++) json::iov::push(content, member);
|
2017-11-16 02:37:09 +01:00
|
|
|
|
2017-12-12 21:33:14 +01:00
|
|
|
send(accounts, me.user_id, "ircd.user", user_id, content);
|
2017-11-16 02:37:09 +01:00
|
|
|
}
|
|
|
|
|
2017-11-25 23:30:03 +01:00
|
|
|
void
|
2017-12-12 21:33:14 +01:00
|
|
|
ircd::m::user::password(const string_view &password)
|
2017-11-16 02:37:09 +01:00
|
|
|
try
|
|
|
|
{
|
2017-11-30 19:51:01 +01:00
|
|
|
//TODO: ADD SALT
|
2018-02-03 08:20:26 +01:00
|
|
|
char b64[64], hash[32];
|
|
|
|
sha256{hash, password};
|
2017-11-16 03:18:25 +01:00
|
|
|
const auto digest{b64encode_unpadded(b64, hash)};
|
2017-11-30 19:51:01 +01:00
|
|
|
send(accounts, me.user_id, "ircd.password", user_id,
|
2017-11-16 03:18:25 +01:00
|
|
|
{
|
2017-11-30 19:51:01 +01:00
|
|
|
{ "sha256", digest }
|
|
|
|
});
|
2017-11-16 02:37:09 +01:00
|
|
|
}
|
|
|
|
catch(const m::ALREADY_MEMBER &e)
|
|
|
|
{
|
|
|
|
throw m::error
|
|
|
|
{
|
|
|
|
http::CONFLICT, "M_USER_IN_USE", "The desired user ID is already in use."
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
bool
|
|
|
|
ircd::m::user::is_password(const string_view &supplied_password)
|
|
|
|
const
|
|
|
|
{
|
2017-11-30 19:51:01 +01:00
|
|
|
//TODO: ADD SALT
|
2018-02-03 08:20:26 +01:00
|
|
|
char b64[64], hash[32];
|
|
|
|
sha256{hash, supplied_password};
|
2018-02-09 21:19:45 +01:00
|
|
|
const auto supplied_hash
|
|
|
|
{
|
|
|
|
b64encode_unpadded(b64, hash)
|
|
|
|
};
|
|
|
|
|
|
|
|
static const string_view type{"ircd.password"};
|
|
|
|
const string_view &state_key{user_id};
|
|
|
|
const room room
|
|
|
|
{
|
|
|
|
accounts.room_id
|
|
|
|
};
|
|
|
|
|
|
|
|
bool ret{false};
|
|
|
|
room.get(type, state_key, [&supplied_hash, &ret]
|
|
|
|
(const m::event &event)
|
2017-11-16 02:37:09 +01:00
|
|
|
{
|
|
|
|
const json::object &content
|
|
|
|
{
|
|
|
|
json::at<"content"_>(event)
|
|
|
|
};
|
|
|
|
|
2017-11-16 03:18:25 +01:00
|
|
|
const auto &correct_hash
|
2017-11-16 02:37:09 +01:00
|
|
|
{
|
2017-11-16 03:18:25 +01:00
|
|
|
unquote(content.at("sha256"))
|
2017-11-16 02:37:09 +01:00
|
|
|
};
|
|
|
|
|
2018-02-09 21:19:45 +01:00
|
|
|
ret = supplied_hash == correct_hash;
|
|
|
|
});
|
2017-11-16 02:37:09 +01:00
|
|
|
|
2018-02-09 21:19:45 +01:00
|
|
|
return ret;
|
2017-11-16 02:37:09 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
bool
|
|
|
|
ircd::m::user::is_active()
|
|
|
|
const
|
|
|
|
{
|
2017-11-30 19:51:01 +01:00
|
|
|
bool ret{false};
|
|
|
|
accounts.get("ircd.user", user_id, [&ret]
|
|
|
|
(const auto &event)
|
|
|
|
{
|
|
|
|
const json::object &content
|
|
|
|
{
|
|
|
|
at<"content"_>(event)
|
|
|
|
};
|
|
|
|
|
|
|
|
ret = unquote(content.at("membership")) == "join";
|
|
|
|
});
|
|
|
|
|
|
|
|
return ret;
|
2017-11-16 02:37:09 +01:00
|
|
|
}
|